From 705dd89ba2bcfbccbb3123e4facb261d0dc1bb5a Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 06:08:27 +0200 Subject: [PATCH 01/55] Define authoritative experiment binding contracts --- .ground-control.yaml | 1 - .../invalid/duplicate-canonical-target.json | 51 + .../valid/reference.json | 28 + .../invalid/digest-mismatch.json | 31 + .../valid/reference.json | 49 + .../valid/reference.json | 47 + .../entries/backend-manifest-v2.json | 6 +- .../entries/backend-profile-v1.json | 6 +- .../experiment-authoring-input-v1.json | 6 +- .../experiment-binding-descriptors-v1.json | 10 + .../entries/experiment-run-v1.json | 6 +- .../participant-configuration-result-v1.json | 10 + ...articipant-implementation-manifest-v1.json | 6 +- ...ticipant-implementation-provenance-v1.json | 6 +- .../entries/processor-manifest-v2.json | 6 +- .../backend-manifest/backend-manifest-v2.json | 184 +++ .../experiment-authoring-input-v1.json | 440 ++++++ .../experiment-binding-descriptors-v1.json | 374 +++++ .../experiment-core/experiment-run-v1.json | 397 +++++ .../participant-configuration-result-v1.json | 282 ++++ ...articipant-implementation-manifest-v1.json | 185 +++ ...ticipant-implementation-provenance-v1.json | 30 + .../processor-manifest-v2.json | 184 +++ .../schemas/profiles/backend-profile-v1.json | 1 + docs/decisions/adrs/README.md | 2 + ...itative-cross-plane-experiment-bindings.md | 123 ++ docs/decisions/adrs/adr-index.yaml | 3 + ...-experiment-binding-contracts-preflight.md | 468 ++++++ docs/explain/reference/README.md | 3 + .../reference/canonical-reference-map.md | 2 + .../reference/experiment-binding-contracts.md | 81 ++ docs/index.md | 1 + .../packages/raes_backend_stubs/manifest.py | 5 +- .../conformance/validators.py | 4 + .../raes_contracts/contracts/__init__.py | 26 +- .../raes_contracts/contracts/bundle.py | 6 + .../contracts/experiment_bindings.py | 426 ++++++ .../contracts/experiment_capture.py | 7 + .../contracts/experiment_disclosure.py | 8 + .../contracts/experiment_run.py | 3 + .../contracts/experiment_spec.py | 80 + .../raes_contracts/contracts/manifests.py | 19 +- .../contracts/participant_manifests.py | 67 +- .../raes_contracts/experiment_bindings.py | 176 +++ .../raes_contracts/manifest_authority.py | 4 + .../participant_configuration.py | 156 ++ .../packages/raes_contracts/versions.py | 2 + .../raes_reference_backend/manifest.py | 4 +- .../python/tests/test_backend_manifest.py | 4 +- .../python/tests/test_experiment_bindings.py | 1290 +++++++++++++++++ ...est_participant_implementation_manifest.py | 36 +- .../tests/test_sem_227_shared_time_model.py | 3 +- tools/generate_contract_schemas.py | 2 + 53 files changed, 5318 insertions(+), 39 deletions(-) create mode 100644 contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/invalid/duplicate-canonical-target.json create mode 100644 contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/valid/reference.json create mode 100644 contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/invalid/digest-mismatch.json create mode 100644 contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/valid/reference.json create mode 100644 contracts/schema-publication/entries/experiment-binding-descriptors-v1.json create mode 100644 contracts/schema-publication/entries/participant-configuration-result-v1.json create mode 100644 contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json create mode 100644 contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json create mode 100644 docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md create mode 100644 docs/decisions/issue-903-experiment-binding-contracts-preflight.md create mode 100644 docs/explain/reference/experiment-binding-contracts.md create mode 100644 implementations/python/packages/raes_contracts/contracts/experiment_bindings.py create mode 100644 implementations/python/packages/raes_contracts/experiment_bindings.py create mode 100644 implementations/python/packages/raes_contracts/participant_configuration.py create mode 100644 implementations/python/tests/test_experiment_bindings.py diff --git a/.ground-control.yaml b/.ground-control.yaml index 35764b894..eb852fb8f 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -25,7 +25,6 @@ requirements: routing: enabled: true default_provider: claude - default_fallback: parent stages: {} sonarcloud: project_key: Brad-Edwards_aces diff --git a/contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/invalid/duplicate-canonical-target.json b/contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/invalid/duplicate-canonical-target.json new file mode 100644 index 000000000..22904be56 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/invalid/duplicate-canonical-target.json @@ -0,0 +1,51 @@ +{ + "schema_version": "experiment-binding-descriptors/v1", + "descriptors": [ + { + "binding_id": "binding.worker-count.first", + "source_factor_id": "factor.worker-count", + "source_factor_level_id": "four", + "source_condition_id": "condition.four-workers", + "target": { + "plane": "scenario", + "scenario_family_id": "family.techvault", + "variation_point_id": "variation.worker-count", + "target_id": "variables.worker_count" + }, + "value_type": "integer", + "value": { + "kind": "literal", + "value": 4 + }, + "owner": { + "contract_id": "sdl-authoring-input-v1", + "contract_version": "1", + "validator_id": "raes-sdl-instantiation", + "validator_version": "1" + } + }, + { + "binding_id": "binding.worker-count.second", + "source_factor_id": "factor.worker-count", + "source_factor_level_id": "four", + "source_condition_id": "condition.four-workers", + "target": { + "plane": "scenario", + "scenario_family_id": "family.techvault", + "variation_point_id": "variation.worker-count", + "target_id": "variables.worker_count" + }, + "value_type": "integer", + "value": { + "kind": "literal", + "value": 4 + }, + "owner": { + "contract_id": "sdl-authoring-input-v1", + "contract_version": "1", + "validator_id": "raes-sdl-instantiation", + "validator_version": "1" + } + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/valid/reference.json new file mode 100644 index 000000000..33ec2c6c1 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-binding-descriptors-v1/valid/reference.json @@ -0,0 +1,28 @@ +{ + "schema_version": "experiment-binding-descriptors/v1", + "descriptors": [ + { + "binding_id": "binding.worker-count", + "source_factor_id": "factor.worker-count", + "source_factor_level_id": "four", + "source_condition_id": "condition.four-workers", + "target": { + "plane": "scenario", + "scenario_family_id": "family.techvault", + "variation_point_id": "variation.worker-count", + "target_id": "variables.worker_count" + }, + "value_type": "integer", + "value": { + "kind": "literal", + "value": 4 + }, + "owner": { + "contract_id": "sdl-authoring-input-v1", + "contract_version": "1", + "validator_id": "raes-sdl-instantiation", + "validator_version": "1" + } + } + ] +} diff --git a/contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/invalid/digest-mismatch.json b/contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/invalid/digest-mismatch.json new file mode 100644 index 000000000..dd1cd1708 --- /dev/null +++ b/contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/invalid/digest-mismatch.json @@ -0,0 +1,31 @@ +{ + "schema_version": "participant-configuration-result/v1", + "participant_address": "participants.red", + "manifest_ref": "manifests/reference-red-agent.json", + "manifest_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "configuration": { + "implementation_identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_version": "participant-implementation-manifest/v1", + "owner": { + "contract_id": "participant-configuration-result-v1", + "contract_version": "1", + "validator_id": "reference-participant-configuration", + "validator_version": "1" + }, + "values": [ + { + "target_id": "policy.mode", + "value_type": "string", + "origin": "override", + "value": { + "kind": "literal", + "value": "deterministic" + } + } + ] + }, + "configuration_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/valid/reference.json b/contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/valid/reference.json new file mode 100644 index 000000000..51992fe97 --- /dev/null +++ b/contracts/fixtures/participant-implementation-configuration/participant-configuration-result-v1/valid/reference.json @@ -0,0 +1,49 @@ +{ + "schema_version": "participant-configuration-result/v1", + "participant_address": "participants.red", + "manifest_ref": "manifests/reference-red-agent.json", + "manifest_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "configuration": { + "implementation_identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_version": "participant-implementation-manifest/v1", + "owner": { + "contract_id": "participant-configuration-result-v1", + "contract_version": "1", + "validator_id": "reference-participant-configuration", + "validator_version": "1" + }, + "values": [ + { + "target_id": "credentials.api", + "value_type": "string", + "origin": "override", + "value": { + "kind": "secret-reference", + "reference_id": "operator-secret.reference-red-api" + } + }, + { + "target_id": "policy.mode", + "value_type": "string", + "origin": "override", + "value": { + "kind": "literal", + "value": "deterministic" + } + }, + { + "target_id": "policy.temperature", + "value_type": "number", + "origin": "default", + "value": { + "kind": "literal", + "value": 0.25 + } + } + ] + }, + "configuration_digest": "sha256:3949cb272258d96d9506f23077c89cbb77d4dad2d0218fed74145bbd5a971655" +} diff --git a/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json b/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json index b0ed7a333..0b70db930 100644 --- a/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json +++ b/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json @@ -8,6 +8,8 @@ "supported_contract_versions": [ "participant-implementation-manifest-v1", "participant-implementation-provenance-v1", + "experiment-binding-descriptors-v1", + "participant-configuration-result-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1" @@ -66,5 +68,50 @@ "task-statement", "observation-stream" ] + }, + "configuration_registry": { + "owner": { + "contract_id": "participant-configuration-result-v1", + "contract_version": "1", + "validator_id": "reference-participant-configuration", + "validator_version": "1" + }, + "targets": { + "policy.temperature": { + "target_id": "policy.temperature", + "value_type": "number", + "aliases": [ + "temperature" + ], + "allowed_value_kinds": [ + "literal" + ], + "sensitivity": "public", + "default": { + "kind": "literal", + "value": 0.25 + } + }, + "policy.mode": { + "target_id": "policy.mode", + "value_type": "string", + "aliases": [ + "mode" + ], + "allowed_value_kinds": [ + "literal" + ], + "sensitivity": "internal" + }, + "credentials.api": { + "target_id": "credentials.api", + "value_type": "string", + "aliases": [], + "allowed_value_kinds": [ + "secret-reference" + ], + "sensitivity": "secret" + } + } } } diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index 5316d983f..f5d62636a 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": "178db9d22d522e069bcad2bc91cfb985e58a22e01ee5dc2b8de506373c1952f0", + "content_hash": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48", "last_change": { - "summary": "Published service materialization, portable shared-time, and autonomous participant capability controls.", - "content_hash": "178db9d22d522e069bcad2bc91cfb985e58a22e01ee5dc2b8de506373c1952f0" + "summary": "Added a typed, owner-governed apparatus configuration target registry and experiment binding contract support.", + "content_hash": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48" } } diff --git a/contracts/schema-publication/entries/backend-profile-v1.json b/contracts/schema-publication/entries/backend-profile-v1.json index 1b8ccbf53..2998372de 100644 --- a/contracts/schema-publication/entries/backend-profile-v1.json +++ b/contracts/schema-publication/entries/backend-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "05f507351c826558d03cec48b0e99501022acbbd5b6e1d816f86727e9f4ad78d", + "content_hash": "fd118e131b273684ac540675a8ba8d4b6ef1f10990ddd0d0dc5776c1b6c4a233", "last_change": { - "summary": "Added API-421 shared-time contract ids to the governed backend contract vocabulary.", - "content_hash": "05f507351c826558d03cec48b0e99501022acbbd5b6e1d816f86727e9f4ad78d" + "summary": "Added the authoritative experiment binding descriptor contract to the governed backend contract vocabulary.", + "content_hash": "fd118e131b273684ac540675a8ba8d4b6ef1f10990ddd0d0dc5776c1b6c4a233" } } diff --git a/contracts/schema-publication/entries/experiment-authoring-input-v1.json b/contracts/schema-publication/entries/experiment-authoring-input-v1.json index 2e471e6b3..e31bcdbde 100644 --- a/contracts/schema-publication/entries/experiment-authoring-input-v1.json +++ b/contracts/schema-publication/entries/experiment-authoring-input-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-authoring-input-v1", "schema_path": "contracts/schemas/experiment-core/experiment-authoring-input-v1.json", "stability": "draft", - "content_hash": "829ac69c37136596506bf83e0f2fde6cd8c333a79d497c84b93b0497e37aabc9", + "content_hash": "47a2a28bd72c252aff22a651e58b216cd4700e9d7c4cabc87728bce0657183a6", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "829ac69c37136596506bf83e0f2fde6cd8c333a79d497c84b93b0497e37aabc9" + "summary": "Added explicit binding semantics and authoritative cross-plane binding descriptors joined to factor levels and conditions.", + "content_hash": "47a2a28bd72c252aff22a651e58b216cd4700e9d7c4cabc87728bce0657183a6" } } diff --git a/contracts/schema-publication/entries/experiment-binding-descriptors-v1.json b/contracts/schema-publication/entries/experiment-binding-descriptors-v1.json new file mode 100644 index 000000000..7b77375c6 --- /dev/null +++ b/contracts/schema-publication/entries/experiment-binding-descriptors-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "experiment-binding-descriptors-v1", + "schema_path": "contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json", + "stability": "draft", + "content_hash": "88d57279e1d4467de97fa32893dfad58f3dccb5bf6a9d6e9610ce84e78265f2c", + "last_change": { + "summary": "Published explicit, typed, plane-specific experiment binding descriptors with factor and condition provenance.", + "content_hash": "88d57279e1d4467de97fa32893dfad58f3dccb5bf6a9d6e9610ce84e78265f2c" + } +} diff --git a/contracts/schema-publication/entries/experiment-run-v1.json b/contracts/schema-publication/entries/experiment-run-v1.json index e4ad3dece..a84977cc5 100644 --- a/contracts/schema-publication/entries/experiment-run-v1.json +++ b/contracts/schema-publication/entries/experiment-run-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-run-v1", "schema_path": "contracts/schemas/experiment-core/experiment-run-v1.json", "stability": "draft", - "content_hash": "5c936907be31c0b1d0f7071ecd1785a39804afb52f6277671c11c95d325baf16", + "content_hash": "718d8b108bfc7cc7cee4d55970c12e7fd87c3873f13aaee51b2b4de468ba5db7", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "5c936907be31c0b1d0f7071ecd1785a39804afb52f6277671c11c95d325baf16" + "summary": "Added realized cross-plane binding provenance with explicit source, target, value origin, and configuration digest.", + "content_hash": "718d8b108bfc7cc7cee4d55970c12e7fd87c3873f13aaee51b2b4de468ba5db7" } } diff --git a/contracts/schema-publication/entries/participant-configuration-result-v1.json b/contracts/schema-publication/entries/participant-configuration-result-v1.json new file mode 100644 index 000000000..7a421024d --- /dev/null +++ b/contracts/schema-publication/entries/participant-configuration-result-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-configuration-result-v1", + "schema_path": "contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json", + "stability": "draft", + "content_hash": "915c2557c352fc5866fa0e5171abb02afb0b9eea485749aefa94ba0bc0dc3c51", + "last_change": { + "summary": "Published atomic normalized participant configuration results with value provenance and a canonical digest.", + "content_hash": "915c2557c352fc5866fa0e5171abb02afb0b9eea485749aefa94ba0bc0dc3c51" + } +} diff --git a/contracts/schema-publication/entries/participant-implementation-manifest-v1.json b/contracts/schema-publication/entries/participant-implementation-manifest-v1.json index e3029d604..f6578bf51 100644 --- a/contracts/schema-publication/entries/participant-implementation-manifest-v1.json +++ b/contracts/schema-publication/entries/participant-implementation-manifest-v1.json @@ -2,5 +2,9 @@ "contract_id": "participant-implementation-manifest-v1", "schema_path": "contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json", "stability": "draft", - "content_hash": "93e30291adbc08b1969adb9b938f888b004d317ac864e90151d0f43a395948e4" + "content_hash": "5b7c636a98c540e1afc74ca74a1e9a421609f498c3b85c473a52c1516e398984", + "last_change": { + "summary": "Added typed participant configuration targets, aliases, defaults, sensitivity, and governed validator ownership.", + "content_hash": "5b7c636a98c540e1afc74ca74a1e9a421609f498c3b85c473a52c1516e398984" + } } diff --git a/contracts/schema-publication/entries/participant-implementation-provenance-v1.json b/contracts/schema-publication/entries/participant-implementation-provenance-v1.json index a84cc539e..7084e0330 100644 --- a/contracts/schema-publication/entries/participant-implementation-provenance-v1.json +++ b/contracts/schema-publication/entries/participant-implementation-provenance-v1.json @@ -2,5 +2,9 @@ "contract_id": "participant-implementation-provenance-v1", "schema_path": "contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json", "stability": "draft", - "content_hash": "a4c050fb2a53129148f7d2960a7cb06d36c4682483064734e505acda4677d7ec" + "content_hash": "88076dc67b837b282a0efe482d4b74d5fb053ad7b44cb6bc0d862b4fe17eb30e", + "last_change": { + "summary": "Required participant configuration references and authoritative normalized configuration digests to be supplied together.", + "content_hash": "88076dc67b837b282a0efe482d4b74d5fb053ad7b44cb6bc0d862b4fe17eb30e" + } } diff --git a/contracts/schema-publication/entries/processor-manifest-v2.json b/contracts/schema-publication/entries/processor-manifest-v2.json index fc6e34b48..e33ec7646 100644 --- a/contracts/schema-publication/entries/processor-manifest-v2.json +++ b/contracts/schema-publication/entries/processor-manifest-v2.json @@ -2,9 +2,9 @@ "contract_id": "processor-manifest-v2", "schema_path": "contracts/schemas/processor-manifest/processor-manifest-v2.json", "stability": "draft", - "content_hash": "504f5dee2028bbcd49c023c1c57c8ad664f6ea57f51e8c2431e8a185ae275e87", + "content_hash": "bd2e67b226d6eb18eb1cfec69e8840e7d8349161f3357fa71832771f9b8e68dc", "last_change": { - "summary": "Added API-421 time-model-v1 to the processor contract surface.", - "content_hash": "504f5dee2028bbcd49c023c1c57c8ad664f6ea57f51e8c2431e8a185ae275e87" + "summary": "Added a typed, owner-governed apparatus configuration target registry and experiment binding contract support.", + "content_hash": "bd2e67b226d6eb18eb1cfec69e8840e7d8349161f3357fa71832771f9b8e68dc" } } diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index 0918682f5..11b6c1de7 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -119,6 +119,52 @@ "title": "BackendCompatibilityModel", "type": "object" }, + "BindingOwnerModel": { + "additionalProperties": false, + "description": "Governed contract and validator profile that owns one binding.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "contract_version": { + "minLength": 1, + "title": "Contract Version", + "type": "string" + }, + "validator_id": { + "minLength": 1, + "title": "Validator Id", + "type": "string" + }, + "validator_version": { + "minLength": 1, + "title": "Validator Version", + "type": "string" + } + }, + "required": [ + "contract_id", + "contract_version", + "validator_id", + "validator_version" + ], + "title": "BindingOwnerModel", + "type": "object" + }, + "BindingScalarType": { + "description": "Exact JSON scalar type declared by a binding owner.", + "enum": [ + "string", + "integer", + "number", + "boolean", + "null" + ], + "title": "BindingScalarType", + "type": "string" + }, "CleanupCapabilitiesModel": { "additionalProperties": false, "description": "Backend support for the portable SCE-007 cleanup contract family.", @@ -209,6 +255,95 @@ "title": "ConceptBindingEntryModel", "type": "object" }, + "ConfigurationTargetDeclarationModel": { + "additionalProperties": false, + "description": "One scalar target admitted by an owning portable manifest.", + "properties": { + "aliases": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Aliases", + "type": "array" + }, + "allowed_value_kinds": { + "items": { + "enum": [ + "literal", + "secret-reference" + ], + "type": "string" + }, + "minItems": 1, + "title": "Allowed Value Kinds", + "type": "array" + }, + "default": { + "anyOf": [ + { + "$ref": "#/$defs/LiteralBindingValueModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "restricted", + "secret" + ], + "title": "Sensitivity", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "value_type": { + "$ref": "#/$defs/BindingScalarType" + } + }, + "required": [ + "target_id", + "value_type", + "allowed_value_kinds", + "sensitivity" + ], + "title": "ConfigurationTargetDeclarationModel", + "type": "object" + }, + "ConfigurationTargetRegistryModel": { + "additionalProperties": false, + "description": "Collision-free target registry published by one manifest owner.", + "properties": { + "owner": { + "$ref": "#/$defs/BindingOwnerModel" + }, + "targets": { + "additionalProperties": { + "$ref": "#/$defs/ConfigurationTargetDeclarationModel" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Targets", + "type": "object" + } + }, + "required": [ + "owner", + "targets" + ], + "title": "ConfigurationTargetRegistryModel", + "type": "object" + }, "EvaluatorCapabilitiesModel": { "additionalProperties": false, "allOf": [ @@ -324,6 +459,43 @@ "title": "EvaluatorCapabilitiesModel", "type": "object" }, + "LiteralBindingValueModel": { + "additionalProperties": false, + "description": "Portable literal value; strict type validation occurs at its descriptor.", + "properties": { + "kind": { + "const": "literal", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "kind", + "value" + ], + "title": "LiteralBindingValueModel", + "type": "object" + }, "ObservationCapabilitiesModel": { "additionalProperties": false, "description": "EXP-715 backend observation and evidence-collection capability declaration.", @@ -1375,6 +1547,17 @@ "title": "Concept Bindings", "type": "array" }, + "configuration_registry": { + "anyOf": [ + { + "$ref": "#/$defs/ConfigurationTargetRegistryModel" + }, + { + "type": "null" + } + ], + "default": null + }, "constraints": { "additionalProperties": { "type": "string" @@ -1414,6 +1597,7 @@ "items": { "enum": [ "backend-manifest-v2", + "experiment-binding-descriptors-v1", "realization-envelope-v1", "provisioning-plan-v1", "orchestration-plan-v1", diff --git a/contracts/schemas/experiment-core/experiment-authoring-input-v1.json b/contracts/schemas/experiment-core/experiment-authoring-input-v1.json index 2fe4cc4d9..5fb4235e8 100644 --- a/contracts/schemas/experiment-core/experiment-authoring-input-v1.json +++ b/contracts/schemas/experiment-core/experiment-authoring-input-v1.json @@ -1,5 +1,102 @@ { "$defs": { + "ApparatusBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target declared by a selected portable apparatus manifest.", + "properties": { + "component_kind": { + "enum": [ + "processor", + "backend", + "participant-runtime", + "other" + ], + "title": "Component Kind", + "type": "string" + }, + "component_name": { + "minLength": 1, + "title": "Component Name", + "type": "string" + }, + "component_version": { + "minLength": 1, + "title": "Component Version", + "type": "string" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "plane": { + "const": "apparatus", + "title": "Plane", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + } + }, + "required": [ + "plane", + "component_kind", + "component_name", + "component_version", + "manifest_version", + "target_id" + ], + "title": "ApparatusBindingTargetModel", + "type": "object" + }, + "BindingOwnerModel": { + "additionalProperties": false, + "description": "Governed contract and validator profile that owns one binding.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "contract_version": { + "minLength": 1, + "title": "Contract Version", + "type": "string" + }, + "validator_id": { + "minLength": 1, + "title": "Validator Id", + "type": "string" + }, + "validator_version": { + "minLength": 1, + "title": "Validator Version", + "type": "string" + } + }, + "required": [ + "contract_id", + "contract_version", + "validator_id", + "validator_version" + ], + "title": "BindingOwnerModel", + "type": "object" + }, + "BindingScalarType": { + "description": "Exact JSON scalar type declared by a binding owner.", + "enum": [ + "string", + "integer", + "number", + "boolean", + "null" + ], + "title": "BindingScalarType", + "type": "string" + }, "ExperimentApparatusConstraintModel": { "additionalProperties": false, "anyOf": [ @@ -261,6 +358,129 @@ "title": "ExperimentBackendReferenceModel", "type": "object" }, + "ExperimentBindingDescriptorModel": { + "additionalProperties": false, + "description": "One explicit factor/condition value bound to one authoritative target.", + "properties": { + "binding_id": { + "minLength": 1, + "title": "Binding Id", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/BindingOwnerModel" + }, + "source_condition_id": { + "minLength": 1, + "title": "Source Condition Id", + "type": "string" + }, + "source_factor_id": { + "minLength": 1, + "title": "Source Factor Id", + "type": "string" + }, + "source_factor_level_id": { + "minLength": 1, + "title": "Source Factor Level Id", + "type": "string" + }, + "target": { + "discriminator": { + "mapping": { + "apparatus": "#/$defs/ApparatusBindingTargetModel", + "participant-implementation": "#/$defs/ParticipantImplementationBindingTargetModel", + "scenario": "#/$defs/ScenarioBindingTargetModel" + }, + "propertyName": "plane" + }, + "oneOf": [ + { + "$ref": "#/$defs/ScenarioBindingTargetModel" + }, + { + "$ref": "#/$defs/ParticipantImplementationBindingTargetModel" + }, + { + "$ref": "#/$defs/ApparatusBindingTargetModel" + } + ], + "title": "Target" + }, + "value": { + "discriminator": { + "mapping": { + "literal": "#/$defs/LiteralBindingValueModel", + "secret-reference": "#/$defs/SecretReferenceBindingValueModel" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/LiteralBindingValueModel" + }, + { + "$ref": "#/$defs/SecretReferenceBindingValueModel" + } + ], + "title": "Value" + }, + "value_type": { + "$ref": "#/$defs/BindingScalarType" + } + }, + "required": [ + "binding_id", + "source_factor_id", + "source_factor_level_id", + "source_condition_id", + "target", + "value_type", + "value", + "owner" + ], + "title": "ExperimentBindingDescriptorModel", + "type": "object" + }, + "ExperimentBindingDescriptorSetModel": { + "additionalProperties": false, + "description": "Versioned, collision-free set of authoritative experiment bindings.", + "properties": { + "descriptors": { + "items": { + "$ref": "#/$defs/ExperimentBindingDescriptorModel" + }, + "minItems": 1, + "title": "Descriptors", + "type": "array" + }, + "schema_version": { + "const": "experiment-binding-descriptors/v1", + "default": "experiment-binding-descriptors/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "descriptors" + ], + "title": "ExperimentBindingDescriptorSetModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Binding ids must be unique and target resolution must be injective within each source condition.", + "id": "binding-descriptors-canonical-targets-injective", + "inputs": [ + { + "contract_id": "experiment-binding-descriptors-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ExperimentBindingDescriptorSetModel._validate_unique_bindings_and_targets" + } + ] + }, "ExperimentCaptureSpecReferenceModel": { "additionalProperties": false, "description": "Reference constrained to a declarative capture specification.", @@ -1781,6 +2001,89 @@ "title": "GovernedEntropyRefModel", "type": "object" }, + "LiteralBindingValueModel": { + "additionalProperties": false, + "description": "Portable literal value; strict type validation occurs at its descriptor.", + "properties": { + "kind": { + "const": "literal", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "kind", + "value" + ], + "title": "LiteralBindingValueModel", + "type": "object" + }, + "ParticipantImplementationBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target declared by one selected participant implementation.", + "properties": { + "implementation_name": { + "minLength": 1, + "title": "Implementation Name", + "type": "string" + }, + "implementation_version": { + "minLength": 1, + "title": "Implementation Version", + "type": "string" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "plane": { + "const": "participant-implementation", + "title": "Plane", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + } + }, + "required": [ + "plane", + "participant_address", + "implementation_name", + "implementation_version", + "manifest_version", + "target_id" + ], + "title": "ParticipantImplementationBindingTargetModel", + "type": "object" + }, "PublicSeedModel": { "additionalProperties": false, "description": "Inline public root-entropy seed: fixed-width canonical hex bytes.\n\nFixed-width lowercase hex removes leading-zero, integer-width, sign, and\nUnicode ambiguity from the executable entropy encoding (per the EXP-718\npreflight's \"Canonical Inputs\" section).", @@ -1915,11 +2218,116 @@ ], "title": "RandomStreamProfileReferenceModel", "type": "object" + }, + "ScenarioBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target owned by a composed SDL scenario family.", + "properties": { + "plane": { + "const": "scenario", + "title": "Plane", + "type": "string" + }, + "scenario_family_id": { + "minLength": 1, + "title": "Scenario Family Id", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "variation_point_id": { + "minLength": 1, + "title": "Variation Point Id", + "type": "string" + } + }, + "required": [ + "plane", + "scenario_family_id", + "variation_point_id", + "target_id" + ], + "title": "ScenarioBindingTargetModel", + "type": "object" + }, + "SecretReferenceBindingValueModel": { + "additionalProperties": false, + "description": "Non-sensitive identity of a secret resolved only at an authorized sink.", + "properties": { + "kind": { + "const": "secret-reference", + "title": "Kind", + "type": "string" + }, + "reference_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$", + "title": "Reference Id", + "type": "string" + } + }, + "required": [ + "kind", + "reference_id" + ], + "title": "SecretReferenceBindingValueModel", + "type": "object" } }, "$id": "https://aces.dev/schemas/experiment-authoring-input-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "binding_semantics": { + "const": "explicit-required" + } + }, + "required": [ + "binding_semantics" + ] + }, + "then": { + "properties": { + "binding_descriptors": { + "not": { + "type": "null" + } + } + }, + "required": [ + "binding_descriptors" + ] + } + }, + { + "if": { + "properties": { + "binding_descriptors": { + "not": { + "type": "null" + } + } + }, + "required": [ + "binding_descriptors" + ] + }, + "then": { + "properties": { + "binding_semantics": { + "const": "explicit-required" + } + } + } + } + ], "description": "Pre-run experiment authoring input: a design that binds a task to a run plan.\n\nThis is the authoring/input counterpart to the archival experiment-core\noutputs (run/study/apparatus-context). It references the separately\nauthored task (and optionally a scenario snapshot) and declares the\npre-run experimental design \u2014 apparatus intent, run plan, factors,\nintended capture, and validity notes \u2014 before any run executes. It is\nnever a run, study, or apparatus-context record (ADR-055 / ADR-074).", "properties": { "apparatus_intent": { @@ -1940,6 +2348,26 @@ "title": "Artifact Refs", "type": "array" }, + "binding_descriptors": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentBindingDescriptorSetModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "binding_semantics": { + "default": "descriptive", + "enum": [ + "descriptive", + "explicit-required" + ], + "title": "Binding Semantics", + "type": "string" + }, "capture_spec_refs": { "items": { "$ref": "#/$defs/ExperimentCaptureSpecReferenceModel" @@ -2030,6 +2458,18 @@ ], "level": "error", "validator": "raes_contracts.contracts.ExperimentSpecModel._validate_experiment_spec" + }, + { + "description": "Explicit bindings must cover every compared condition and resolve exact declared factor levels.", + "id": "experiment-binding-source-joins-valid", + "inputs": [ + { + "contract_id": "experiment-authoring-input-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ExperimentSpecModel._validate_binding_descriptors" } ], "x-aces-semantic-profile": { diff --git a/contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json b/contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json new file mode 100644 index 000000000..dfa902c43 --- /dev/null +++ b/contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json @@ -0,0 +1,374 @@ +{ + "$defs": { + "ApparatusBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target declared by a selected portable apparatus manifest.", + "properties": { + "component_kind": { + "enum": [ + "processor", + "backend", + "participant-runtime", + "other" + ], + "title": "Component Kind", + "type": "string" + }, + "component_name": { + "minLength": 1, + "title": "Component Name", + "type": "string" + }, + "component_version": { + "minLength": 1, + "title": "Component Version", + "type": "string" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "plane": { + "const": "apparatus", + "title": "Plane", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + } + }, + "required": [ + "plane", + "component_kind", + "component_name", + "component_version", + "manifest_version", + "target_id" + ], + "title": "ApparatusBindingTargetModel", + "type": "object" + }, + "BindingOwnerModel": { + "additionalProperties": false, + "description": "Governed contract and validator profile that owns one binding.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "contract_version": { + "minLength": 1, + "title": "Contract Version", + "type": "string" + }, + "validator_id": { + "minLength": 1, + "title": "Validator Id", + "type": "string" + }, + "validator_version": { + "minLength": 1, + "title": "Validator Version", + "type": "string" + } + }, + "required": [ + "contract_id", + "contract_version", + "validator_id", + "validator_version" + ], + "title": "BindingOwnerModel", + "type": "object" + }, + "BindingScalarType": { + "description": "Exact JSON scalar type declared by a binding owner.", + "enum": [ + "string", + "integer", + "number", + "boolean", + "null" + ], + "title": "BindingScalarType", + "type": "string" + }, + "ExperimentBindingDescriptorModel": { + "additionalProperties": false, + "description": "One explicit factor/condition value bound to one authoritative target.", + "properties": { + "binding_id": { + "minLength": 1, + "title": "Binding Id", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/BindingOwnerModel" + }, + "source_condition_id": { + "minLength": 1, + "title": "Source Condition Id", + "type": "string" + }, + "source_factor_id": { + "minLength": 1, + "title": "Source Factor Id", + "type": "string" + }, + "source_factor_level_id": { + "minLength": 1, + "title": "Source Factor Level Id", + "type": "string" + }, + "target": { + "discriminator": { + "mapping": { + "apparatus": "#/$defs/ApparatusBindingTargetModel", + "participant-implementation": "#/$defs/ParticipantImplementationBindingTargetModel", + "scenario": "#/$defs/ScenarioBindingTargetModel" + }, + "propertyName": "plane" + }, + "oneOf": [ + { + "$ref": "#/$defs/ScenarioBindingTargetModel" + }, + { + "$ref": "#/$defs/ParticipantImplementationBindingTargetModel" + }, + { + "$ref": "#/$defs/ApparatusBindingTargetModel" + } + ], + "title": "Target" + }, + "value": { + "discriminator": { + "mapping": { + "literal": "#/$defs/LiteralBindingValueModel", + "secret-reference": "#/$defs/SecretReferenceBindingValueModel" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/LiteralBindingValueModel" + }, + { + "$ref": "#/$defs/SecretReferenceBindingValueModel" + } + ], + "title": "Value" + }, + "value_type": { + "$ref": "#/$defs/BindingScalarType" + } + }, + "required": [ + "binding_id", + "source_factor_id", + "source_factor_level_id", + "source_condition_id", + "target", + "value_type", + "value", + "owner" + ], + "title": "ExperimentBindingDescriptorModel", + "type": "object" + }, + "LiteralBindingValueModel": { + "additionalProperties": false, + "description": "Portable literal value; strict type validation occurs at its descriptor.", + "properties": { + "kind": { + "const": "literal", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "kind", + "value" + ], + "title": "LiteralBindingValueModel", + "type": "object" + }, + "ParticipantImplementationBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target declared by one selected participant implementation.", + "properties": { + "implementation_name": { + "minLength": 1, + "title": "Implementation Name", + "type": "string" + }, + "implementation_version": { + "minLength": 1, + "title": "Implementation Version", + "type": "string" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "plane": { + "const": "participant-implementation", + "title": "Plane", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + } + }, + "required": [ + "plane", + "participant_address", + "implementation_name", + "implementation_version", + "manifest_version", + "target_id" + ], + "title": "ParticipantImplementationBindingTargetModel", + "type": "object" + }, + "ScenarioBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target owned by a composed SDL scenario family.", + "properties": { + "plane": { + "const": "scenario", + "title": "Plane", + "type": "string" + }, + "scenario_family_id": { + "minLength": 1, + "title": "Scenario Family Id", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "variation_point_id": { + "minLength": 1, + "title": "Variation Point Id", + "type": "string" + } + }, + "required": [ + "plane", + "scenario_family_id", + "variation_point_id", + "target_id" + ], + "title": "ScenarioBindingTargetModel", + "type": "object" + }, + "SecretReferenceBindingValueModel": { + "additionalProperties": false, + "description": "Non-sensitive identity of a secret resolved only at an authorized sink.", + "properties": { + "kind": { + "const": "secret-reference", + "title": "Kind", + "type": "string" + }, + "reference_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$", + "title": "Reference Id", + "type": "string" + } + }, + "required": [ + "kind", + "reference_id" + ], + "title": "SecretReferenceBindingValueModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/experiment-binding-descriptors-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Versioned, collision-free set of authoritative experiment bindings.", + "properties": { + "descriptors": { + "items": { + "$ref": "#/$defs/ExperimentBindingDescriptorModel" + }, + "minItems": 1, + "title": "Descriptors", + "type": "array" + }, + "schema_version": { + "const": "experiment-binding-descriptors/v1", + "default": "experiment-binding-descriptors/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "descriptors" + ], + "title": "ExperimentBindingDescriptorSetModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Binding ids must be unique and target resolution must be injective within each source condition.", + "id": "binding-descriptors-canonical-targets-injective", + "inputs": [ + { + "contract_id": "experiment-binding-descriptors-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ExperimentBindingDescriptorSetModel._validate_unique_bindings_and_targets" + } + ], + "x-aces-semantic-profile": { + "contract_id": "experiment-binding-descriptors-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/experiment-core/experiment-run-v1.json b/contracts/schemas/experiment-core/experiment-run-v1.json index ba7635c1a..d9f0a5889 100644 --- a/contracts/schemas/experiment-core/experiment-run-v1.json +++ b/contracts/schemas/experiment-core/experiment-run-v1.json @@ -1,5 +1,56 @@ { "$defs": { + "ApparatusBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target declared by a selected portable apparatus manifest.", + "properties": { + "component_kind": { + "enum": [ + "processor", + "backend", + "participant-runtime", + "other" + ], + "title": "Component Kind", + "type": "string" + }, + "component_name": { + "minLength": 1, + "title": "Component Name", + "type": "string" + }, + "component_version": { + "minLength": 1, + "title": "Component Version", + "type": "string" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "plane": { + "const": "apparatus", + "title": "Plane", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + } + }, + "required": [ + "plane", + "component_kind", + "component_name", + "component_version", + "manifest_version", + "target_id" + ], + "title": "ApparatusBindingTargetModel", + "type": "object" + }, "ApparatusIdentityModel": { "additionalProperties": false, "properties": { @@ -21,6 +72,52 @@ "title": "ApparatusIdentityModel", "type": "object" }, + "BindingOwnerModel": { + "additionalProperties": false, + "description": "Governed contract and validator profile that owns one binding.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "contract_version": { + "minLength": 1, + "title": "Contract Version", + "type": "string" + }, + "validator_id": { + "minLength": 1, + "title": "Validator Id", + "type": "string" + }, + "validator_version": { + "minLength": 1, + "title": "Validator Version", + "type": "string" + } + }, + "required": [ + "contract_id", + "contract_version", + "validator_id", + "validator_version" + ], + "title": "BindingOwnerModel", + "type": "object" + }, + "BindingScalarType": { + "description": "Exact JSON scalar type declared by a binding owner.", + "enum": [ + "string", + "integer", + "number", + "boolean", + "null" + ], + "title": "BindingScalarType", + "type": "string" + }, "ClockDeclarationModel": { "additionalProperties": false, "properties": { @@ -848,6 +945,90 @@ } ] }, + "ExperimentBindingDescriptorModel": { + "additionalProperties": false, + "description": "One explicit factor/condition value bound to one authoritative target.", + "properties": { + "binding_id": { + "minLength": 1, + "title": "Binding Id", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/BindingOwnerModel" + }, + "source_condition_id": { + "minLength": 1, + "title": "Source Condition Id", + "type": "string" + }, + "source_factor_id": { + "minLength": 1, + "title": "Source Factor Id", + "type": "string" + }, + "source_factor_level_id": { + "minLength": 1, + "title": "Source Factor Level Id", + "type": "string" + }, + "target": { + "discriminator": { + "mapping": { + "apparatus": "#/$defs/ApparatusBindingTargetModel", + "participant-implementation": "#/$defs/ParticipantImplementationBindingTargetModel", + "scenario": "#/$defs/ScenarioBindingTargetModel" + }, + "propertyName": "plane" + }, + "oneOf": [ + { + "$ref": "#/$defs/ScenarioBindingTargetModel" + }, + { + "$ref": "#/$defs/ParticipantImplementationBindingTargetModel" + }, + { + "$ref": "#/$defs/ApparatusBindingTargetModel" + } + ], + "title": "Target" + }, + "value": { + "discriminator": { + "mapping": { + "literal": "#/$defs/LiteralBindingValueModel", + "secret-reference": "#/$defs/SecretReferenceBindingValueModel" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/LiteralBindingValueModel" + }, + { + "$ref": "#/$defs/SecretReferenceBindingValueModel" + } + ], + "title": "Value" + }, + "value_type": { + "$ref": "#/$defs/BindingScalarType" + } + }, + "required": [ + "binding_id", + "source_factor_id", + "source_factor_level_id", + "source_condition_id", + "target", + "value_type", + "value", + "owner" + ], + "title": "ExperimentBindingDescriptorModel", + "type": "object" + }, "ExperimentCaptureSpecReferenceModel": { "additionalProperties": false, "description": "Reference constrained to a declarative capture specification.", @@ -2277,6 +2458,43 @@ "title": "GovernedRandomOutcomeRefModel", "type": "object" }, + "LiteralBindingValueModel": { + "additionalProperties": false, + "description": "Portable literal value; strict type validation occurs at its descriptor.", + "properties": { + "kind": { + "const": "literal", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "kind", + "value" + ], + "title": "LiteralBindingValueModel", + "type": "object" + }, "ParticipantExposurePolicyModel": { "additionalProperties": false, "properties": { @@ -2368,6 +2586,52 @@ "title": "ParticipantExposurePolicyModel", "type": "object" }, + "ParticipantImplementationBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target declared by one selected participant implementation.", + "properties": { + "implementation_name": { + "minLength": 1, + "title": "Implementation Name", + "type": "string" + }, + "implementation_version": { + "minLength": 1, + "title": "Implementation Version", + "type": "string" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "plane": { + "const": "participant-implementation", + "title": "Plane", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + } + }, + "required": [ + "plane", + "participant_address", + "implementation_name", + "implementation_version", + "manifest_version", + "target_id" + ], + "title": "ParticipantImplementationBindingTargetModel", + "type": "object" + }, "ParticipantImplementationProvenanceModel": { "additionalProperties": false, "properties": { @@ -2431,6 +2695,36 @@ }, "ParticipantImplementationSelectionModel": { "additionalProperties": false, + "oneOf": [ + { + "properties": { + "configuration_digest": { + "not": { + "type": "null" + } + }, + "configuration_ref": { + "not": { + "type": "null" + } + } + }, + "required": [ + "configuration_ref", + "configuration_digest" + ] + }, + { + "properties": { + "configuration_digest": { + "type": "null" + }, + "configuration_ref": { + "type": "null" + } + } + } + ], "properties": { "configuration_digest": { "anyOf": [ @@ -2765,6 +3059,44 @@ "title": "RandomStreamProfileReferenceModel", "type": "object" }, + "RealizedBindingProvenanceModel": { + "additionalProperties": false, + "description": "Portable provenance for one binding actually realized by its owner.", + "properties": { + "configuration_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": "Configuration Digest" + }, + "descriptor": { + "$ref": "#/$defs/ExperimentBindingDescriptorModel" + }, + "origin": { + "enum": [ + "selection", + "default", + "override" + ], + "title": "Origin", + "type": "string" + } + }, + "required": [ + "descriptor", + "origin" + ], + "title": "RealizedBindingProvenanceModel", + "type": "object" + }, "RealizedTimeModelProvenanceModel": { "additionalProperties": false, "description": "Run-scoped declaration/realization comparison and apparatus evidence.", @@ -2854,6 +3186,64 @@ "title": "RealizedTimeModelProvenanceModel", "type": "object" }, + "ScenarioBindingTargetModel": { + "additionalProperties": false, + "description": "Canonical target owned by a composed SDL scenario family.", + "properties": { + "plane": { + "const": "scenario", + "title": "Plane", + "type": "string" + }, + "scenario_family_id": { + "minLength": 1, + "title": "Scenario Family Id", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "variation_point_id": { + "minLength": 1, + "title": "Variation Point Id", + "type": "string" + } + }, + "required": [ + "plane", + "scenario_family_id", + "variation_point_id", + "target_id" + ], + "title": "ScenarioBindingTargetModel", + "type": "object" + }, + "SecretReferenceBindingValueModel": { + "additionalProperties": false, + "description": "Non-sensitive identity of a secret resolved only at an authorized sink.", + "properties": { + "kind": { + "const": "secret-reference", + "title": "Kind", + "type": "string" + }, + "reference_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$", + "title": "Reference Id", + "type": "string" + } + }, + "required": [ + "kind", + "reference_id" + ], + "title": "SecretReferenceBindingValueModel", + "type": "object" + }, "StreamAddressModel": { "additionalProperties": false, "description": "Closed semantic random-draw address (SVR-013).\n\nA pure canonical function of the randomness namespace, logical trial\ncoordinate, selection-policy id, variation-point id, draw purpose, and a\nstable local draw coordinate. It is not a concatenated string, arbitrary\nmapping, JSON Pointer, or scheduler/compiled-runtime address, and it\nadmits no worker/process/thread/host, wall-time, retry, or aggregate\nexperiment-digest field (``extra=\"forbid\"`` closes the shape).", @@ -4222,6 +4612,13 @@ ], "default": null }, + "realized_bindings": { + "items": { + "$ref": "#/$defs/RealizedBindingProvenanceModel" + }, + "title": "Realized Bindings", + "type": "array" + }, "realized_form_disclosures": { "items": { "$ref": "#/$defs/ExperimentRealizedFormDisclosureModel" diff --git a/contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json b/contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json new file mode 100644 index 000000000..bd7c2c2ba --- /dev/null +++ b/contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json @@ -0,0 +1,282 @@ +{ + "$defs": { + "ApparatusIdentityModel": { + "additionalProperties": false, + "properties": { + "name": { + "minLength": 1, + "title": "Name", + "type": "string" + }, + "version": { + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "ApparatusIdentityModel", + "type": "object" + }, + "BindingOwnerModel": { + "additionalProperties": false, + "description": "Governed contract and validator profile that owns one binding.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "contract_version": { + "minLength": 1, + "title": "Contract Version", + "type": "string" + }, + "validator_id": { + "minLength": 1, + "title": "Validator Id", + "type": "string" + }, + "validator_version": { + "minLength": 1, + "title": "Validator Version", + "type": "string" + } + }, + "required": [ + "contract_id", + "contract_version", + "validator_id", + "validator_version" + ], + "title": "BindingOwnerModel", + "type": "object" + }, + "BindingScalarType": { + "description": "Exact JSON scalar type declared by a binding owner.", + "enum": [ + "string", + "integer", + "number", + "boolean", + "null" + ], + "title": "BindingScalarType", + "type": "string" + }, + "LiteralBindingValueModel": { + "additionalProperties": false, + "description": "Portable literal value; strict type validation occurs at its descriptor.", + "properties": { + "kind": { + "const": "literal", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "kind", + "value" + ], + "title": "LiteralBindingValueModel", + "type": "object" + }, + "ParticipantConfigurationModel": { + "additionalProperties": false, + "description": "Canonical digest payload for one complete participant configuration.", + "properties": { + "implementation_identity": { + "$ref": "#/$defs/ApparatusIdentityModel" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/BindingOwnerModel" + }, + "values": { + "items": { + "$ref": "#/$defs/RealizedConfigurationValueModel" + }, + "minItems": 1, + "title": "Values", + "type": "array" + } + }, + "required": [ + "implementation_identity", + "manifest_version", + "owner", + "values" + ], + "title": "ParticipantConfigurationModel", + "type": "object" + }, + "RealizedConfigurationValueModel": { + "additionalProperties": false, + "description": "One normalized value in a complete participant configuration.", + "properties": { + "origin": { + "enum": [ + "default", + "override" + ], + "title": "Origin", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "value": { + "discriminator": { + "mapping": { + "literal": "#/$defs/LiteralBindingValueModel", + "secret-reference": "#/$defs/SecretReferenceBindingValueModel" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/LiteralBindingValueModel" + }, + { + "$ref": "#/$defs/SecretReferenceBindingValueModel" + } + ], + "title": "Value" + }, + "value_type": { + "$ref": "#/$defs/BindingScalarType" + } + }, + "required": [ + "target_id", + "value_type", + "origin", + "value" + ], + "title": "RealizedConfigurationValueModel", + "type": "object" + }, + "SecretReferenceBindingValueModel": { + "additionalProperties": false, + "description": "Non-sensitive identity of a secret resolved only at an authorized sink.", + "properties": { + "kind": { + "const": "secret-reference", + "title": "Kind", + "type": "string" + }, + "reference_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$", + "title": "Reference Id", + "type": "string" + } + }, + "required": [ + "kind", + "reference_id" + ], + "title": "SecretReferenceBindingValueModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/participant-configuration-result-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Portable result of one complete, atomic participant configuration validation.", + "properties": { + "configuration": { + "$ref": "#/$defs/ParticipantConfigurationModel" + }, + "configuration_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": "Configuration Digest", + "type": "string" + }, + "manifest_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": "Manifest Digest", + "type": "string" + }, + "manifest_ref": { + "minLength": 1, + "title": "Manifest Ref", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "schema_version": { + "const": "participant-configuration-result/v1", + "default": "participant-configuration-result/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "participant_address", + "manifest_ref", + "manifest_digest", + "configuration", + "configuration_digest" + ], + "title": "ParticipantConfigurationResultModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "The configuration digest must be the RFC 8785/JCS digest of the complete normalized configuration.", + "id": "participant-configuration-digest-valid", + "inputs": [ + { + "contract_id": "participant-configuration-result-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantConfigurationResultModel._validate_configuration_digest" + } + ], + "x-aces-semantic-profile": { + "contract_id": "participant-configuration-result-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json b/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json index 2cc58c56a..71adec30d 100644 --- a/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json +++ b/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json @@ -21,6 +21,52 @@ "title": "ApparatusIdentityModel", "type": "object" }, + "BindingOwnerModel": { + "additionalProperties": false, + "description": "Governed contract and validator profile that owns one binding.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "contract_version": { + "minLength": 1, + "title": "Contract Version", + "type": "string" + }, + "validator_id": { + "minLength": 1, + "title": "Validator Id", + "type": "string" + }, + "validator_version": { + "minLength": 1, + "title": "Validator Version", + "type": "string" + } + }, + "required": [ + "contract_id", + "contract_version", + "validator_id", + "validator_version" + ], + "title": "BindingOwnerModel", + "type": "object" + }, + "BindingScalarType": { + "description": "Exact JSON scalar type declared by a binding owner.", + "enum": [ + "string", + "integer", + "number", + "boolean", + "null" + ], + "title": "BindingScalarType", + "type": "string" + }, "ConceptBindingEntryModel": { "additionalProperties": false, "description": "Binds a vocabulary surface in an artifact to a canonical concept family.", @@ -45,6 +91,132 @@ "title": "ConceptBindingEntryModel", "type": "object" }, + "ConfigurationTargetDeclarationModel": { + "additionalProperties": false, + "description": "One scalar target admitted by an owning portable manifest.", + "properties": { + "aliases": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Aliases", + "type": "array" + }, + "allowed_value_kinds": { + "items": { + "enum": [ + "literal", + "secret-reference" + ], + "type": "string" + }, + "minItems": 1, + "title": "Allowed Value Kinds", + "type": "array" + }, + "default": { + "anyOf": [ + { + "$ref": "#/$defs/LiteralBindingValueModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "restricted", + "secret" + ], + "title": "Sensitivity", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "value_type": { + "$ref": "#/$defs/BindingScalarType" + } + }, + "required": [ + "target_id", + "value_type", + "allowed_value_kinds", + "sensitivity" + ], + "title": "ConfigurationTargetDeclarationModel", + "type": "object" + }, + "ConfigurationTargetRegistryModel": { + "additionalProperties": false, + "description": "Collision-free target registry published by one manifest owner.", + "properties": { + "owner": { + "$ref": "#/$defs/BindingOwnerModel" + }, + "targets": { + "additionalProperties": { + "$ref": "#/$defs/ConfigurationTargetDeclarationModel" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Targets", + "type": "object" + } + }, + "required": [ + "owner", + "targets" + ], + "title": "ConfigurationTargetRegistryModel", + "type": "object" + }, + "LiteralBindingValueModel": { + "additionalProperties": false, + "description": "Portable literal value; strict type validation occurs at its descriptor.", + "properties": { + "kind": { + "const": "literal", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "kind", + "value" + ], + "title": "LiteralBindingValueModel", + "type": "object" + }, "ParticipantImplementationCapabilitiesModel": { "additionalProperties": false, "properties": { @@ -148,6 +320,17 @@ "title": "Concept Bindings", "type": "array" }, + "configuration_registry": { + "anyOf": [ + { + "$ref": "#/$defs/ConfigurationTargetRegistryModel" + }, + { + "type": "null" + } + ], + "default": null + }, "constraints": { "additionalProperties": { "type": "string" @@ -174,6 +357,8 @@ "enum": [ "participant-implementation-manifest-v1", "participant-implementation-provenance-v1", + "experiment-binding-descriptors-v1", + "participant-configuration-result-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1" diff --git a/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json b/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json index 00d7d6d1a..84f26db6b 100644 --- a/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json +++ b/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json @@ -114,6 +114,36 @@ }, "ParticipantImplementationSelectionModel": { "additionalProperties": false, + "oneOf": [ + { + "properties": { + "configuration_digest": { + "not": { + "type": "null" + } + }, + "configuration_ref": { + "not": { + "type": "null" + } + } + }, + "required": [ + "configuration_ref", + "configuration_digest" + ] + }, + { + "properties": { + "configuration_digest": { + "type": "null" + }, + "configuration_ref": { + "type": "null" + } + } + } + ], "properties": { "configuration_digest": { "anyOf": [ diff --git a/contracts/schemas/processor-manifest/processor-manifest-v2.json b/contracts/schemas/processor-manifest/processor-manifest-v2.json index b145d7d62..71bb16ee9 100644 --- a/contracts/schemas/processor-manifest/processor-manifest-v2.json +++ b/contracts/schemas/processor-manifest/processor-manifest-v2.json @@ -21,6 +21,52 @@ "title": "ApparatusIdentityModel", "type": "object" }, + "BindingOwnerModel": { + "additionalProperties": false, + "description": "Governed contract and validator profile that owns one binding.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "contract_version": { + "minLength": 1, + "title": "Contract Version", + "type": "string" + }, + "validator_id": { + "minLength": 1, + "title": "Validator Id", + "type": "string" + }, + "validator_version": { + "minLength": 1, + "title": "Validator Version", + "type": "string" + } + }, + "required": [ + "contract_id", + "contract_version", + "validator_id", + "validator_version" + ], + "title": "BindingOwnerModel", + "type": "object" + }, + "BindingScalarType": { + "description": "Exact JSON scalar type declared by a binding owner.", + "enum": [ + "string", + "integer", + "number", + "boolean", + "null" + ], + "title": "BindingScalarType", + "type": "string" + }, "ConceptBindingEntryModel": { "additionalProperties": false, "description": "Binds a vocabulary surface in an artifact to a canonical concept family.", @@ -45,6 +91,132 @@ "title": "ConceptBindingEntryModel", "type": "object" }, + "ConfigurationTargetDeclarationModel": { + "additionalProperties": false, + "description": "One scalar target admitted by an owning portable manifest.", + "properties": { + "aliases": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Aliases", + "type": "array" + }, + "allowed_value_kinds": { + "items": { + "enum": [ + "literal", + "secret-reference" + ], + "type": "string" + }, + "minItems": 1, + "title": "Allowed Value Kinds", + "type": "array" + }, + "default": { + "anyOf": [ + { + "$ref": "#/$defs/LiteralBindingValueModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "restricted", + "secret" + ], + "title": "Sensitivity", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "value_type": { + "$ref": "#/$defs/BindingScalarType" + } + }, + "required": [ + "target_id", + "value_type", + "allowed_value_kinds", + "sensitivity" + ], + "title": "ConfigurationTargetDeclarationModel", + "type": "object" + }, + "ConfigurationTargetRegistryModel": { + "additionalProperties": false, + "description": "Collision-free target registry published by one manifest owner.", + "properties": { + "owner": { + "$ref": "#/$defs/BindingOwnerModel" + }, + "targets": { + "additionalProperties": { + "$ref": "#/$defs/ConfigurationTargetDeclarationModel" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Targets", + "type": "object" + } + }, + "required": [ + "owner", + "targets" + ], + "title": "ConfigurationTargetRegistryModel", + "type": "object" + }, + "LiteralBindingValueModel": { + "additionalProperties": false, + "description": "Portable literal value; strict type validation occurs at its descriptor.", + "properties": { + "kind": { + "const": "literal", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + } + }, + "required": [ + "kind", + "value" + ], + "title": "LiteralBindingValueModel", + "type": "object" + }, "ProcessorCapabilitiesV2Model": { "additionalProperties": false, "properties": { @@ -129,6 +301,17 @@ "title": "Concept Bindings", "type": "array" }, + "configuration_registry": { + "anyOf": [ + { + "$ref": "#/$defs/ConfigurationTargetRegistryModel" + }, + { + "type": "null" + } + ], + "default": null + }, "constraints": { "additionalProperties": { "type": "string" @@ -149,6 +332,7 @@ "items": { "enum": [ "processor-manifest-v2", + "experiment-binding-descriptors-v1", "provisioning-plan-v1", "orchestration-plan-v1", "evaluation-plan-v1", diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index 42be64bfe..d35cde8a1 100644 --- a/contracts/schemas/profiles/backend-profile-v1.json +++ b/contracts/schemas/profiles/backend-profile-v1.json @@ -13,6 +13,7 @@ "items": { "enum": [ "backend-manifest-v2", + "experiment-binding-descriptors-v1", "realization-envelope-v1", "provisioning-plan-v1", "orchestration-plan-v1", diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index 14a55979a..e4b740373 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -137,6 +137,7 @@ adr-090-shared-time-domain-clock-and-progression-authority adr-091-portable-time-capability-control-and-provenance-contracts adr-092-autonomous-benign-participants-under-shared-time adr-093-raes-rename-and-compatibility-boundaries +adr-094-authoritative-cross-plane-experiment-bindings ``` | ADR | Title | Status | Date | @@ -234,3 +235,4 @@ adr-093-raes-rename-and-compatibility-boundaries | [091](adr-091-portable-time-capability-control-and-provenance-contracts.md) | Portable Time Capability, Control, And Provenance Contracts | accepted | 2026-07-24 | | [092](adr-092-autonomous-benign-participants-under-shared-time.md) | Autonomous Benign Participants Under Shared Time | accepted | 2026-07-24 | | [093](adr-093-raes-rename-and-compatibility-boundaries.md) | RAES Rename and Compatibility Boundaries | accepted | 2026-07-23 | +| [094](adr-094-authoritative-cross-plane-experiment-bindings.md) | Authoritative Cross-Plane Experiment Bindings | accepted | 2026-07-26 | diff --git a/docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md b/docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md new file mode 100644 index 000000000..01fb51080 --- /dev/null +++ b/docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md @@ -0,0 +1,123 @@ +# ADR-094: Authoritative Cross-Plane Experiment Bindings + +## Status + +accepted + +## Date + +2026-07-26 + +## Classification + +Classification: FM2 + +Required artifacts: ADR, published schemas, schema-publication records, +positive and negative fixtures, conformance registration, reference +implementation, and behavioral tests. + +Waivers: no runtime mutation, provider selection, scheduling, secret +resolution, HTTP endpoint, CLI command, or MCP tool is introduced. + +## Context + +`ExperimentParameterModel` and condition-assignment parameters record a name, +scalar value, value kind, and redaction posture. They do not identify an +authoritative configuration plane, canonical owner target, source factor and +condition, exact scalar type, validator, default, or realized-value +provenance. Inferring those facts from names, prefixes, free-text constraints, +or matching backend fields permits cross-plane configuration injection. + +ADR-084 already assigns scenario variation to SDL variation points and public +instantiation. ADR-041 assigns participant implementation declaration and +selection to participant manifests and provenance. Processor and backend +manifests separately own apparatus capability. The missing surface is a +portable, typed bridge between experiment intent and those existing owners. + +## Decision + +Publish `experiment-binding-descriptors-v1` as one closed descriptor family +with three planes: + +- `scenario`; +- `participant-implementation`; and +- `apparatus`. + +Each descriptor records an explicit factor id, factor-level id, condition id, +plane-specific target, exact JSON scalar type, literal or secret-reference +value, and governing contract/validator identity. Plane is discriminated data; +it is never inferred. Scenario targets identify a scenario family, variation +point, and owner target. Participant and apparatus targets identify the +selected manifest owner and a manifest-declared configuration target. + +Canonical target resolution is owner-specific. SDL variation authority +resolves scenario targets. Participant, processor, and backend manifests may +publish a typed `configuration_registry` containing canonical ids, bounded +aliases, scalar types, allowed value kinds, sensitivity, defaults, and a +governed validator identity. Aliases are accepted inputs, not identities. +Resolution preserves all inputs until collisions are checked. Two bindings in +one condition that resolve to the same canonical target fail even when their +values match. + +`ExperimentSpecModel` gains explicit binding semantics. The +`explicit-required` posture requires descriptors, rejects legacy +`required_parameters`, and verifies every descriptor's factor, level, and +condition join against the allocation. Existing descriptive authoring remains +valid only under the `descriptive` posture and makes no mutation claim. + +Participant configuration realization is a complete atomic operation: + +1. resolve all canonical ids and aliases; +2. reject unknown or duplicate canonical targets; +3. apply owner-declared defaults and overrides without coercion; +4. require every target with no default; +5. invoke an optional trusted complete-configuration validator; +6. preserve default/override provenance; and +7. emit one `participant-configuration-result-v1` with an RFC 8785/JCS digest. + +Normalization cannot change target identity or JSON scalar type. No partial +result is emitted on failure. + +Literal values and secret references are a discriminated union. Portable +contracts carry only a bounded, non-sensitive reference identity. They have no +field for a resolved value, provider credential, environment variable, file +path, command, or backend-native locator. Resolved secret material never +participates in canonicalization, diagnostics, fixtures, or provenance. + +`experiment-run-v1` may archive realized binding provenance. The provenance +embeds the admitted descriptor, default/override/selection origin, and the +authoritative configuration digest when the binding belongs to participant or +apparatus configuration. + +## Validation and compatibility + +Strict Pydantic scalars distinguish Boolean, integer, number, string, and null. +Strings are not parsed as numbers or booleans, Boolean is not integer, and +non-finite numbers fail before canonicalization. The reference implementation +uses `canonical_contract_digest()` rather than a binding-specific serializer. + +The affected published schemas are `draft` under ADR-061. Their in-line +changes remain reviewable through per-contract `last_change` hashes. The two +new roots are registered with the existing conformance runner and carry valid +and invalid fixtures. + +## Consequences + +Experiment authors can state binding intent without encoding authority in a +name. Participant and apparatus owners can publish a portable target surface +without exposing private backend schemas. Downstream trial compilation can +resolve every target before mutation and reuse the public SDL instantiation, +manifest, and provenance paths. + +Adding a target or validator version extends an owner registry. Adding an +authority plane requires a new contract-lineage and ADR change. The design +does not create a global configuration registry, generic patch language, +plugin dispatcher, secret resolver, or second SDL binder. + +## References + +- [ADR-041](adr-041-participant-implementation-manifest-and-provenance.md) +- [ADR-061](adr-061-published-schema-evolution-policy.md) +- [ADR-074](adr-074-experiment-authoring-input-contract-boundary.md) +- [ADR-084](adr-084-scenario-variation-and-deterministic-trial-realization.md) +- [Experiment binding contracts](../../explain/reference/experiment-binding-contracts.md) diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 3f09fbb23..64d3fc73e 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -435,3 +435,6 @@ adrs: - ref: "#866-pypi-name-correction" - ref: "#884" - ref: "#894" + - id: ADR-094 + path: docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md + pin: 90578754323795ca8775c47dd6095752ccb5a769a0f932ebe60abbe1d8954246 diff --git a/docs/decisions/issue-903-experiment-binding-contracts-preflight.md b/docs/decisions/issue-903-experiment-binding-contracts-preflight.md new file mode 100644 index 000000000..a5163906b --- /dev/null +++ b/docs/decisions/issue-903-experiment-binding-contracts-preflight.md @@ -0,0 +1,468 @@ +# Issue 903 Cross-Plane Experiment Binding Contracts Preflight + +Date: 2026-07-26 + +Issue: #903. + +Requirement: none. The GitHub issue title, body, acceptance criteria, and +non-goals are the authoritative contract. + +This note records architecture guardrails for publishing experiment binding +descriptors and typed participant-implementation configuration targets. It is +guidance only: it does not add or change contract models, schemas, validators, +runtime behavior, trial plans, fixtures, or an implementation plan. + +## Binding Authorities And Existing Gaps + +- ADR-009 makes `contracts/schemas/` the hand-governed machine-readable + authority and the Python `schema_bundle()` output its parity proof. ADR-061 + and `contracts/schema-publication/` govern schema lineage, compatibility, + content hashes, and removal. +- ADR-055, ADR-065, and ADR-074 make experiment authoring input, apparatus + context, archival runs, studies, and participant-implementation provenance + distinct artifacts. An authored parameter is not automatically a scenario + mutation, apparatus setting, or run fact. +- `ExperimentParameterModel` currently has only `name`, scalar `value`, + `value_kind`, and `redaction`. + `ExperimentConditionAssignmentParameterModel` narrows that shape, while + `_parameter_satisfies_requirement()` and + `_condition_assignment_run_criteria_signature()` still compare parameters by + name, kind, type name, and serialized value. None of those fields identifies + a binding plane, owner, canonical target, source factor/condition, or + validator. They are legacy descriptive/audit parameters, not an authority + boundary. +- ADR-084, `raes.variation`, `SemanticValidator._verify_variation_points()`, + `instantiate_scenario()`, and instantiation provenance already own scenario + family selection and scalar SDL binding. Issue #903 must connect experiment + intent to that path; it must not add another SDL target registry, + substitution engine, or instantiation path. +- ADR-076, `DeclarationIndex`, and SDL composition already preserve canonical + declaration collisions before alias projection. Aliases are lookup + conveniences, not identities. Cross-plane binding needs the same + collision-preserving rule without importing private SDL implementation code + into `raes_contracts` or treating processor runtime addresses as authoring + addresses. +- ADR-041 and + `ParticipantImplementationManifestModel` / + `ParticipantImplementationSelectionModel` own participant implementation + declaration and selection. `configuration_ref` and + `configuration_digest` preserve opaque configuration identity, but the + manifest currently declares no typed configuration targets and no portable + complete-configuration validation result. +- `ParticipantImplementationManifestModel.constraints` and + `ParticipantExposurePolicyModel.constraints` are disclosure text. They are + not schemas, validators, target registries, defaults, or permission to inject + arbitrary configuration. +- `ExperimentApparatusContextModel.configuration_parameters` records apparatus + parameters but does not make their names authoritative. Processor, backend, + participant-implementation, and other apparatus components remain separate + manifest owners. +- Runtime fact contracts and `RuntimeFactBindingPlane` already demonstrate + strict scalar unions, value-or-secret-reference separation, typed sink + policy, explicit failure dispositions, and value-free portable events. + Runtime facts are nevertheless run-local late-bound action inputs. They must + not be reused as pre-run experiment factors, scenario selections, + participant configuration, or apparatus configuration. +- `canonical_contract_digest()` already implements RFC 8785/JCS plus SHA-256 + for closed contracts. Ad hoc `json.dumps(sort_keys=True)` digests are not a + second canonicalization profile. + +## Architecture Decisions And Guardrails + +### One descriptor family, three closed authority planes + +Publish one versioned, closed binding-descriptor contract family with a closed +plane vocabulary: + +- `scenario`; +- `participant-implementation`; and +- `apparatus`. + +Unknown planes are invalid. A plane is declared data, never inferred from +`value_kind`, a parameter name or prefix, a target spelling, a matching field, +the selected component kind, or fallback order. + +The descriptor records the source factor id, factor level id, and condition id +explicitly. Those ids must resolve against the owning +`ExperimentSpecModel` / `ExperimentStudyModel` factor and allocation maps. +Collection position and equality between a parameter name and a factor name +carry no provenance meaning. + +The descriptor also records: + +- the exact plane-specific canonical target; +- the exact JSON scalar type (`string`, `integer`, `number`, `boolean`, or + `null`); +- a discriminated literal-value or secret-reference disposition; +- the owning contract id and version; and +- a governed validator/profile id and version. + +The validator identity is a portable governed identifier, not a Python import +path, callback, command, entry point, template, plugin name, or experiment- +selected dispatch string. Trusted code maps the governed identity to an +installed validator behind the owning package/backend boundary. + +Use strict Pydantic scalar types and explicit type predicates, following +`RuntimeFactScalar` and `_value_matches_type()`. Boolean is not integer, integer +and number remain distinguishable where declared, strings are not parsed into +numbers or booleans, and non-finite numbers fail before canonicalization. +`ContractModel` closure alone is insufficient because default Pydantic scalar +coercion and float acceptance do not prove this invariant. + +### Canonical targets are typed owner references, not generic paths + +The target is a discriminated plane-specific reference, not one universal +string path: + +| Plane | Canonical owner and target | Admission rule | +| --- | --- | --- | +| Scenario | The exact scenario-family identity plus canonical variation-point address and its owner-declared target | Resolve through the existing composed `Scenario` variation registry, declaration index, variation semantic validator, selection application, and public instantiation/admission path. Do not bind directly to arbitrary SDL fields or `${...}` occurrences. | +| Participant implementation | Selected implementation identity and manifest version plus one manifest-declared configuration-target id | Resolve only in the selected `ParticipantImplementationManifestModel` target registry and validate through that manifest's governed configuration contract/validator. | +| Apparatus | Selected apparatus component identity/manifest plus one configuration-target id declared by that public owning contract | Processor, backend, host, or other component configuration is admissible only when its selected portable manifest/contract publishes the target. Component presence, `configuration_parameters`, private backend schema fields, and free-text constraints do not create targets. | + +This preserves the difference between SDL declaration addresses, processor +compiled addresses (`raes_contracts.addressing.CompiledAddress`), participant +configuration ids, and apparatus component configuration ids. Common dotted +rendering does not give those concepts common authority. + +Each owner may publish bounded input aliases for migration or ergonomics. +Resolution produces exactly one canonical target before any value validation or +mutation. Preserve every supplied spelling until collision analysis is +complete. Two bindings that resolve to the same `(plane, owner, canonical +target)` fail even when values, source factors, or secret-reference identities +are equal. An alias colliding with another canonical id or alias also fails. +Never trim, lowercase, case-fold, choose first/last writer, deduplicate through +a set/map, or use source order to resolve ambiguity. + +Reuse the collision-preserving semantics of `DeclarationIndex`; do not import +the private SDL class across the package boundary. If a dependency-neutral +helper is extracted, it may own only canonical-key/alias collision mechanics. +It must not become a global registry that owns SDL, participant, and apparatus +target meaning. + +### Target declarations and complete participant configuration + +A configuration-target declaration is a reusable closed scalar-target value +model, but its registry and validation remain owner-specific. A declaration +needs a stable target id, exact scalar type, optional same-type default, allowed +literal or secret-reference dispositions, sensitivity posture, aliases, owning +contract/validator identity, and any bounded declarative constraints that are +portable. It must not contain arbitrary JSON Schema fragments, Python +callables, commands, paths, environment lookups, backend option maps, or +free-text constraints interpreted as executable validation. + +Extend the participant implementation manifest capability surface with a keyed +typed target registry. Map keys must equal embedded target ids, aliases must be +unique across the complete registry, defaults must validate at manifest +admission, and the manifest's supported contract ids must declare the +configuration contract/result versions it claims. + +Participant configuration validation is a complete, atomic owner operation: + +1. resolve all aliases and reject unknown, ambiguous, or duplicate canonical + targets; +2. apply declared defaults and explicit overrides without type coercion; +3. require every target whose declaration has neither a default nor an + admitted override; +4. validate the complete configuration with the selected manifest's governed + validator; +5. return normalized same-type realized values, default/override origin, + per-target provenance, and one authoritative configuration digest; and +6. publish no mutation or success result if any target fails. + +Normalization may canonicalize an admitted value within its declared type; it +must not convert between JSON scalar types, silently drop inputs, substitute a +different target, clamp values, or turn a failure into a default. + +The portable validation/result carrier belongs with neutral participant +contracts in `raes_contracts`; the callable/protocol that realizes owner +validation belongs behind the participant/backend protocol boundary. It must +compose with `ParticipantImplementationSelectionModel` and the existing +participant action-admission path, not create a second participant identity, +action binder, control plane, or backend plugin mechanism. + +### Secret references are structurally distinct and never resolved here + +Literal values and secret references are a discriminated union. A secret +reference cannot be represented as a string literal plus `redaction`, and a +redacted legacy parameter is not automatically a secret reference. + +Only a bounded, explicitly non-sensitive reference identity may enter an +authoring descriptor, trial plan, validation result, digest, or provenance +record. Provider credentials, secret locator details that themselves expose +private data, resolved values, hashes of resolved values, environment-variable +names, file paths, command fragments, and backend-native objects remain +outside portable contracts. + +Secret dereference is a separate deny-first runtime authorization at a +protected sink. Successful descriptor/configuration validation does not grant +dereference authority. Borrow the value-free event and protected-sink posture +from runtime fact binding where applicable; do not reuse a runtime fact as the +experiment binding itself. + +ADR-056/057 distinguish authored scenario fixture values from operator secrets. +That does not authorize operator secrets in experiment binding artifacts. +Scenario fixture values remain governed by their SDL owner; issue #903's secret +reference form exists specifically so resolved external secret material never +enters the portable experiment/configuration lifecycle. + +### Canonicalization, ordering, identity, and provenance + +Canonicalization happens only after plane, owner, target, type, source +factor/level/condition, disposition, and value/reference validation succeeds. +Normalize semantically unordered bindings by `(plane, owner identity, +canonical target)` before RFC 8785/JCS serialization. Input list order, aliases, +map insertion order, source paths, private validator objects, and resolved +secret values never affect identity. + +Reuse `canonical_contract_digest()` and its RFC 8785 semantics, or move that +dependency-neutral implementation behind a shared public contract helper if +its current satisfiability module placement would create a conceptually wrong +import. Do not create a binding-only canonical JSON implementation. + +An authoritative realized-configuration digest commits to: + +- configuration contract/profile and owner identity/version; +- each canonical target and exact declared type; +- each default/override origin; +- normalized non-secret realized values; and +- admitted non-sensitive secret-reference identities. + +It never commits to resolved secret material. If an external configuration +artifact also has a byte checksum, keep that artifact checksum distinct from +the authoritative normalized configuration digest. Existing +`ParticipantImplementationSelectionModel.configuration_digest` must equal or +unambiguously reference the authoritative result digest; do not publish two +fields with overlapping meanings. + +Define one reusable realized-binding provenance value model, distinct from the +authoring descriptor. It preserves source factor, level and condition, plane, +canonical target, exact type, default/override origin, normalized non-secret +value or non-sensitive reference identity, owning contract/validator version, +and authoritative configuration digest. + +Each lifecycle owner embeds that value rather than copying its fields: + +- admitted trial intent records the binding to be realized; +- scenario instantiation provenance records the scenario binding actually + applied through the existing instantiation path; +- participant configuration validation records the complete normalized + configuration result; and +- `ExperimentRunModel` / participant implementation provenance archive the + realized binding and digest used by the run. + +Do not place binding provenance in `RuntimeSnapshot.metadata`, generic +`metadata`/`details`, audit text, logs, or backend-private state. Repeated +representations must be joined by exact ids/digests and validated for equality, +not treated as independent authorities. + +### Compatibility is explicit and fail-closed + +The affected published schemas are currently `draft`, so ADR-061 permits +reviewed in-line structural changes, but every change still needs its +per-contract publication record, content hash/change summary, generated-schema +parity, reader tests, and explicit compatibility statement. A `v1` suffix does +not by itself prove stability or old-reader compatibility. + +When a workflow/profile requires explicit binding semantics, legacy +`ExperimentParameterModel` or +`ExperimentConditionAssignmentParameterModel` inputs without a descriptor are +invalid before trial compilation or runtime mutation. No adapter may infer a +plane, target, factor, condition, type, sensitivity, default, owner, or +validator from `name`, `value_kind`, prefixes, free text, matching fields, +collection order, or a selected backend. + +Legacy descriptive parameters may remain accepted only on paths that make no +binding/mutation claim. Any deterministic migration must be version-pair +specific, preserve the original input, and fail on ambiguity; a best-effort +upgrade is prohibited. + +## Required Incumbents + +- Experiment contracts and joins: + `ExperimentParameterModel`, + `ExperimentConditionAssignmentParameterModel`, + `ExperimentStudyFactorModel`, + `ExperimentConditionAssignmentModel`, + `ExperimentRunAllocationPlanModel`, `ExperimentSpecModel`, + `ExperimentApparatusContextModel`, `ExperimentRunModel`, + `_run_satisfies_condition_assignment()`, and the existing task/run/study + semantic validators and `x-aces-invariants`. +- Scenario-family authority: + `Variable`, `VariableTarget`, `ParameterVariationPoint`, the other closed + variation target types, `DeclarationIndex`, + `SemanticValidator._verify_variation_points()`, module composition, + `instantiate_scenario()`, `admit_instantiated_scenario()`, + `InstantiationProvenance`, and canonical instantiated snapshots. +- Participant and apparatus authority: + `ParticipantImplementationManifestModel`, + `ParticipantImplementationCapabilitiesModel`, + `ParticipantImplementationSelectionModel`, + `ParticipantImplementationProvenanceModel`, + `ProcessorManifestV2Model`, `BackendManifestV2Model`, + manifest authority allowlists, controlled vocabulary validation, + `ParticipantActionAdmissionRequest`, and participant/backend protocols. +- Shared contract mechanics: + `ContractModel(extra="forbid")`, strict Pydantic scalar types, + `Diagnostic` / `DiagnosticModel`, `canonical_contract_digest()` RFC 8785 + semantics, `schema_bundle()`, `x-aces-invariants`, and the existing + reference-model/concept-authority machinery where a new public concept + binding is actually required. +- Schema and conformance: + `contracts/schemas/`, `contracts/fixtures/`, + `contracts/schema-publication/entries/`, + `tools/generate_contract_schemas.py`, + `tools/check_generated_schemas.py`, + `tools/check_schema_publication.py`, `tools/check_json_artifacts.py`, + `raes_conformance.conformance.validators`, + `raes_conformance.conformance.semantics`, and backend profile contract sets + only when a backend is expected to claim the new contracts. +- Security, persistence, and public delivery: + `ControlPlaneSecurityConfig.strict_defaults()`, read/mutating role + dependencies, request-size guards, request fingerprints, idempotency keys, + `AuditEvent`, `ControlPlaneStore`, bounded `HTTPException` details, and the + redacted `{"detail": "internal server error"}` handler. +- Repository workflow: + `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, + `tools/check_repo_policy.py`, `tools/check_requirement_governance.py`, + `tools/check_authority_boundary.py`, `tools/check_sdl_catalog_parity.py`, + `tools/check_semantic_coverage.py`, `tools/check_specification_coverage.py`, + and `tools/verify_all.py`. + +## Cross-Cutting Layers The Implementation Must Pass + +- **Authoring/config shape:** experiment input enters closed experiment models; + scenario targets additionally pass safe SDL parsing, phase-specific schema, + composition/trust, portable-id, declaration collision, variation semantic, + instantiation, and post-instantiation admission gates. Participant/apparatus + targets pass their selected manifest and owner registry. Raw mappings never + become configuration merely because Pydantic can parse them. +- **Factor/condition join:** every source id resolves against the exact + experiment spec/study allocation artifact. Factor level and condition + membership are validated before target resolution, and target resolution is + completed for the whole binding set before any mutation. +- **Type/default/normalization:** strict scalar and finite-number validation + runs for explicit values, manifest defaults, owner-normalized results, and + reconstructed provenance. Default application is owner-declared behavior, + not missing-field inference. +- **Alias/collision:** preserve canonical declarations and all aliases until + an injective resolution is proven. Duplicate canonical targets are rejected + before constructing dictionaries or digests, including identical-value + duplicates. +- **Manifest/owner validation:** selected participant/apparatus identity, + manifest digest/version, supported contracts, target registry, validator + identity, complete realized configuration, and result digest agree. Manifest + constraints remain disclosure only. +- **Authentication/authorization:** publishing and validating offline + contracts adds no auth surface. Any later HTTP mutation reuses control-plane + strict defaults, target-bound principals, mutating roles, request limits, + idempotency/fingerprints, and audit. Experiment binding authority, caller + authorization, apparatus support, and secret dereference authorization are + independent gates. +- **Secret handling:** only the non-sensitive reference identity crosses the + portable boundary. Resolved values and sensitive locator/provider details + are excluded from models, digests, fixtures, diagnostics, provenance, audit, + persistence, and logs. Redaction is structural, not a promise to scrub later. +- **Environment and OS exposure:** environment binding is a non-goal. Do not + resolve target/value/secret data through environment names, arbitrary files, + filenames, process argv, shell interpolation, stdout/stderr, or plugin + dispatch. If a trusted owner adapter later needs a process boundary, it uses + fixed invocation shapes, controlled working directories, bounded input via + an appropriate protected channel, bounded timeouts, no `shell=True`, and + redacted output handling. +- **Error envelope and observability:** expected failures use bounded + `Diagnostic` codes, domains, JSON-pointer addresses, and safe messages or the + existing contract/SDL error envelopes. Do not include rejected values, + secret refs, raw Pydantic `input_value`, backend exception strings, full + payloads, or tracebacks. HTTP 500 remains redacted; logs/audit may carry safe + ids, contract/profile versions, digests, counts, dispositions, stages, and + durations only. +- **Persistence and archival joins:** this issue needs no new repository, + controller, database, cache, or mutable parameter store. Git-tracked schemas, + fixtures, publication records, and specs are the publication audit surface. + Live state continues through existing control-plane/runtime carriers; + admitted intent and archival evidence continue through trial, + instantiation, participant provenance, apparatus context, and experiment-run + contracts. +- **Schema/conformance:** structure, cross-object semantics, canonicalization, + and runtime owner validation are separate gates. Every new root contract is + routed explicitly, exported publicly, registered with conformance, published + with positive/negative fixtures, and included in applicable support + allowlists/profiles. JSON Schema acceptance alone is not owner validation. + +## Extensibility Seam + +The extension seam is an owner-published configuration-target registry plus a +governed validator profile. It is parameterized by plane, owner +identity/manifest, target id and aliases, exact scalar type, default and allowed +value disposition, sensitivity, validator contract/profile version, and +normalization/digest profile. + +The next reasonable change is another participant implementation target, +another portable apparatus component target, or another version of an owning +validator. It should add a declaration/validator version and fixtures behind +that seam. It must not require editing the cross-plane resolver, adding a +backend-specific field to experiment input, creating a second SDL binder, or +allowing experiment input to select executable code. + +Adding an entirely new authoritative plane is a contract-lineage and +architecture change: extend the closed union, provenance, compatibility rules, +validators, schemas, and negative fixtures together. Do not admit +`x-` planes as an escape hatch. + +## Gotchas And Anti-Patterns + +Avoid: + +- enriching `ExperimentParameterModel` while retaining name equality as the + binding authority; +- inferring plane or target from `value_kind`, prefixes, field names, free-text + constraints, component order, selected backend, or fallback; +- treating SDL variable names, variation-point ids, compiled resource + addresses, participant target ids, backend option names, and environment + names as interchangeable strings; +- binding scenario values directly to arbitrary JSON/YAML pointers, templates, + overlays, patches, object attributes, or private compiler fields; +- using participant manifest `constraints`, apparatus + `configuration_parameters`, backend driver config, or a private provider + schema as a portable target registry; +- accepting duplicate canonical targets because their values match, or losing + alias collisions through dict/set construction; +- relying on permissive union parsing, bool/int equivalence, numeric/string + coercion, `NaN`/infinity, insertion order, or non-JCS JSON serialization; +- making defaults, normalization, validation, and digest computation separate + mutable passes that can observe different configuration; +- hashing a resolved secret, treating a redacted scalar as a secret reference, + putting a sensitive locator in the reference identity, or leaking rejected + input through validation errors; +- letting an experiment choose a Python entry point, validator callable, + provider plugin, command, environment variable, file path, or backend option + map; +- using runtime facts to select pre-run factors/conditions, scenario variation, + participant implementation configuration, or apparatus; +- creating duplicate target schemas, reference resolvers, canonicalizers, + validator registries, exception hierarchies, diagnostic envelopes, + provenance roots, configuration stores, audit streams, or conformance + runners; and +- hand-editing generated/reference schemas without the authoritative schema, + publication record, compatibility, fixture, and parity changes moving + together. + +## Non-Goals And Implementation Boundary + +- This preflight does not implement issue #903 or downstream issues #787, + #788, #789, #790, or #345. +- Issue #903 does not select or construct participant providers, execute or + schedule trials, resolve secrets, mutate runtime state, or add HTTP/CLI/MCP + execution surfaces. +- It does not define an APTL-specific allowlist, expose arbitrary backend or + provider configuration, or make free-text manifest constraints executable. +- It does not add environment-variable binding, arbitrary filesystem paths, + command fragments, Python entry points, templates, generic overlays, + experiment-selected plugins, or backend-private schema injection. +- It does not persist or hash resolved secret values, and it does not turn SDL + scenario fixture credentials into operator-secret references. +- It does not replace SDL variation/instantiation, participant action + admission, runtime fact binding, apparatus manifests, control-plane + security/persistence, experiment run/study provenance, or schema publication + governance. diff --git a/docs/explain/reference/README.md b/docs/explain/reference/README.md index 08723cddf..2707b63f2 100644 --- a/docs/explain/reference/README.md +++ b/docs/explain/reference/README.md @@ -30,3 +30,6 @@ themselves normative specifications or ADRs. - [scenario-variation-and-trial-realization.md](scenario-variation-and-trial-realization.md) - Complete SCE-002 phase, ownership, identity, random-stream, trial-plan, runtime-fact, migration, and follow-on architecture +- [experiment-binding-contracts.md](experiment-binding-contracts.md) + - Authoritative experiment-to-scenario, participant, and apparatus binding + contracts and configuration realization diff --git a/docs/explain/reference/canonical-reference-map.md b/docs/explain/reference/canonical-reference-map.md index 12ef43274..5cf73f68e 100644 --- a/docs/explain/reference/canonical-reference-map.md +++ b/docs/explain/reference/canonical-reference-map.md @@ -29,6 +29,7 @@ page is an index, not a replacement for the linked artifacts. | Instantiation, planning, and realization path | [`docs/explain/sdl/runtime-architecture.md`](../sdl/runtime-architecture.md) | | Explicitness and realized-form semantics | [`explicitness-realization-semantics.md`](explicitness-realization-semantics.md), [`realization-envelopes.md`](realization-envelopes.md) | | Variation and trial realization | [`scenario-variation-and-trial-realization.md`](scenario-variation-and-trial-realization.md) | +| Experiment binding contracts | [`experiment-binding-contracts.md`](experiment-binding-contracts.md), [ADR-094](../../decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md) | | Backend conformance | [`backend-conformance.md`](backend-conformance.md) | | Evidence-bounded claim guidance | [`docs/explain/sdl/agent-guidance.md`](../sdl/agent-guidance.md), [`docs/explain/sdl/scientific-scenario-completeness.md`](../sdl/scientific-scenario-completeness.md) | @@ -59,6 +60,7 @@ page is an index, not a replacement for the linked artifacts. | Design precedents | [`docs/explain/sdl/precedents.md`](../sdl/precedents.md) | | Academic lineage | [`docs/explain/sdl/lineage.md`](../sdl/lineage.md) | | Scenario variation and trial realization design | [`scenario-variation-and-trial-realization.md`](scenario-variation-and-trial-realization.md), [ADR-084](../../decisions/adrs/adr-084-scenario-variation-and-deterministic-trial-realization.md) | +| Cross-plane experiment binding and configuration | [`experiment-binding-contracts.md`](experiment-binding-contracts.md), [ADR-094](../../decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md) | ## Contracts And Processing diff --git a/docs/explain/reference/experiment-binding-contracts.md b/docs/explain/reference/experiment-binding-contracts.md new file mode 100644 index 000000000..6b1328563 --- /dev/null +++ b/docs/explain/reference/experiment-binding-contracts.md @@ -0,0 +1,81 @@ +# Experiment Binding Contracts + +Issue #903 publishes the portable contract surfaces that connect experiment +factors and conditions to scenario variation, participant implementation +configuration, and apparatus configuration. The contracts state intent and +validation results; they do not mutate a runtime. + +## Authority planes + +| Plane | Canonical owner | Target | +|-------|-----------------|--------| +| `scenario` | Composed SDL scenario family | Declared variation point and its owner target | +| `participant-implementation` | Selected participant implementation manifest | Manifest `configuration_registry` target | +| `apparatus` | Selected processor or backend manifest | Manifest `configuration_registry` target | + +A descriptor carries its source factor, factor level, and condition. These +joins are validated by `ExperimentSpecModel` when +`binding_semantics: explicit-required` is selected. Legacy +`required_parameters` remain descriptive data and are rejected in that mode. + +Targets never fall back across planes. A participant target that is absent +from the selected participant manifest does not become a scenario or backend +target because the same spelling exists elsewhere. + +## Target registries + +An owner registry declares canonical target ids, bounded aliases, exact scalar +types, admitted literal/secret-reference forms, sensitivity, optional defaults, +and the governing contract and validator version. + +Aliases are input spellings only. Admission resolves aliases to canonical ids +before constructing a configuration or digest. Alias/canonical collisions and +two inputs that resolve to one target fail, including equal-value duplicates. + +The extension seam is the owner registry: a manifest can add a target or a new +validator version without changing the cross-plane resolver. A new authority +plane is not an extension id; it requires a versioned contract change. + +## Participant configuration realization + +`realize_participant_configuration()` accepts a selected participant manifest +and an override list. It resolves the whole list, applies defaults, requires +missing values, enforces strict types, and optionally invokes a trusted +`ParticipantConfigurationValidator` over the complete configuration. It +returns no partial result. + +The normalized entries are sorted by canonical target id and preserve whether +each value came from a default or override. +`participant-configuration-result-v1` records the selected manifest identity, +governed validator, normalized values, and a `canonical_contract_digest()` +RFC 8785/JCS digest. Alias spelling and input ordering do not affect that +digest. + +## Scalar and secret posture + +The scalar vocabulary is closed: string, integer, number, Boolean, and null. +Validation is non-coercing. Boolean does not satisfy integer, numeric strings +remain strings, and NaN or infinity is invalid. + +A secret reference is structurally different from a string literal. Portable +artifacts carry only its non-sensitive reference identity. They contain no +resolved-secret field and do not resolve environment variables, files, +commands, provider objects, or private locators. The digest commits to the +reference identity, never to resolved secret material. + +## Lifecycle integration + +- `experiment-authoring-input-v1` carries explicit descriptors and validates + their factor/condition provenance. +- Scenario admission delegates target resolution to the public SDL + variation/instantiation authority. +- Participant, processor, and backend manifests publish their own target + registries. +- `participant-configuration-result-v1` is the authoritative normalized + participant configuration result. +- `experiment-run-v1` may archive the realized descriptor, its origin, and the + configuration digest. + +Downstream admitted trial-plan and compiler work consumes these surfaces. It +must not add another target registry, SDL binder, canonicalizer, or secret +resolver. diff --git a/docs/index.md b/docs/index.md index c1fd163e6..0df62efd3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -143,6 +143,7 @@ explain/reference/objective-semantics explain/reference/explicitness-realization-semantics explain/reference/realization-envelopes explain/reference/scenario-variation-and-trial-realization +explain/reference/experiment-binding-contracts ``` ```{toctree} diff --git a/implementations/python/packages/raes_backend_stubs/manifest.py b/implementations/python/packages/raes_backend_stubs/manifest.py index 9c328706d..6dd2f9258 100644 --- a/implementations/python/packages/raes_backend_stubs/manifest.py +++ b/implementations/python/packages/raes_backend_stubs/manifest.py @@ -40,7 +40,10 @@ from raes_contracts.manifest_authority import BACKEND_SUPPORTED_CONTRACT_IDS from raes_contracts.vocabulary import RealizationSupportMode -REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS = frozenset(BACKEND_SUPPORTED_CONTRACT_IDS) - {"realization-envelope-v1"} +REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS = frozenset(BACKEND_SUPPORTED_CONTRACT_IDS) - { + "experiment-binding-descriptors-v1", + "realization-envelope-v1", +} REFERENCE_PARTICIPANT_ROLES = frozenset( PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_ROLE_SCOPE] ) diff --git a/implementations/python/packages/raes_conformance/conformance/validators.py b/implementations/python/packages/raes_conformance/conformance/validators.py index 3477dfcc2..175026f2d 100644 --- a/implementations/python/packages/raes_conformance/conformance/validators.py +++ b/implementations/python/packages/raes_conformance/conformance/validators.py @@ -10,6 +10,7 @@ EvaluationPlanModel, EvaluationResultStateModel, ExperimentApparatusContextModel, + ExperimentBindingDescriptorSetModel, ExperimentCaptureSpecModel, ExperimentDerivedMeasureModel, ExperimentEvidenceRecordModel, @@ -20,6 +21,7 @@ OperationStatusModel, OrchestrationPlanModel, ParticipantBehaviorHistoryEventModel, + ParticipantConfigurationResultModel, ParticipantEpisodeHistoryEventModel, ParticipantEpisodeStateModel, ParticipantImplementationManifestModel, @@ -67,6 +69,8 @@ "experiment-evidence-record-v1": ExperimentEvidenceRecordModel.model_validate, "experiment-derived-measure-v1": ExperimentDerivedMeasureModel.model_validate, "experiment-run-v1": ExperimentRunModel.model_validate, + "experiment-binding-descriptors-v1": ExperimentBindingDescriptorSetModel.model_validate, + "participant-configuration-result-v1": ParticipantConfigurationResultModel.model_validate, } diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index f3ddb889d..b59807c07 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -1,5 +1,7 @@ """Schema-first external contract models for RAES artifact boundaries.""" +# ruff: noqa: F405 - this package intentionally re-exports subdomain __all__ surfaces + from __future__ import annotations from ..versions import ( @@ -115,19 +117,9 @@ ExperimentDerivedMeasureReferenceModel, ExperimentMeasurementChannelReferenceModel, ) -from .experiment_capture import ( - ExperimentCaptureRequirementModel, - ExperimentCaptureSpecModel, - ExperimentCaptureWindowModel, - ExperimentValidityNoteModel, -) -from .experiment_disclosure import ( - ExperimentApparatusConstraintModel, - ExperimentAugmentationDisclosureModel, - ExperimentEvaluationProtocolModel, - ExperimentMetricDefinitionModel, - ExperimentSplitAndLeakageControlsModel, -) +from .experiment_bindings import * # noqa: F403 - package-level contract re-export +from .experiment_capture import * # noqa: F403 - package-level contract re-export +from .experiment_disclosure import * # noqa: F403 - package-level contract re-export from .experiment_evidence import ( ExperimentDerivedMeasureMethodModel, ExperimentDerivedMeasureModel, @@ -400,10 +392,12 @@ "ControlledVocabularyTermModel", "ContractModel", "ExperimentAnalysisPlanModel", "NIST_CSF_DEFENSIVE_CATEGORIES_SOURCE_SCHEMA_VERSION", "NistCsfDefensiveCategorySourceModel", "NistCsfDefensiveCategorySourceTermModel", + "ApparatusBindingTargetModel", "BindingOwnerModel", "BindingScalarType", "ConfigurationTargetDeclarationModel", "ConfigurationTargetRegistryModel", "ExperimentApparatusCompatibilityReferenceModel", "ExperimentApparatusComponentModel", "ExperimentApparatusConstraintModel", "ExperimentApparatusContextModel", "ExperimentArtifactRefModel", "ExperimentAugmentationDisclosureModel", "ExperimentBackendReferenceModel", "ExperimentCaptureRequirementModel", "ExperimentCaptureSpecModel", "ExperimentCaptureSpecReferenceModel", + "ExperimentBindingDescriptorModel", "ExperimentBindingDescriptorSetModel", "ExperimentCaptureWindowModel", "ExperimentChecksumModel", "ExperimentClockContextModel", "ExperimentConditionAssignmentParameterModel", "ExperimentConditionAssignmentReferenceModel", "ExperimentDerivedMeasureMethodModel", "ExperimentDerivedMeasureModel", @@ -420,7 +414,10 @@ "ExperimentStatisticalMethodModel", "ExperimentStochasticControlModel", "ExperimentStudyFactorModel", "ExperimentStudyMembershipModel", "ExperimentStudyModel", "ExperimentTaskReferenceModel", "ExperimentTaskModel", "ExperimentUncertaintyMethodModel", "ExperimentValidityNoteModel", + "LiteralBindingValueModel", "ParticipantConfigurationModel", "ParticipantConfigurationResultModel", "ParticipantImplementationBindingTargetModel", + "RealizedBindingProvenanceModel", "ScenarioBindingTargetModel", "SecretReferenceBindingValueModel", "EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION", "EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION", + "EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION", "EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION", "EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION", "EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION", "EXPERIMENT_RUN_SCHEMA_VERSION", "EXPERIMENT_STUDY_SCHEMA_VERSION", "EXPERIMENT_TASK_SCHEMA_VERSION", "EvaluationHistoryEventModel", @@ -432,7 +429,8 @@ "OPERATION_SCHEMA_VERSION", "OperationReceiptModel", "OperationStatusModel", "ObservationCapabilitiesModel", "OrchestrationPlanModel", "OrchestratorCapabilitiesModel", "PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION", "PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION", - "PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION", "ParticipantActionEffectResultModel", + "PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION", + "PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION", "ParticipantActionEffectResultModel", "ParticipantActionPreconditionResultModel", "ParticipantActionResultModel", "ParticipantAttributionCandidateModel", "ParticipantAttributionEdgeModel", "ParticipantAttributionEvidenceBasisModel", "ParticipantAttributionOrderingBasisModel", diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index a7e062e8f..12c367a5d 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -25,6 +25,10 @@ WorkflowHistoryEventModel, ) from .experiment_apparatus import ExperimentApparatusContextModel, ExperimentTaskModel +from .experiment_bindings import ( + ExperimentBindingDescriptorSetModel, + ParticipantConfigurationResultModel, +) from .experiment_capture import ExperimentCaptureSpecModel from .experiment_evidence import ExperimentDerivedMeasureModel, ExperimentEvidenceRecordModel from .experiment_run import ExperimentRunModel @@ -138,6 +142,7 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "random-stream-vector-v1": RandomStreamVectorModel.model_json_schema(), "experiment-apparatus-context-v1": ExperimentApparatusContextModel.model_json_schema(), "experiment-authoring-input-v1": ExperimentSpecModel.model_json_schema(), + "experiment-binding-descriptors-v1": ExperimentBindingDescriptorSetModel.model_json_schema(), "experiment-capture-spec-v1": ExperimentCaptureSpecModel.model_json_schema(), "experiment-derived-measure-v1": ExperimentDerivedMeasureModel.model_json_schema(), "experiment-evidence-record-v1": ExperimentEvidenceRecordModel.model_json_schema(), @@ -192,6 +197,7 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "participant-context-view-v1": ParticipantContextViewModel.model_json_schema(), "runtime-fact-binding-plane-v1": RuntimeFactBindingPlaneModel.model_json_schema(), "participant-decision-surface-v1": ParticipantDecisionSurfaceModel.model_json_schema(), + "participant-configuration-result-v1": ParticipantConfigurationResultModel.model_json_schema(), "operation-receipt-v1": OperationReceiptModel.model_json_schema(), "operation-status-v1": OperationStatusModel.model_json_schema(), "associated-artifact-manifest-v1": AssociatedArtifactManifestModel.model_json_schema(), diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py b/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py new file mode 100644 index 000000000..3d7137572 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py @@ -0,0 +1,426 @@ +"""Portable contracts for authoritative cross-plane experiment bindings.""" + +from __future__ import annotations + +import math +from enum import Enum +from typing import Annotated, Literal + +from pydantic import ( + Field, + GetJsonSchemaHandler, + SerializerFunctionWrapHandler, + StrictBool, + StrictFloat, + StrictInt, + StrictStr, + model_serializer, + model_validator, +) +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from ..versions import ( + EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION, + PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION, +) +from .base import ContractModel, NonEmptyString, PrefixedDigestString +from .capabilities import ApparatusIdentityModel +from .schema_invariants import _add_aces_invariant + + +class BindingScalarType(str, Enum): + """Exact JSON scalar type declared by a binding owner.""" + + STRING = "string" + INTEGER = "integer" + NUMBER = "number" + BOOLEAN = "boolean" + NULL = "null" + + +BindingScalar = StrictBool | StrictInt | StrictFloat | StrictStr | None + + +class LiteralBindingValueModel(ContractModel): + """Portable literal value; strict type validation occurs at its descriptor.""" + + kind: Literal["literal"] + value: BindingScalar + + +class SecretReferenceBindingValueModel(ContractModel): + """Non-sensitive identity of a secret resolved only at an authorized sink.""" + + kind: Literal["secret-reference"] + reference_id: Annotated[ + str, + Field( + min_length=1, + max_length=256, + pattern=r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$", + ), + ] + + +BindingValue = Annotated[ + LiteralBindingValueModel | SecretReferenceBindingValueModel, + Field(discriminator="kind"), +] + + +class ScenarioBindingTargetModel(ContractModel): + """Canonical target owned by a composed SDL scenario family.""" + + plane: Literal["scenario"] + scenario_family_id: NonEmptyString + variation_point_id: NonEmptyString + target_id: NonEmptyString + + def canonical_key(self) -> tuple[str, ...]: + return ( + self.plane, + self.scenario_family_id, + self.variation_point_id, + self.target_id, + ) + + +class ParticipantImplementationBindingTargetModel(ContractModel): + """Canonical target declared by one selected participant implementation.""" + + plane: Literal["participant-implementation"] + participant_address: NonEmptyString + implementation_name: NonEmptyString + implementation_version: NonEmptyString + manifest_version: NonEmptyString + target_id: NonEmptyString + + def canonical_key(self) -> tuple[str, ...]: + return ( + self.plane, + self.participant_address, + self.implementation_name, + self.implementation_version, + self.manifest_version, + self.target_id, + ) + + +class ApparatusBindingTargetModel(ContractModel): + """Canonical target declared by a selected portable apparatus manifest.""" + + plane: Literal["apparatus"] + component_kind: Literal["processor", "backend", "participant-runtime", "other"] + component_name: NonEmptyString + component_version: NonEmptyString + manifest_version: NonEmptyString + target_id: NonEmptyString + + def canonical_key(self) -> tuple[str, ...]: + return ( + self.plane, + self.component_kind, + self.component_name, + self.component_version, + self.manifest_version, + self.target_id, + ) + + +BindingTarget = Annotated[ + ScenarioBindingTargetModel | ParticipantImplementationBindingTargetModel | ApparatusBindingTargetModel, + Field(discriminator="plane"), +] + + +class BindingOwnerModel(ContractModel): + """Governed contract and validator profile that owns one binding.""" + + contract_id: NonEmptyString + contract_version: NonEmptyString + validator_id: NonEmptyString + validator_version: NonEmptyString + + +class ConfigurationTargetDeclarationModel(ContractModel): + """One scalar target admitted by an owning portable manifest.""" + + target_id: NonEmptyString + value_type: BindingScalarType + aliases: list[NonEmptyString] = Field(default_factory=list) + allowed_value_kinds: list[Literal["literal", "secret-reference"]] = Field(min_length=1) + sensitivity: Literal["public", "internal", "restricted", "secret"] + default: LiteralBindingValueModel | None = None + + @model_validator(mode="after") + def _validate_target_declaration(self) -> ConfigurationTargetDeclarationModel: + if len(self.aliases) != len(set(self.aliases)): + raise ValueError("configuration target aliases must be unique") + if self.target_id in self.aliases: + raise ValueError("configuration target aliases must not repeat the canonical target id") + if len(self.allowed_value_kinds) != len(set(self.allowed_value_kinds)): + raise ValueError("allowed_value_kinds must be unique") + if self.sensitivity == "secret" and self.allowed_value_kinds != ["secret-reference"]: + raise ValueError("secret configuration targets admit only secret-reference values") + if self.default is not None: + if self.sensitivity == "secret": + raise ValueError("secret configuration targets must not declare portable defaults") + if "literal" not in self.allowed_value_kinds: + raise ValueError("a literal default requires literal values to be allowed") + self.validate_value(self.default) + return self + + @model_serializer(mode="wrap") + def _serialize_optional_default( + self, + handler: SerializerFunctionWrapHandler, + ) -> dict[str, object]: + payload = handler(self) + if self.default is None: + payload.pop("default", None) + return payload + + def validate_value(self, value: BindingValue) -> None: + if value.kind not in self.allowed_value_kinds: + raise ValueError(f"configuration target {self.target_id!r} rejects value kind {value.kind!r}") + if isinstance(value, LiteralBindingValueModel): + if not _binding_value_matches_type(value.value, self.value_type): + raise ValueError(f"configuration target {self.target_id!r} value does not match value_type") + if isinstance(value.value, float) and not math.isfinite(value.value): + raise ValueError(f"configuration target {self.target_id!r} number must be finite") + elif self.value_type == BindingScalarType.NULL: + raise ValueError(f"configuration target {self.target_id!r} null type cannot admit a secret reference") + + +class ConfigurationTargetRegistryModel(ContractModel): + """Collision-free target registry published by one manifest owner.""" + + owner: BindingOwnerModel + targets: dict[NonEmptyString, ConfigurationTargetDeclarationModel] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_registry(self) -> ConfigurationTargetRegistryModel: + claimed_names: dict[str, str] = {} + for key, declaration in self.targets.items(): + if key != declaration.target_id: + raise ValueError("configuration target map key must match embedded target_id") + for name in (declaration.target_id, *declaration.aliases): + prior = claimed_names.get(name) + if prior is not None: + raise ValueError(f"configuration target alias or id {name!r} collides with target {prior!r}") + claimed_names[name] = declaration.target_id + return self + + def resolve(self, supplied_id: str) -> ConfigurationTargetDeclarationModel: + direct = self.targets.get(supplied_id) + if direct is not None: + return direct + matches = [target for target in self.targets.values() if supplied_id in target.aliases] + if len(matches) != 1: + raise ValueError(f"unknown configuration target {supplied_id!r}") + return matches[0] + + +class ExperimentBindingDescriptorModel(ContractModel): + """One explicit factor/condition value bound to one authoritative target.""" + + binding_id: NonEmptyString + source_factor_id: NonEmptyString + source_factor_level_id: NonEmptyString + source_condition_id: NonEmptyString + target: BindingTarget + value_type: BindingScalarType + value: BindingValue + owner: BindingOwnerModel + + @model_validator(mode="after") + def _validate_declared_value_type(self) -> ExperimentBindingDescriptorModel: + if isinstance(self.value, SecretReferenceBindingValueModel): + if self.value_type == BindingScalarType.NULL: + raise ValueError("secret references cannot use value_type null") + return self + if not _binding_value_matches_type(self.value.value, self.value_type): + raise ValueError("literal binding value does not match declared value_type") + if isinstance(self.value.value, float) and not math.isfinite(self.value.value): + raise ValueError("literal number must be finite") + return self + + +class ExperimentBindingDescriptorSetModel(ContractModel): + """Versioned, collision-free set of authoritative experiment bindings.""" + + schema_version: Literal[EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION] = ( + EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION + ) + descriptors: list[ExperimentBindingDescriptorModel] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_unique_bindings_and_targets(self) -> ExperimentBindingDescriptorSetModel: + binding_ids = [descriptor.binding_id for descriptor in self.descriptors] + if len(binding_ids) != len(set(binding_ids)): + raise ValueError("binding_id values must be unique") + canonical_targets = [ + (descriptor.source_condition_id, *descriptor.target.canonical_key()) for descriptor in self.descriptors + ] + if len(canonical_targets) != len(set(canonical_targets)): + raise ValueError("binding descriptors must not contain a duplicate canonical target") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + _add_aces_invariant( + json_schema, + "binding-descriptors-canonical-targets-injective", + "Binding ids must be unique and target resolution must be injective within each source condition.", + validator=( + "raes_contracts.contracts.ExperimentBindingDescriptorSetModel._validate_unique_bindings_and_targets" + ), + inputs=[{"contract_id": "experiment-binding-descriptors-v1", "instance_path": "#"}], + ) + return json_schema + + +class RealizedBindingProvenanceModel(ContractModel): + """Portable provenance for one binding actually realized by its owner.""" + + descriptor: ExperimentBindingDescriptorModel + origin: Literal["selection", "default", "override"] + configuration_digest: PrefixedDigestString | None = None + + @model_validator(mode="after") + def _validate_configuration_digest_scope(self) -> RealizedBindingProvenanceModel: + if self.descriptor.target.plane != "scenario" and self.configuration_digest is None: + raise ValueError("participant and apparatus realized bindings require configuration_digest") + return self + + +class RealizedConfigurationValueModel(ContractModel): + """One normalized value in a complete participant configuration.""" + + target_id: NonEmptyString + value_type: BindingScalarType + origin: Literal["default", "override"] + value: BindingValue + + @model_validator(mode="after") + def _validate_value_type(self) -> RealizedConfigurationValueModel: + if isinstance(self.value, LiteralBindingValueModel): + if not _binding_value_matches_type(self.value.value, self.value_type): + raise ValueError("realized configuration value does not match value_type") + if isinstance(self.value.value, float) and not math.isfinite(self.value.value): + raise ValueError("realized configuration number must be finite") + elif self.value_type == BindingScalarType.NULL: + raise ValueError("null configuration values cannot be secret references") + return self + + +class ParticipantConfigurationModel(ContractModel): + """Canonical digest payload for one complete participant configuration.""" + + implementation_identity: ApparatusIdentityModel + manifest_version: NonEmptyString + owner: BindingOwnerModel + values: list[RealizedConfigurationValueModel] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_canonical_values(self) -> ParticipantConfigurationModel: + target_ids = [entry.target_id for entry in self.values] + if target_ids != sorted(target_ids): + raise ValueError("participant configuration values must be ordered by canonical target id") + if len(target_ids) != len(set(target_ids)): + raise ValueError("participant configuration values must have unique canonical target ids") + return self + + +class ParticipantConfigurationResultModel(ContractModel): + """Portable result of one complete, atomic participant configuration validation.""" + + schema_version: Literal[PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION] = ( + PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION + ) + participant_address: NonEmptyString + manifest_ref: NonEmptyString + manifest_digest: PrefixedDigestString + configuration: ParticipantConfigurationModel + configuration_digest: PrefixedDigestString + + @model_validator(mode="after") + def _validate_configuration_digest(self) -> ParticipantConfigurationResultModel: + from ..satisfiability import canonical_contract_digest + + if self.configuration_digest != canonical_contract_digest(self.configuration): + raise ValueError("configuration_digest must match the canonical normalized configuration") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + _add_aces_invariant( + json_schema, + "participant-configuration-digest-valid", + "The configuration digest must be the RFC 8785/JCS digest of the complete normalized configuration.", + validator=("raes_contracts.contracts.ParticipantConfigurationResultModel._validate_configuration_digest"), + inputs=[{"contract_id": "participant-configuration-result-v1", "instance_path": "#"}], + ) + return json_schema + + +def _binding_value_matches_type(value: BindingScalar, value_type: BindingScalarType) -> bool: + if value_type == BindingScalarType.NULL: + return value is None + if value_type == BindingScalarType.BOOLEAN: + return isinstance(value, bool) + if value_type == BindingScalarType.INTEGER: + return isinstance(value, int) and not isinstance(value, bool) + if value_type == BindingScalarType.NUMBER: + return isinstance(value, (int, float)) and not isinstance(value, bool) + if value_type == BindingScalarType.STRING: + return isinstance(value, str) + return False + + +def _validate_realized_bindings(bindings: list[RealizedBindingProvenanceModel]) -> None: + binding_ids = [binding.descriptor.binding_id for binding in bindings] + if len(binding_ids) != len(set(binding_ids)): + raise ValueError("realized binding ids must be unique") + canonical_targets = [ + (binding.descriptor.source_condition_id, *binding.descriptor.target.canonical_key()) for binding in bindings + ] + if len(canonical_targets) != len(set(canonical_targets)): + raise ValueError("realized bindings must not contain duplicate canonical targets") + + +__all__ = [ + "ApparatusBindingTargetModel", + "BindingOwnerModel", + "BindingScalar", + "BindingScalarType", + "BindingTarget", + "BindingValue", + "ConfigurationTargetDeclarationModel", + "ConfigurationTargetRegistryModel", + "EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION", + "ExperimentBindingDescriptorModel", + "ExperimentBindingDescriptorSetModel", + "LiteralBindingValueModel", + "PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION", + "ParticipantConfigurationModel", + "ParticipantConfigurationResultModel", + "ParticipantImplementationBindingTargetModel", + "RealizedBindingProvenanceModel", + "RealizedConfigurationValueModel", + "ScenarioBindingTargetModel", + "SecretReferenceBindingValueModel", +] diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_capture.py b/implementations/python/packages/raes_contracts/contracts/experiment_capture.py index 3287e3b2a..428878667 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_capture.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_capture.py @@ -19,6 +19,13 @@ from .schema_invariants import _add_aces_invariant, _add_aces_plane from .validators import _validate_unique_string_values +__all__ = [ + "ExperimentCaptureRequirementModel", + "ExperimentCaptureSpecModel", + "ExperimentCaptureWindowModel", + "ExperimentValidityNoteModel", +] + class ExperimentValidityNoteModel(ContractModel): """Validity threat, limitation, or mitigation note for experiment interpretation.""" diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py b/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py index d6c1c0ee3..c1439fd05 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py @@ -22,6 +22,14 @@ from .schema_invariants import _add_aces_invariant from .validators import _validate_unique_string_values +__all__ = [ + "ExperimentApparatusConstraintModel", + "ExperimentAugmentationDisclosureModel", + "ExperimentEvaluationProtocolModel", + "ExperimentMetricDefinitionModel", + "ExperimentSplitAndLeakageControlsModel", +] + _SEM_225_PORTABLE_CARRIER_KINDS = frozenset( { "apparatus-context", diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_run.py b/implementations/python/packages/raes_contracts/contracts/experiment_run.py index 60facbc1f..974415650 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_run.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_run.py @@ -31,6 +31,7 @@ _reference_identity_satisfies_requirement, _reference_satisfies_requirement, ) +from .experiment_bindings import RealizedBindingProvenanceModel, _validate_realized_bindings from .experiment_disclosure import ExperimentAugmentationDisclosureModel from .experiment_evidence import ( ExperimentRealizedFormDisclosureModel, @@ -122,6 +123,7 @@ class ExperimentRunModel(ContractModel): apparatus_context: ExperimentApparatusContextModel participant_implementation_provenance: ParticipantImplementationProvenanceModel | None = None parameter_set: list[ExperimentParameterModel] = Field(min_length=1) + realized_bindings: list[RealizedBindingProvenanceModel] = Field(default_factory=list) stochastic_controls: list[ExperimentStochasticControlModel] = Field(min_length=1) stochastic_draws: list[RandomStreamDrawRecordModel] = Field(default_factory=list) started_at: Rfc3339DateTimeString @@ -152,6 +154,7 @@ def _validate_archival_run(self) -> ExperimentRunModel: _validate_run_evidence_artifact_refs(self) _validate_run_realized_form_disclosures(self) _validate_run_augmentation_disclosures(self) + _validate_realized_bindings(self.realized_bindings) validate_carrier_validation_basis_disclosures(self, subject_kind="experiment_run") if self.realized_time_model is not None: validate_realized_time_model( diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_spec.py b/implementations/python/packages/raes_contracts/contracts/experiment_spec.py index c3b9a52b2..b61832d00 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_spec.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_spec.py @@ -12,6 +12,7 @@ from .base import BehavioralClaimBindingModel, ContractModel, NonEmptyString, PositiveInteger from .experiment_apparatus import ExperimentClockContextModel, ExperimentStochasticControlModel from .experiment_artifacts import ExperimentArtifactRefModel +from .experiment_bindings import ExperimentBindingDescriptorSetModel from .experiment_capture import ExperimentValidityNoteModel from .experiment_disclosure import ExperimentApparatusConstraintModel from .experiment_manifest_references import ExperimentCaptureSpecReferenceModel @@ -338,6 +339,8 @@ class ExperimentSpecModel(ContractModel): intended_scenario_ref: ExperimentScenarioReferenceModel | None = None apparatus_intent: ExperimentApparatusConstraintModel | None = None factors: dict[NonEmptyString, ExperimentStudyFactorModel] = Field(default_factory=dict) + binding_semantics: Literal["descriptive", "explicit-required"] = "descriptive" + binding_descriptors: ExperimentBindingDescriptorSetModel | None = None capture_spec_refs: list[ExperimentCaptureSpecReferenceModel] = Field(default_factory=list) validity_notes: list[ExperimentValidityNoteModel] = Field(default_factory=list) artifact_refs: list[ExperimentArtifactRefModel] = Field(default_factory=list) @@ -352,8 +355,55 @@ def _validate_experiment_spec(self) -> ExperimentSpecModel: raise ValueError( f"run_plan allocation blocking factor '{blocking_factor}' must be a declared factor" ) + self._validate_binding_descriptors() return self + def _validate_binding_descriptors(self) -> None: + if self.binding_semantics == "explicit-required" and self.binding_descriptors is None: + raise ValueError("binding_semantics explicit-required requires binding_descriptors") + if self.binding_descriptors is None: + return + if self.binding_semantics != "explicit-required": + raise ValueError("binding_descriptors require binding_semantics explicit-required") + allocation = self.run_plan.allocation + if allocation is None: + raise ValueError("explicit binding descriptors require condition-based run allocation") + legacy_conditions = sorted( + condition_id + for condition_id, assignment in allocation.condition_assignments.items() + if assignment.required_parameters + ) + if legacy_conditions: + raise ValueError( + "explicit binding semantics reject legacy required_parameters: " + ", ".join(legacy_conditions) + ) + covered_conditions: set[str] = set() + for descriptor in self.binding_descriptors.descriptors: + factor = self.factors.get(descriptor.source_factor_id) + if factor is None: + raise ValueError( + f"binding source factor {descriptor.source_factor_id!r} must reference a declared factor" + ) + if descriptor.source_factor_level_id not in factor.levels: + raise ValueError( + f"binding source factor level {descriptor.source_factor_level_id!r} must be declared " + f"by factor {descriptor.source_factor_id!r}" + ) + assignment = allocation.condition_assignments.get(descriptor.source_condition_id) + if assignment is None: + raise ValueError( + f"binding source condition {descriptor.source_condition_id!r} must reference an allocation condition" + ) + assigned_level = assignment.factor_levels.get(descriptor.source_factor_id) + if assigned_level != descriptor.source_factor_level_id: + raise ValueError("binding source factor level must match its condition assignment") + covered_conditions.add(descriptor.source_condition_id) + missing_conditions = sorted(set(allocation.compared_conditions) - covered_conditions) + if missing_conditions: + raise ValueError( + "explicit binding descriptors must cover every compared condition: " + ", ".join(missing_conditions) + ) + @classmethod def __get_pydantic_json_schema__( cls, @@ -362,6 +412,29 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"binding_semantics": {"const": "explicit-required"}}, + "required": ["binding_semantics"], + }, + "then": { + "required": ["binding_descriptors"], + "properties": {"binding_descriptors": {"not": {"type": "null"}}}, + }, + }, + { + "if": { + "properties": {"binding_descriptors": {"not": {"type": "null"}}}, + "required": ["binding_descriptors"], + }, + "then": { + "properties": {"binding_semantics": {"const": "explicit-required"}}, + }, + }, + ] + ) _add_aces_invariant( json_schema, "experiment-spec-blocking-factors-declared", @@ -370,4 +443,11 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ExperimentSpecModel._validate_experiment_spec", inputs=[{"contract_id": "experiment-authoring-input-v1", "instance_path": "#"}], ) + _add_aces_invariant( + json_schema, + "experiment-binding-source-joins-valid", + "Explicit bindings must cover every compared condition and resolve exact declared factor levels.", + validator="raes_contracts.contracts.ExperimentSpecModel._validate_binding_descriptors", + inputs=[{"contract_id": "experiment-authoring-input-v1", "instance_path": "#"}], + ) return json_schema diff --git a/implementations/python/packages/raes_contracts/contracts/manifests.py b/implementations/python/packages/raes_contracts/contracts/manifests.py index 9e7654409..7edf4add3 100644 --- a/implementations/python/packages/raes_contracts/contracts/manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/manifests.py @@ -5,7 +5,7 @@ import re from typing import Literal -from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic import Field, GetJsonSchemaHandler, SerializerFunctionWrapHandler, model_serializer, model_validator from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema @@ -27,6 +27,7 @@ ProcessorCompatibilityModel, ProvisionerCapabilitiesModel, ) +from .experiment_bindings import ConfigurationTargetRegistryModel from .trial_cleanup import CleanupActionKind from .validators import ( _validate_canonical_concept_bindings, @@ -424,16 +425,32 @@ class ProcessorManifestV2Model(ContractModel): concept_bindings: list[ConceptBindingEntryModel] = Field(min_length=1) constraints: dict[str, str] = Field(default_factory=dict) capabilities: ProcessorCapabilitiesV2Model + configuration_registry: ConfigurationTargetRegistryModel | None = None @model_validator(mode="after") def _validate_unique_binding_scopes(self) -> ProcessorManifestV2Model: validate_processor_supported_contract_versions(self.supported_contract_versions) + if ( + self.configuration_registry is not None + and "experiment-binding-descriptors-v1" not in self.supported_contract_versions + ): + raise ValueError("configuration_registry requires experiment-binding-descriptors-v1 support") scopes = [binding.scope for binding in self.concept_bindings] if len(scopes) != len(set(scopes)): raise ValueError("concept_bindings must not contain duplicate scopes") _validate_canonical_concept_bindings(self, allowed_scopes=_PROCESSOR_CONCEPT_BINDING_SCOPES) return self + @model_serializer(mode="wrap") + def _serialize_optional_configuration_registry( + self, + handler: SerializerFunctionWrapHandler, + ) -> dict[str, object]: + payload = handler(self) + if self.configuration_registry is None: + payload.pop("configuration_registry", None) + return payload + @classmethod def __get_pydantic_json_schema__( cls, diff --git a/implementations/python/packages/raes_contracts/contracts/participant_manifests.py b/implementations/python/packages/raes_contracts/contracts/participant_manifests.py index 2f5d04b96..404e8e59e 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_manifests.py @@ -4,7 +4,7 @@ from typing import Annotated, Any, Literal -from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic import Field, GetJsonSchemaHandler, SerializerFunctionWrapHandler, model_serializer, model_validator from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema @@ -31,6 +31,7 @@ BackendCompatibilityModel, RealizationSupportDeclarationModel, ) +from .experiment_bindings import ConfigurationTargetRegistryModel from .manifests import BackendCapabilitiesV2Model, ConceptBindingEntryModel from .realization_plans import RealizationEnvelopeIdentityModel from .validators import ( @@ -50,10 +51,16 @@ class BackendManifestV2Model(ContractModel): concept_bindings: list[ConceptBindingEntryModel] = Field(min_length=1) constraints: dict[str, str] = Field(default_factory=dict) capabilities: BackendCapabilitiesV2Model + configuration_registry: ConfigurationTargetRegistryModel | None = None @model_validator(mode="after") def _validate_unique_binding_scopes(self) -> BackendManifestV2Model: validate_backend_supported_contract_versions(self.supported_contract_versions) + if ( + self.configuration_registry is not None + and "experiment-binding-descriptors-v1" not in self.supported_contract_versions + ): + raise ValueError("configuration_registry requires experiment-binding-descriptors-v1 support") self._validate_realization_envelope_contract() self._validate_cleanup_contracts() self._validate_time_contracts() @@ -104,6 +111,16 @@ def _validate_concept_bindings(self) -> None: raise ValueError("concept_bindings must not contain duplicate scopes") _validate_canonical_concept_bindings(self, allowed_scopes=_BACKEND_CONCEPT_BINDING_SCOPES) + @model_serializer(mode="wrap") + def _serialize_optional_configuration_registry( + self, + handler: SerializerFunctionWrapHandler, + ) -> dict[str, object]: + payload = handler(self) + if self.configuration_registry is None: + payload.pop("configuration_registry", None) + return payload + @classmethod def __get_pydantic_json_schema__( cls, @@ -230,6 +247,7 @@ class ParticipantImplementationManifestModel(ContractModel): concept_bindings: list[ConceptBindingEntryModel] = Field(min_length=1) constraints: dict[str, str] = Field(default_factory=dict) capabilities: ParticipantImplementationCapabilitiesModel + configuration_registry: ConfigurationTargetRegistryModel | None = None @model_validator(mode="after") def _validate_participant_implementation_manifest(self) -> ParticipantImplementationManifestModel: @@ -244,6 +262,14 @@ def _validate_participant_implementation_manifest(self) -> ParticipantImplementa raise ValueError( "supported_participant_contracts must be declared in supported_contract_versions: " + joined ) + if self.configuration_registry is not None: + required_configuration_contracts = { + "experiment-binding-descriptors-v1", + "participant-configuration-result-v1", + } + missing = sorted(required_configuration_contracts - set(self.supported_contract_versions)) + if missing: + raise ValueError("configuration_registry requires supported_contract_versions: " + ", ".join(missing)) scopes = [binding.scope for binding in self.concept_bindings] if len(scopes) != len(set(scopes)): raise ValueError("concept_bindings must not contain duplicate scopes") @@ -253,6 +279,16 @@ def _validate_participant_implementation_manifest(self) -> ParticipantImplementa ) return self + @model_serializer(mode="wrap") + def _serialize_optional_configuration_registry( + self, + handler: SerializerFunctionWrapHandler, + ) -> dict[str, object]: + payload = handler(self) + if self.configuration_registry is None: + payload.pop("configuration_registry", None) + return payload + @classmethod def __get_pydantic_json_schema__( cls, @@ -316,6 +352,8 @@ class ParticipantImplementationSelectionModel(ContractModel): @model_validator(mode="after") def _validate_participant_implementation_selection(self) -> ParticipantImplementationSelectionModel: + if (self.configuration_ref is None) != (self.configuration_digest is None): + raise ValueError("configuration_ref and configuration_digest must be supplied together") _validate_unique_string_values("participant_contract_versions", self.participant_contract_versions) validate_participant_supported_contract_versions(self.participant_contract_versions) _validate_controlled_vocabulary_terms( @@ -324,6 +362,33 @@ def _validate_participant_implementation_selection(self) -> ParticipantImplement ) return self + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("oneOf", []).extend( + [ + { + "required": ["configuration_ref", "configuration_digest"], + "properties": { + "configuration_ref": {"not": {"type": "null"}}, + "configuration_digest": {"not": {"type": "null"}}, + }, + }, + { + "properties": { + "configuration_ref": {"type": "null"}, + "configuration_digest": {"type": "null"}, + } + }, + ] + ) + return json_schema + class ParticipantImplementationProvenanceModel(ContractModel): schema_version: Literal[PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION] = ( diff --git a/implementations/python/packages/raes_contracts/experiment_bindings.py b/implementations/python/packages/raes_contracts/experiment_bindings.py new file mode 100644 index 000000000..6d38fa55f --- /dev/null +++ b/implementations/python/packages/raes_contracts/experiment_bindings.py @@ -0,0 +1,176 @@ +"""Cross-artifact admission for authoritative experiment binding targets.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Literal, Protocol + +from .contracts.base import ContractModel, NonEmptyString +from .contracts.experiment_bindings import ( + ApparatusBindingTargetModel, + BindingOwnerModel, + BindingScalarType, + ConfigurationTargetDeclarationModel, + ExperimentBindingDescriptorModel, + ExperimentBindingDescriptorSetModel, + ParticipantImplementationBindingTargetModel, + ScenarioBindingTargetModel, +) +from .contracts.manifests import ProcessorManifestV2Model +from .contracts.participant_manifests import ( + BackendManifestV2Model, + ParticipantImplementationManifestModel, +) + +ParticipantManifestKey = tuple[str, str, str, str] +ApparatusManifestKey = tuple[str, str, str, str] +ApparatusManifest = ProcessorManifestV2Model | BackendManifestV2Model + + +class ScenarioBindingResolution(ContractModel): + """Canonical SDL target result returned by the public variation authority.""" + + canonical_target_id: NonEmptyString + value_type: BindingScalarType + allowed_value_kinds: list[Literal["literal", "secret-reference"]] + sensitivity: Literal["public", "internal", "restricted", "secret"] + owner: BindingOwnerModel + + +class ScenarioBindingTargetResolver(Protocol): + """Public SDL variation-target resolution seam used during admission.""" + + def resolve( + self, + scenario_family_id: str, + variation_point_id: str, + supplied_target_id: str, + ) -> ScenarioBindingResolution: ... + + +def validate_experiment_binding_targets( + descriptors: ExperimentBindingDescriptorSetModel, + *, + scenario_resolver: ScenarioBindingTargetResolver, + participant_manifests: Mapping[ParticipantManifestKey, ParticipantImplementationManifestModel], + apparatus_manifests: Mapping[ApparatusManifestKey, ApparatusManifest], +) -> ExperimentBindingDescriptorSetModel: + """Resolve every descriptor through exactly one plane owner before admission.""" + + admitted: list[ExperimentBindingDescriptorModel] = [] + for descriptor in descriptors.descriptors: + target = descriptor.target + if isinstance(target, ScenarioBindingTargetModel): + canonical_target = _resolve_scenario_target(descriptor, target, scenario_resolver) + elif isinstance(target, ParticipantImplementationBindingTargetModel): + canonical_target = _resolve_participant_target(descriptor, target, participant_manifests) + elif isinstance(target, ApparatusBindingTargetModel): + canonical_target = _resolve_apparatus_target(descriptor, target, apparatus_manifests) + else: # pragma: no cover - the discriminated union is closed before dispatch + raise ValueError("unknown binding plane") + admitted.append(descriptor.model_copy(update={"target": canonical_target})) + return ExperimentBindingDescriptorSetModel( + schema_version=descriptors.schema_version, + descriptors=admitted, + ) + + +def _resolve_scenario_target( + descriptor: ExperimentBindingDescriptorModel, + target: ScenarioBindingTargetModel, + resolver: ScenarioBindingTargetResolver, +) -> ScenarioBindingTargetModel: + resolution = resolver.resolve( + target.scenario_family_id, + target.variation_point_id, + target.target_id, + ) + if resolution.value_type != descriptor.value_type: + raise ValueError("scenario target value_type does not match binding descriptor") + if resolution.owner != descriptor.owner: + raise ValueError("scenario target owner does not match binding descriptor") + if descriptor.value.kind not in resolution.allowed_value_kinds: + raise ValueError("scenario target rejects the binding value kind") + if resolution.sensitivity == "secret" and descriptor.value.kind != "secret-reference": + raise ValueError("secret scenario targets admit only secret-reference values") + return target.model_copy(update={"target_id": resolution.canonical_target_id}) + + +def _resolve_participant_target( + descriptor: ExperimentBindingDescriptorModel, + target: ParticipantImplementationBindingTargetModel, + manifests: Mapping[ParticipantManifestKey, ParticipantImplementationManifestModel], +) -> ParticipantImplementationBindingTargetModel: + key = ( + target.participant_address, + target.implementation_name, + target.implementation_version, + target.manifest_version, + ) + manifest = manifests.get(key) + if manifest is None: + raise ValueError("participant binding target owner must resolve to the selected manifest") + if ( + manifest.identity.name != target.implementation_name + or manifest.identity.version != target.implementation_version + or manifest.schema_version != target.manifest_version + ): + raise ValueError("participant binding target identity must match the resolved manifest") + registry = manifest.configuration_registry + if registry is None: + raise ValueError("selected participant manifest has no configuration target registry") + declaration = registry.resolve(target.target_id) + _validate_declared_target(descriptor, declaration, registry.owner) + return target.model_copy(update={"target_id": declaration.target_id}) + + +def _resolve_apparatus_target( + descriptor: ExperimentBindingDescriptorModel, + target: ApparatusBindingTargetModel, + manifests: Mapping[ApparatusManifestKey, ApparatusManifest], +) -> ApparatusBindingTargetModel: + key = ( + target.component_kind, + target.component_name, + target.component_version, + target.manifest_version, + ) + manifest = manifests.get(key) + if manifest is None: + raise ValueError("apparatus binding target owner must resolve to the selected manifest") + manifest_kind = "processor" if isinstance(manifest, ProcessorManifestV2Model) else "backend" + if ( + target.component_kind != manifest_kind + or manifest.identity.name != target.component_name + or manifest.identity.version != target.component_version + or manifest.schema_version != target.manifest_version + ): + raise ValueError("apparatus binding target identity and kind must match the resolved manifest") + registry = manifest.configuration_registry + if registry is None: + raise ValueError("selected apparatus manifest has no configuration target registry") + declaration = registry.resolve(target.target_id) + _validate_declared_target(descriptor, declaration, registry.owner) + return target.model_copy(update={"target_id": declaration.target_id}) + + +def _validate_declared_target( + descriptor: ExperimentBindingDescriptorModel, + declaration: ConfigurationTargetDeclarationModel, + owner: BindingOwnerModel, +) -> None: + if descriptor.owner != owner: + raise ValueError("configuration target owner does not match binding descriptor") + if descriptor.value_type != declaration.value_type: + raise ValueError("configuration target value_type does not match binding descriptor") + declaration.validate_value(descriptor.value) + + +__all__ = [ + "ApparatusManifest", + "ApparatusManifestKey", + "ParticipantManifestKey", + "ScenarioBindingResolution", + "ScenarioBindingTargetResolver", + "validate_experiment_binding_targets", +] diff --git a/implementations/python/packages/raes_contracts/manifest_authority.py b/implementations/python/packages/raes_contracts/manifest_authority.py index b4f4cd631..ac265c7d8 100644 --- a/implementations/python/packages/raes_contracts/manifest_authority.py +++ b/implementations/python/packages/raes_contracts/manifest_authority.py @@ -12,6 +12,7 @@ # separate authority surfaces and do not belong in this declaration field. PROCESSOR_SUPPORTED_CONTRACT_IDS = ( "processor-manifest-v2", + "experiment-binding-descriptors-v1", "provisioning-plan-v1", "orchestration-plan-v1", "evaluation-plan-v1", @@ -36,6 +37,7 @@ # separate authority surfaces and do not belong in this declaration field. BACKEND_SUPPORTED_CONTRACT_IDS = ( "backend-manifest-v2", + "experiment-binding-descriptors-v1", "realization-envelope-v1", "provisioning-plan-v1", "orchestration-plan-v1", @@ -71,6 +73,8 @@ PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS = ( "participant-implementation-manifest-v1", "participant-implementation-provenance-v1", + "experiment-binding-descriptors-v1", + "participant-configuration-result-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", diff --git a/implementations/python/packages/raes_contracts/participant_configuration.py b/implementations/python/packages/raes_contracts/participant_configuration.py new file mode 100644 index 000000000..0d37624e5 --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_configuration.py @@ -0,0 +1,156 @@ +"""Atomic realization of manifest-declared participant configuration.""" + +from __future__ import annotations + +from typing import Protocol + +from pydantic import Field + +from .contracts.base import ContractModel, NonEmptyString, PrefixedDigestString +from .contracts.experiment_bindings import ( + BindingValue, + ConfigurationTargetDeclarationModel, + ParticipantConfigurationModel, + ParticipantConfigurationResultModel, + RealizedConfigurationValueModel, +) +from .contracts.participant_manifests import ( + ParticipantImplementationManifestModel, + ParticipantImplementationSelectionModel, +) +from .satisfiability import canonical_contract_digest + + +class ConfigurationOverrideModel(ContractModel): + """One author-supplied override using a canonical target id or declared alias.""" + + target_id: NonEmptyString + value: BindingValue = Field(discriminator="kind") + + +class ParticipantConfigurationValidator(Protocol): + """Trusted owner hook for complete same-type normalization and validation.""" + + def validate_and_normalize( + self, + configuration: ParticipantConfigurationModel, + ) -> ParticipantConfigurationModel: ... + + +def realize_participant_configuration( + *, + participant_address: str, + manifest: ParticipantImplementationManifestModel, + manifest_ref: str, + manifest_digest: PrefixedDigestString, + overrides: list[ConfigurationOverrideModel], + validator: ParticipantConfigurationValidator | None = None, +) -> ParticipantConfigurationResultModel: + """Validate a complete configuration and return one atomic normalized result.""" + + registry = manifest.configuration_registry + if registry is None: + raise ValueError("participant implementation manifest has no configuration target registry") + + overrides_by_target: dict[str, ConfigurationOverrideModel] = {} + for override in overrides: + declaration = registry.resolve(override.target_id) + canonical_target = declaration.target_id + if canonical_target in overrides_by_target: + raise ValueError(f"duplicate canonical target {canonical_target!r} in configuration overrides") + declaration.validate_value(override.value) + overrides_by_target[canonical_target] = override + + realized_values: list[RealizedConfigurationValueModel] = [] + for target_id in sorted(registry.targets): + declaration = registry.targets[target_id] + override = overrides_by_target.get(target_id) + if override is not None: + value = override.value + origin = "override" + elif declaration.default is not None: + value = declaration.default + origin = "default" + else: + raise ValueError(f"required configuration target {target_id!r} has no override") + declaration.validate_value(value) + realized_values.append( + RealizedConfigurationValueModel( + target_id=target_id, + value_type=declaration.value_type, + origin=origin, + value=value, + ) + ) + + configuration = ParticipantConfigurationModel( + implementation_identity=manifest.identity, + manifest_version=manifest.schema_version, + owner=registry.owner, + values=realized_values, + ) + if validator is not None: + normalized = validator.validate_and_normalize(configuration) + configuration = _validate_owner_normalization(configuration, normalized, registry.targets) + + digest = canonical_contract_digest(configuration) + return ParticipantConfigurationResultModel( + participant_address=participant_address, + manifest_ref=manifest_ref, + manifest_digest=manifest_digest, + configuration=configuration, + configuration_digest=digest, + ) + + +def _validate_owner_normalization( + original: ParticipantConfigurationModel, + normalized: ParticipantConfigurationModel, + declarations: dict[str, ConfigurationTargetDeclarationModel], +) -> ParticipantConfigurationModel: + if ( + normalized.implementation_identity != original.implementation_identity + or normalized.manifest_version != original.manifest_version + or normalized.owner != original.owner + ): + raise ValueError("participant configuration validator must preserve owner and manifest identity") + if [entry.target_id for entry in normalized.values] != [entry.target_id for entry in original.values]: + raise ValueError("participant configuration validator must preserve the complete canonical target set") + original_by_target = {entry.target_id: entry for entry in original.values} + for entry in normalized.values: + original_entry = original_by_target[entry.target_id] + declaration = declarations[entry.target_id] + if entry.origin != original_entry.origin: + raise ValueError("participant configuration validator must preserve default/override origin") + if entry.value_type != declaration.value_type: + raise ValueError("participant configuration validator must preserve declared value types") + if entry.value.kind != original_entry.value.kind: + raise ValueError("participant configuration validator must preserve literal/secret-reference disposition") + if entry.value.kind == "secret-reference" and entry.value != original_entry.value: + raise ValueError("participant configuration validator must preserve secret-reference identity") + declaration.validate_value(entry.value) + return normalized + + +def validate_participant_configuration_selection( + selection: ParticipantImplementationSelectionModel, + result: ParticipantConfigurationResultModel, +) -> None: + """Verify that a participant selection names one authoritative configuration result.""" + + if selection.participant_address != result.participant_address: + raise ValueError("participant selection address does not match configuration result") + if selection.implementation_identity != result.configuration.implementation_identity: + raise ValueError("participant selection implementation identity does not match configuration result") + if selection.manifest_ref != result.manifest_ref or selection.manifest_digest != result.manifest_digest: + raise ValueError("participant selection manifest identity does not match configuration result") + if selection.configuration_digest != result.configuration_digest: + raise ValueError("participant selection configuration digest does not match authoritative result") + + +__all__ = [ + "ConfigurationOverrideModel", + "ParticipantConfigurationValidator", + "realize_participant_configuration", + "validate_participant_configuration_selection", +] diff --git a/implementations/python/packages/raes_contracts/versions.py b/implementations/python/packages/raes_contracts/versions.py index 65b3abe37..8ca7db44d 100644 --- a/implementations/python/packages/raes_contracts/versions.py +++ b/implementations/python/packages/raes_contracts/versions.py @@ -44,6 +44,8 @@ EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION = "experiment-evidence-record/v1" EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION = "experiment-derived-measure/v1" EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION = "experiment-authoring-input/v1" +EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION = "experiment-binding-descriptors/v1" +PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION = "participant-configuration-result/v1" REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION = "reusable-asset-trust-policy/v1" ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION = "associated-artifact-manifest/v1" RANDOM_STREAM_PROFILE_SCHEMA_VERSION = "random-stream-profile/v1" diff --git a/implementations/python/packages/raes_reference_backend/manifest.py b/implementations/python/packages/raes_reference_backend/manifest.py index 5c597db3b..27d05e091 100644 --- a/implementations/python/packages/raes_reference_backend/manifest.py +++ b/implementations/python/packages/raes_reference_backend/manifest.py @@ -39,7 +39,9 @@ REFERENCE_BACKEND_NAME = "reference-emulation" REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS = frozenset( - contract_id for contract_id in BACKEND_SUPPORTED_CONTRACT_IDS if contract_id != "realization-envelope-v1" + contract_id + for contract_id in BACKEND_SUPPORTED_CONTRACT_IDS + if contract_id not in {"experiment-binding-descriptors-v1", "realization-envelope-v1"} ) _TIME_DEDICATED_CONTRACT_VERSIONS = frozenset({"time-model-v1", "time-runtime-state-v1", "realized-time-model-v1"}) diff --git a/implementations/python/tests/test_backend_manifest.py b/implementations/python/tests/test_backend_manifest.py index 723a5b4bb..836d8ad84 100644 --- a/implementations/python/tests/test_backend_manifest.py +++ b/implementations/python/tests/test_backend_manifest.py @@ -39,7 +39,9 @@ V2_VALID_DIR = FIXTURES_ROOT / "backend-manifest" / "backend-manifest-v2" / "valid" V2_INVALID_DIR = FIXTURES_ROOT / "backend-manifest" / "backend-manifest-v2" / "invalid" EXPECTED_SUPPORTED_CONTRACT_VERSIONS_V2 = [ - contract_id for contract_id in BACKEND_SUPPORTED_CONTRACT_IDS if contract_id != "realization-envelope-v1" + contract_id + for contract_id in BACKEND_SUPPORTED_CONTRACT_IDS + if contract_id not in {"experiment-binding-descriptors-v1", "realization-envelope-v1"} ] diff --git a/implementations/python/tests/test_experiment_bindings.py b/implementations/python/tests/test_experiment_bindings.py new file mode 100644 index 000000000..961dfa3c0 --- /dev/null +++ b/implementations/python/tests/test_experiment_bindings.py @@ -0,0 +1,1290 @@ +"""Authoritative cross-plane experiment binding contract tests.""" + +from __future__ import annotations + +import json +from copy import deepcopy +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from pydantic import ValidationError +from raes_conformance.conformance import _validate_payload +from raes_contracts.contracts import ( + BackendManifestV2Model, + ConfigurationTargetDeclarationModel, + ExperimentBindingDescriptorSetModel, + ExperimentRunModel, + ExperimentSpecModel, + LiteralBindingValueModel, + ParticipantConfigurationResultModel, + ParticipantImplementationManifestModel, + ParticipantImplementationProvenanceModel, + ParticipantImplementationSelectionModel, + ProcessorManifestV2Model, + RealizedBindingProvenanceModel, + schema_bundle, +) +from raes_contracts.experiment_bindings import ( + ScenarioBindingResolution, + validate_experiment_binding_targets, +) +from raes_contracts.participant_configuration import ( + ConfigurationOverrideModel, + realize_participant_configuration, + validate_participant_configuration_selection, +) +from raes_contracts.satisfiability import canonical_contract_digest + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_PARTICIPANT_MANIFEST_FIXTURE = ( + _REPO_ROOT + / "contracts" + / "fixtures" + / "participant-implementation-manifest" + / "participant-implementation-manifest-v1" + / "valid" + / "reference.json" +) + + +def _scenario_binding_payload() -> dict[str, object]: + return { + "schema_version": "experiment-binding-descriptors/v1", + "descriptors": [ + { + "binding_id": "binding.worker-count", + "source_factor_id": "factor.worker-count", + "source_factor_level_id": "four", + "source_condition_id": "condition.four-workers", + "target": { + "plane": "scenario", + "scenario_family_id": "family.techvault", + "variation_point_id": "variation.worker-count", + "target_id": "variables.worker_count", + }, + "value_type": "integer", + "value": {"kind": "literal", "value": 4}, + "owner": { + "contract_id": "sdl-authoring-input-v1", + "contract_version": "1", + "validator_id": "raes-sdl-instantiation", + "validator_version": "1", + }, + } + ], + } + + +def test_binding_descriptor_preserves_explicit_source_and_plane() -> None: + model = ExperimentBindingDescriptorSetModel.model_validate(_scenario_binding_payload()) + + descriptor = model.descriptors[0] + assert descriptor.source_factor_id == "factor.worker-count" + assert descriptor.source_factor_level_id == "four" + assert descriptor.source_condition_id == "condition.four-workers" + assert descriptor.target.plane == "scenario" + assert descriptor.target.target_id == "variables.worker_count" + + +@pytest.mark.parametrize("value", [True, "4", 4.0]) +def test_binding_descriptor_rejects_integer_coercion(value: object) -> None: + payload = _scenario_binding_payload() + payload["descriptors"][0]["value"]["value"] = value # type: ignore[index] + + with pytest.raises(ValidationError, match="value_type"): + ExperimentBindingDescriptorSetModel.model_validate(payload) + + +@pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf")]) +def test_binding_descriptor_rejects_non_finite_numbers(value: float) -> None: + payload = _scenario_binding_payload() + payload["descriptors"][0]["value_type"] = "number" # type: ignore[index] + payload["descriptors"][0]["value"]["value"] = value # type: ignore[index] + + with pytest.raises(ValidationError, match="finite"): + ExperimentBindingDescriptorSetModel.model_validate(payload) + + +def test_binding_descriptor_rejects_unknown_plane() -> None: + payload = _scenario_binding_payload() + payload["descriptors"][0]["target"]["plane"] = "backend-private" # type: ignore[index] + + with pytest.raises(ValidationError): + ExperimentBindingDescriptorSetModel.model_validate(payload) + + +def test_binding_descriptor_rejects_duplicate_canonical_target_even_when_values_match() -> None: + payload = _scenario_binding_payload() + duplicate = deepcopy(payload["descriptors"][0]) # type: ignore[index] + duplicate["binding_id"] = "binding.worker-count-duplicate" + payload["descriptors"].append(duplicate) # type: ignore[union-attr] + + with pytest.raises(ValidationError, match="duplicate canonical target"): + ExperimentBindingDescriptorSetModel.model_validate(payload) + + +def test_binding_descriptor_allows_same_target_in_mutually_exclusive_conditions() -> None: + payload = _scenario_binding_payload() + second = deepcopy(payload["descriptors"][0]) # type: ignore[index] + second["binding_id"] = "binding.worker-count.two" + second["source_factor_level_id"] = "two" + second["source_condition_id"] = "condition.two-workers" + second["value"]["value"] = 2 + payload["descriptors"].append(second) # type: ignore[union-attr] + + model = ExperimentBindingDescriptorSetModel.model_validate(payload) + + assert len(model.descriptors) == 2 + + +def test_secret_reference_is_structurally_distinct_and_has_no_resolved_value_field() -> None: + payload = _scenario_binding_payload() + descriptor = payload["descriptors"][0] # type: ignore[index] + descriptor["value_type"] = "string" + descriptor["value"] = { + "kind": "secret-reference", + "reference_id": "operator-secret.techvault-password", + } + + model = ExperimentBindingDescriptorSetModel.model_validate(payload) + dumped = model.model_dump(mode="json") + + assert dumped["descriptors"][0]["value"] == { + "kind": "secret-reference", + "reference_id": "operator-secret.techvault-password", + } + assert "resolved_value" not in str(dumped) + + +def test_secret_reference_rejects_literal_or_locator_smuggling() -> None: + payload = _scenario_binding_payload() + descriptor = payload["descriptors"][0] # type: ignore[index] + descriptor["value_type"] = "string" + descriptor["value"] = { + "kind": "secret-reference", + "reference_id": "operator-secret.techvault-password", + "resolved_value": "do-not-record", + } + + with pytest.raises(ValidationError): + ExperimentBindingDescriptorSetModel.model_validate(payload) + + +@pytest.mark.parametrize("reference_id", ["/run/secrets/api-key", "../../secret", "ENV:API_KEY"]) +def test_secret_reference_rejects_host_locator_shapes(reference_id: str) -> None: + payload = _scenario_binding_payload() + descriptor = payload["descriptors"][0] # type: ignore[index] + descriptor["value_type"] = "string" + descriptor["value"] = { + "kind": "secret-reference", + "reference_id": reference_id, + } + + with pytest.raises(ValidationError): + ExperimentBindingDescriptorSetModel.model_validate(payload) + + +def _participant_manifest_payload() -> dict[str, object]: + payload = json.loads(_PARTICIPANT_MANIFEST_FIXTURE.read_text(encoding="utf-8")) + for contract_id in ["experiment-binding-descriptors-v1", "participant-configuration-result-v1"]: + if contract_id not in payload["supported_contract_versions"]: + payload["supported_contract_versions"].append(contract_id) + payload["configuration_registry"] = { + "owner": { + "contract_id": "participant-configuration-result-v1", + "contract_version": "1", + "validator_id": "reference-participant-configuration", + "validator_version": "1", + }, + "targets": { + "policy.temperature": { + "target_id": "policy.temperature", + "value_type": "number", + "aliases": ["temperature"], + "allowed_value_kinds": ["literal"], + "sensitivity": "public", + "default": {"kind": "literal", "value": 0.25}, + }, + "policy.mode": { + "target_id": "policy.mode", + "value_type": "string", + "aliases": ["mode"], + "allowed_value_kinds": ["literal"], + "sensitivity": "internal", + }, + "credentials.api": { + "target_id": "credentials.api", + "value_type": "string", + "aliases": [], + "allowed_value_kinds": ["secret-reference"], + "sensitivity": "secret", + }, + }, + } + return payload + + +def test_participant_manifest_publishes_typed_configuration_targets() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + + assert manifest.configuration_registry is not None + assert manifest.configuration_registry.targets["policy.temperature"].value_type == "number" + assert manifest.configuration_registry.targets["policy.temperature"].aliases == ["temperature"] + + +def test_configuration_registry_rejects_alias_collision_with_canonical_target() -> None: + payload = _participant_manifest_payload() + payload["configuration_registry"]["targets"]["policy.temperature"]["aliases"] = ["policy.mode"] # type: ignore[index] + + with pytest.raises(ValidationError, match="alias"): + ParticipantImplementationManifestModel.model_validate(payload) + + +@pytest.mark.parametrize("aliases", [["mode", "mode"], ["policy.mode"]]) +def test_configuration_target_rejects_duplicate_or_self_alias(aliases: list[str]) -> None: + with pytest.raises(ValidationError, match="alias"): + ConfigurationTargetDeclarationModel( + target_id="policy.mode", + value_type="string", + aliases=aliases, + allowed_value_kinds=["literal"], + sensitivity="internal", + ) + + +def test_secret_configuration_target_rejects_literal_value_kind() -> None: + with pytest.raises(ValidationError, match="admit only secret-reference"): + ConfigurationTargetDeclarationModel( + target_id="credentials.api", + value_type="string", + aliases=[], + allowed_value_kinds=["literal", "secret-reference"], + sensitivity="secret", + ) + + +def test_secret_configuration_target_rejects_portable_default() -> None: + with pytest.raises(ValidationError, match="must not declare portable defaults"): + ConfigurationTargetDeclarationModel( + target_id="credentials.api", + value_type="string", + aliases=[], + allowed_value_kinds=["secret-reference"], + sensitivity="secret", + default={"kind": "literal", "value": "plaintext"}, + ) + + +@pytest.mark.parametrize( + "missing_contract_id", + ["experiment-binding-descriptors-v1", "participant-configuration-result-v1"], +) +def test_participant_configuration_registry_requires_supported_contract_ids( + missing_contract_id: str, +) -> None: + payload = _participant_manifest_payload() + payload["supported_contract_versions"].remove(missing_contract_id) # type: ignore[union-attr] + + with pytest.raises(ValidationError, match="configuration_registry requires supported_contract_versions"): + ParticipantImplementationManifestModel.model_validate(payload) + + +def test_complete_participant_configuration_applies_defaults_and_alias_overrides_atomically() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + + result = realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[ + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ], + ) + + entries = {entry.target_id: entry for entry in result.configuration.values} + assert entries["policy.temperature"].origin == "default" + assert entries["policy.mode"].origin == "override" + assert entries["credentials.api"].value.kind == "secret-reference" + assert result.configuration_digest == canonical_contract_digest(result.configuration) + assert "operator-secret.reference-red-api" in result.model_dump_json() + assert "resolved" not in result.model_dump_json() + + +def test_participant_configuration_digest_is_independent_of_override_order_and_alias_spelling() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + common = { + "participant_address": "participants.red", + "manifest": manifest, + "manifest_ref": "manifests/reference-red-agent.json", + "manifest_digest": "sha256:" + "1" * 64, + } + canonical = realize_participant_configuration( + **common, + overrides=[ + ConfigurationOverrideModel( + target_id="policy.mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ], + ) + reordered_alias = realize_participant_configuration( + **common, + overrides=[ + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ], + ) + + assert canonical.configuration_digest == reordered_alias.configuration_digest + + +def test_participant_selection_joins_to_authoritative_configuration_result_digest() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + result = realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[ + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ], + ) + selection_payload = json.loads( + ( + _REPO_ROOT + / "contracts" + / "fixtures" + / "participant-implementation-provenance" + / "participant-implementation-provenance-v1" + / "valid" + / "reference.json" + ).read_text(encoding="utf-8") + )["participant_implementations"][0] + selection_payload.update( + { + "manifest_ref": result.manifest_ref, + "manifest_digest": result.manifest_digest, + "configuration_ref": "participant-configurations/red/result.json", + "configuration_digest": result.configuration_digest, + } + ) + selection = ParticipantImplementationSelectionModel.model_validate(selection_payload) + + validate_participant_configuration_selection(selection, result) + + mismatched = selection.model_copy(update={"configuration_digest": "sha256:" + "f" * 64}) + with pytest.raises(ValueError, match="configuration digest"): + validate_participant_configuration_selection(mismatched, result) + + +def test_participant_configuration_rejects_duplicate_canonical_override_via_alias() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + + with pytest.raises(ValueError, match="duplicate canonical target"): + realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[ + ConfigurationOverrideModel( + target_id="policy.mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ], + ) + + +def test_participant_configuration_rejects_missing_required_target_without_partial_result() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + + with pytest.raises(ValueError, match="required configuration target"): + realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[], + ) + + +def test_participant_configuration_rejects_type_coercion() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + + with pytest.raises(ValueError, match="value_type"): + realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[ + ConfigurationOverrideModel( + target_id="policy.mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="temperature", + value={"kind": "literal", "value": "0.5"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ], + ) + + +class _OriginChangingValidator: + def validate_and_normalize(self, configuration): + values = [ + value.model_copy(update={"origin": "default"}) if value.origin == "override" else value + for value in configuration.values + ] + return configuration.model_copy(update={"values": values}) + + +class _SecretDispositionChangingValidator: + def validate_and_normalize(self, configuration): + values = [ + value.model_copy(update={"value": LiteralBindingValueModel(kind="literal", value="resolved-secret")}) + if value.target_id == "policy.mode" + else value + for value in configuration.values + ] + return configuration.model_copy(update={"values": values}) + + +def test_participant_owner_normalization_must_preserve_default_override_provenance() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + + with pytest.raises(ValueError, match="origin"): + realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[ + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ], + validator=_OriginChangingValidator(), + ) + + +def test_participant_owner_normalization_must_not_replace_secret_reference_with_literal() -> None: + payload = _participant_manifest_payload() + payload["configuration_registry"]["targets"]["policy.mode"]["allowed_value_kinds"] = [ # type: ignore[index] + "literal", + "secret-reference", + ] + manifest = ParticipantImplementationManifestModel.model_validate(payload) + + with pytest.raises(ValueError, match="literal/secret-reference disposition"): + realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[ + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "secret-reference", "reference_id": "operator-secret.mode"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ], + validator=_SecretDispositionChangingValidator(), + ) + + +def _experiment_spec_payload_with_bindings() -> dict[str, object]: + payload = json.loads( + ( + _REPO_ROOT + / "contracts" + / "fixtures" + / "experiment-core" + / "experiment-authoring-input-v1" + / "valid" + / "reference.json" + ).read_text(encoding="utf-8") + ) + for condition_id, assignment in payload["run_plan"]["allocation"]["condition_assignments"].items(): + assignment.pop("required_parameters") + assignment["required_refs"] = [ + { + "ref_kind": "profile", + "ref_id": f"protocol.reference-red-tactic.{condition_id}", + } + ] + payload["binding_semantics"] = "explicit-required" + payload["binding_descriptors"] = { + "schema_version": "experiment-binding-descriptors/v1", + "descriptors": [ + { + **_scenario_binding_payload()["descriptors"][0], + "binding_id": "binding.red-tactic.aggressive", + "source_factor_id": "red-tactic", + "source_factor_level_id": "aggressive", + "source_condition_id": "cond-aggressive", + "value_type": "string", + "value": {"kind": "literal", "value": "aggressive"}, + }, + { + **_scenario_binding_payload()["descriptors"][0], + "binding_id": "binding.red-tactic.stealthy", + "source_factor_id": "red-tactic", + "source_factor_level_id": "stealthy", + "source_condition_id": "cond-stealthy", + "value_type": "string", + "value": {"kind": "literal", "value": "stealthy"}, + }, + ], + } + return payload + + +def test_experiment_spec_joins_binding_sources_to_declared_factor_levels_and_conditions() -> None: + spec = ExperimentSpecModel.model_validate(_experiment_spec_payload_with_bindings()) + + assert spec.binding_semantics == "explicit-required" + assert spec.binding_descriptors is not None + assert {item.source_condition_id for item in spec.binding_descriptors.descriptors} == { + "cond-aggressive", + "cond-stealthy", + } + + +def test_experiment_spec_rejects_binding_factor_level_mismatch() -> None: + payload = _experiment_spec_payload_with_bindings() + payload["binding_descriptors"]["descriptors"][0]["source_factor_level_id"] = "stealthy" # type: ignore[index] + + with pytest.raises(ValidationError, match="factor level"): + ExperimentSpecModel.model_validate(payload) + + +def test_experiment_spec_fails_closed_when_explicit_bindings_are_required_but_absent() -> None: + payload = _experiment_spec_payload_with_bindings() + del payload["binding_descriptors"] + + with pytest.raises(ValidationError, match="explicit-required"): + ExperimentSpecModel.model_validate(payload) + + +def test_experiment_spec_rejects_ambiguous_legacy_parameters_in_explicit_binding_mode() -> None: + payload = _experiment_spec_payload_with_bindings() + assignment = payload["run_plan"]["allocation"]["condition_assignments"]["cond-aggressive"] # type: ignore[index] + assignment["required_parameters"] = [ + { + "name": "red_tactic", + "value": "aggressive", + "value_kind": "protocol", + } + ] + + with pytest.raises(ValidationError, match="legacy required_parameters"): + ExperimentSpecModel.model_validate(payload) + + +def test_realized_binding_provenance_preserves_non_secret_value_and_configuration_digest() -> None: + descriptor = ExperimentBindingDescriptorSetModel.model_validate(_scenario_binding_payload()).descriptors[0] + + provenance = RealizedBindingProvenanceModel( + descriptor=descriptor, + origin="override", + configuration_digest="sha256:" + "2" * 64, + ) + + assert provenance.descriptor.binding_id == "binding.worker-count" + assert provenance.origin == "override" + assert provenance.configuration_digest == "sha256:" + "2" * 64 + + +@pytest.mark.parametrize( + ("model_type", "fixture_path"), + [ + ( + ProcessorManifestV2Model, + _REPO_ROOT / "contracts/fixtures/processor-manifest/processor-manifest-v2/valid/reference.json", + ), + ( + BackendManifestV2Model, + _REPO_ROOT / "contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json", + ), + ], +) +def test_apparatus_manifests_publish_typed_configuration_targets( + model_type: type[ProcessorManifestV2Model] | type[BackendManifestV2Model], + fixture_path: Path, +) -> None: + payload = json.loads(fixture_path.read_text(encoding="utf-8")) + payload["supported_contract_versions"].append("experiment-binding-descriptors-v1") + payload["configuration_registry"] = { + "owner": { + "contract_id": payload["schema_version"], + "contract_version": "1", + "validator_id": f"{payload['identity']['name']}-configuration", + "validator_version": "1", + }, + "targets": { + "execution.parallelism": { + "target_id": "execution.parallelism", + "value_type": "integer", + "aliases": ["parallelism"], + "allowed_value_kinds": ["literal"], + "sensitivity": "internal", + "default": {"kind": "literal", "value": 1}, + } + }, + } + + manifest = model_type.model_validate(payload) + + assert manifest.configuration_registry is not None + assert manifest.configuration_registry.targets["execution.parallelism"].value_type == "integer" + + +def test_experiment_run_archives_realized_binding_provenance() -> None: + payload = json.loads( + ( + _REPO_ROOT / "contracts" / "fixtures" / "experiment-core" / "experiment-run-v1" / "valid" / "reference.json" + ).read_text(encoding="utf-8") + ) + payload["realized_bindings"] = [ + { + "descriptor": _scenario_binding_payload()["descriptors"][0], + "origin": "selection", + } + ] + + run = ExperimentRunModel.model_validate(payload) + + assert run.realized_bindings[0].descriptor.source_condition_id == "condition.four-workers" + + +def test_participant_selection_requires_configuration_ref_and_digest_together() -> None: + payload = json.loads( + ( + _REPO_ROOT + / "contracts" + / "fixtures" + / "participant-implementation-provenance" + / "participant-implementation-provenance-v1" + / "valid" + / "reference.json" + ).read_text(encoding="utf-8") + ) + del payload["participant_implementations"][0]["configuration_digest"] + + with pytest.raises(ValidationError, match="configuration_ref and configuration_digest"): + ParticipantImplementationProvenanceModel.model_validate(payload) + + +class _ScenarioResolver: + def resolve( + self, + scenario_family_id: str, + variation_point_id: str, + supplied_target_id: str, + ) -> ScenarioBindingResolution: + if ( + scenario_family_id, + variation_point_id, + supplied_target_id, + ) != ("family.techvault", "variation.worker-count", "variables.worker_count"): + raise ValueError("unknown scenario variation target") + return ScenarioBindingResolution( + canonical_target_id="variables.worker_count", + value_type="integer", + allowed_value_kinds=["literal"], + sensitivity="public", + owner={ + "contract_id": "sdl-authoring-input-v1", + "contract_version": "1", + "validator_id": "raes-sdl-instantiation", + "validator_version": "1", + }, + ) + + +class _ScenarioTypeMismatchResolver(_ScenarioResolver): + def resolve(self, scenario_family_id, variation_point_id, supplied_target_id): + resolution = super().resolve(scenario_family_id, variation_point_id, supplied_target_id) + return resolution.model_copy(update={"value_type": "string"}) + + +class _ScenarioOwnerMismatchResolver(_ScenarioResolver): + def resolve(self, scenario_family_id, variation_point_id, supplied_target_id): + resolution = super().resolve(scenario_family_id, variation_point_id, supplied_target_id) + owner = resolution.owner.model_copy(update={"validator_version": "2"}) + return resolution.model_copy(update={"owner": owner}) + + +class _SecretLiteralScenarioResolver(_ScenarioResolver): + def resolve(self, scenario_family_id, variation_point_id, supplied_target_id): + resolution = super().resolve(scenario_family_id, variation_point_id, supplied_target_id) + return resolution.model_copy( + update={ + "allowed_value_kinds": ["literal", "secret-reference"], + "sensitivity": "secret", + } + ) + + +def _participant_binding_payload(target_id: str = "mode") -> dict[str, object]: + return { + "schema_version": "experiment-binding-descriptors/v1", + "descriptors": [ + { + "binding_id": "binding.participant-mode", + "source_factor_id": "participant-mode", + "source_factor_level_id": "deterministic", + "source_condition_id": "condition.deterministic", + "target": { + "plane": "participant-implementation", + "participant_address": "participants.red", + "implementation_name": "reference-red-agent", + "implementation_version": "1.0.0", + "manifest_version": "participant-implementation-manifest/v1", + "target_id": target_id, + }, + "value_type": "string", + "value": {"kind": "literal", "value": "deterministic"}, + "owner": { + "contract_id": "participant-configuration-result-v1", + "contract_version": "1", + "validator_id": "reference-participant-configuration", + "validator_version": "1", + }, + } + ], + } + + +def _apparatus_binding_payload( + *, + component_kind: str = "processor", + component_name: str = "aces-reference-processor", + component_version: str = "0.2.0", + manifest_version: str = "processor-manifest/v2", +) -> dict[str, object]: + return { + "schema_version": "experiment-binding-descriptors/v1", + "descriptors": [ + { + "binding_id": "binding.apparatus-parallelism", + "source_factor_id": "parallelism", + "source_factor_level_id": "four", + "source_condition_id": "condition.four-workers", + "target": { + "plane": "apparatus", + "component_kind": component_kind, + "component_name": component_name, + "component_version": component_version, + "manifest_version": manifest_version, + "target_id": "execution.parallelism", + }, + "value_type": "integer", + "value": {"kind": "literal", "value": 4}, + "owner": { + "contract_id": "processor-manifest/v2", + "contract_version": "1", + "validator_id": "aces-reference-processor-configuration", + "validator_version": "1", + }, + } + ], + } + + +def _processor_manifest_payload_with_registry() -> dict[str, object]: + fixture_path = ( + _REPO_ROOT + / "contracts" + / "fixtures" + / "processor-manifest" + / "processor-manifest-v2" + / "valid" + / "reference.json" + ) + payload = json.loads(fixture_path.read_text(encoding="utf-8")) + payload["supported_contract_versions"].append("experiment-binding-descriptors-v1") + payload["configuration_registry"] = { + "owner": { + "contract_id": "processor-manifest/v2", + "contract_version": "1", + "validator_id": "aces-reference-processor-configuration", + "validator_version": "1", + }, + "targets": { + "execution.parallelism": { + "target_id": "execution.parallelism", + "value_type": "integer", + "aliases": ["parallelism"], + "allowed_value_kinds": ["literal"], + "sensitivity": "internal", + "default": {"kind": "literal", "value": 1}, + } + }, + } + return payload + + +def test_canonical_target_identity_preserves_scenario_variation_point() -> None: + payload = _scenario_binding_payload() + second = deepcopy(payload["descriptors"][0]) # type: ignore[index] + second["binding_id"] = "binding.worker-count.alternate-variation" + second["target"]["variation_point_id"] = "variation.alternate-worker-count" + payload["descriptors"].append(second) # type: ignore[union-attr] + + model = ExperimentBindingDescriptorSetModel.model_validate(payload) + + assert len(model.descriptors) == 2 + + +def test_canonical_target_identity_preserves_participant_manifest_version() -> None: + payload = _participant_binding_payload() + second = deepcopy(payload["descriptors"][0]) # type: ignore[index] + second["binding_id"] = "binding.participant-mode.v2" + second["target"]["manifest_version"] = "participant-implementation-manifest/v2" + payload["descriptors"].append(second) # type: ignore[union-attr] + + model = ExperimentBindingDescriptorSetModel.model_validate(payload) + + assert len(model.descriptors) == 2 + + +def test_canonical_target_identity_preserves_apparatus_manifest_version() -> None: + payload = _apparatus_binding_payload() + second = deepcopy(payload["descriptors"][0]) # type: ignore[index] + second["binding_id"] = "binding.apparatus-parallelism.v3" + second["target"]["manifest_version"] = "processor-manifest/v3" + payload["descriptors"].append(second) # type: ignore[union-attr] + + model = ExperimentBindingDescriptorSetModel.model_validate(payload) + + assert len(model.descriptors) == 2 + + +def test_canonical_target_identity_keeps_apparatus_owner_coordinates_structured() -> None: + payload = _apparatus_binding_payload(component_name="alpha:beta", component_version="gamma") + second = deepcopy(payload["descriptors"][0]) # type: ignore[index] + second["binding_id"] = "binding.apparatus-parallelism.distinct-owner" + second["target"]["component_name"] = "alpha" + second["target"]["component_version"] = "beta@gamma" + payload["descriptors"].append(second) # type: ignore[union-attr] + + model = ExperimentBindingDescriptorSetModel.model_validate(payload) + + assert len(model.descriptors) == 2 + + +def test_binding_admission_resolves_alias_only_through_declared_participant_owner() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + descriptors = ExperimentBindingDescriptorSetModel.model_validate(_participant_binding_payload()) + + admitted = validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={ + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + }, + apparatus_manifests={}, + ) + + assert admitted.descriptors[0].target.target_id == "policy.mode" + + +@pytest.mark.parametrize( + ("resolver", "message"), + [ + (_ScenarioTypeMismatchResolver(), "value_type"), + (_ScenarioOwnerMismatchResolver(), "owner"), + ], +) +def test_scenario_binding_admission_rejects_resolver_contract_mismatch( + resolver: _ScenarioResolver, + message: str, +) -> None: + descriptors = ExperimentBindingDescriptorSetModel.model_validate(_scenario_binding_payload()) + + with pytest.raises(ValueError, match=message): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=resolver, + participant_manifests={}, + apparatus_manifests={}, + ) + + +def test_scenario_binding_admission_rejects_literal_for_secret_target() -> None: + descriptors = ExperimentBindingDescriptorSetModel.model_validate(_scenario_binding_payload()) + + with pytest.raises(ValueError, match="secret scenario targets"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_SecretLiteralScenarioResolver(), + participant_manifests={}, + apparatus_manifests={}, + ) + + +def test_binding_admission_rejects_miskeyed_participant_manifest_identity() -> None: + manifest_payload = _participant_manifest_payload() + manifest_payload["identity"]["name"] = "different-red-agent" # type: ignore[index] + manifest = ParticipantImplementationManifestModel.model_validate(manifest_payload) + descriptors = ExperimentBindingDescriptorSetModel.model_validate(_participant_binding_payload()) + + with pytest.raises(ValueError, match="identity must match"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={ + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + }, + apparatus_manifests={}, + ) + + +def test_binding_admission_rejects_participant_manifest_without_registry() -> None: + manifest_payload = _participant_manifest_payload() + del manifest_payload["configuration_registry"] + manifest = ParticipantImplementationManifestModel.model_validate(manifest_payload) + descriptors = ExperimentBindingDescriptorSetModel.model_validate(_participant_binding_payload()) + + with pytest.raises(ValueError, match="no configuration target registry"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={ + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + }, + apparatus_manifests={}, + ) + + +def test_binding_admission_rejects_participant_target_owner_mismatch() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + payload = _participant_binding_payload() + payload["descriptors"][0]["owner"]["validator_version"] = "2" # type: ignore[index] + descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + + with pytest.raises(ValueError, match="owner"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={ + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + }, + apparatus_manifests={}, + ) + + +def test_binding_admission_rejects_participant_target_value_type_mismatch() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + payload = _participant_binding_payload() + payload["descriptors"][0]["value_type"] = "integer" # type: ignore[index] + payload["descriptors"][0]["value"]["value"] = 1 # type: ignore[index] + descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + + with pytest.raises(ValueError, match="value_type"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={ + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + }, + apparatus_manifests={}, + ) + + +def test_binding_admission_rejects_miskeyed_apparatus_manifest_identity() -> None: + manifest_payload = _processor_manifest_payload_with_registry() + manifest_payload["identity"]["name"] = "different-processor" # type: ignore[index] + manifest = ProcessorManifestV2Model.model_validate(manifest_payload) + descriptors = ExperimentBindingDescriptorSetModel.model_validate(_apparatus_binding_payload()) + + with pytest.raises(ValueError, match="identity and kind must match"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={}, + apparatus_manifests={ + ( + "processor", + "aces-reference-processor", + "0.2.0", + "processor-manifest/v2", + ): manifest + }, + ) + + +def test_binding_admission_rejects_apparatus_manifest_without_registry() -> None: + fixture_path = ( + _REPO_ROOT + / "contracts" + / "fixtures" + / "processor-manifest" + / "processor-manifest-v2" + / "valid" + / "reference.json" + ) + manifest = ProcessorManifestV2Model.model_validate_json(fixture_path.read_text(encoding="utf-8")) + descriptors = ExperimentBindingDescriptorSetModel.model_validate(_apparatus_binding_payload()) + + with pytest.raises(ValueError, match="no configuration target registry"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={}, + apparatus_manifests={ + ( + "processor", + "aces-reference-processor", + "0.2.0", + "processor-manifest/v2", + ): manifest + }, + ) + + +def test_binding_admission_rejects_apparatus_kind_mismatch() -> None: + manifest = ProcessorManifestV2Model.model_validate(_processor_manifest_payload_with_registry()) + descriptors = ExperimentBindingDescriptorSetModel.model_validate( + _apparatus_binding_payload(component_kind="backend") + ) + + with pytest.raises(ValueError, match="identity and kind must match"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={}, + apparatus_manifests={ + ( + "backend", + "aces-reference-processor", + "0.2.0", + "processor-manifest/v2", + ): manifest + }, + ) + + +def test_binding_admission_rejects_unknown_participant_target_without_cross_plane_fallback() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + descriptors = ExperimentBindingDescriptorSetModel.model_validate( + _participant_binding_payload("variables.worker_count") + ) + + with pytest.raises(ValueError, match="unknown configuration target"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={ + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + }, + apparatus_manifests={}, + ) + + +def test_binding_admission_rejects_alias_and_canonical_duplicate_after_resolution() -> None: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + payload = _participant_binding_payload() + duplicate = deepcopy(payload["descriptors"][0]) # type: ignore[index] + duplicate["binding_id"] = "binding.participant-mode-duplicate" + duplicate["target"]["target_id"] = "policy.mode" + payload["descriptors"].append(duplicate) # type: ignore[union-attr] + descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + + with pytest.raises(ValidationError, match="duplicate canonical target"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={ + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + }, + apparatus_manifests={}, + ) + + +def test_scenario_binding_admission_enforces_owner_declared_value_disposition() -> None: + payload = _scenario_binding_payload() + payload["descriptors"][0]["value_type"] = "integer" # type: ignore[index] + payload["descriptors"][0]["value"] = { # type: ignore[index] + "kind": "secret-reference", + "reference_id": "operator-secret.worker-count", + } + descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + + with pytest.raises(ValueError, match="value kind"): + validate_experiment_binding_targets( + descriptors, + scenario_resolver=_ScenarioResolver(), + participant_manifests={}, + apparatus_manifests={}, + ) + + +def _participant_configuration_result_payload() -> dict[str, object]: + manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + result = realize_participant_configuration( + participant_address="participants.red", + manifest=manifest, + manifest_ref="manifests/reference-red-agent.json", + manifest_digest="sha256:" + "1" * 64, + overrides=[ + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ], + ) + return result.model_dump(mode="json") + + +@pytest.mark.parametrize( + ("contract_id", "model_type", "payload"), + [ + ( + "experiment-binding-descriptors-v1", + ExperimentBindingDescriptorSetModel, + _scenario_binding_payload(), + ), + ( + "participant-configuration-result-v1", + ParticipantConfigurationResultModel, + _participant_configuration_result_payload(), + ), + ], +) +def test_binding_contract_roots_are_published_and_registered( + contract_id: str, + model_type: type[ExperimentBindingDescriptorSetModel] | type[ParticipantConfigurationResultModel], + payload: dict[str, object], +) -> None: + assert contract_id in schema_bundle() + assert schema_bundle()[contract_id]["additionalProperties"] is False + assert not _validate_payload(contract_id, payload) + assert model_type.model_validate(payload) + + schema_family = ( + "experiment-core" + if contract_id == "experiment-binding-descriptors-v1" + else "participant-implementation-configuration" + ) + schema_path = _REPO_ROOT / "contracts" / "schemas" / schema_family / f"{contract_id}.json" + schema = json.loads(schema_path.read_text(encoding="utf-8")) + assert not list(Draft202012Validator(schema).iter_errors(payload)) + + +@pytest.mark.parametrize( + "contract_id", + ["experiment-binding-descriptors-v1", "participant-configuration-result-v1"], +) +def test_binding_contract_fixture_corpora_are_nonempty_and_enforced(contract_id: str) -> None: + fixture_family = ( + "experiment-core" + if contract_id == "experiment-binding-descriptors-v1" + else "participant-implementation-configuration" + ) + root = _REPO_ROOT / "contracts" / "fixtures" / fixture_family / contract_id + valid_paths = sorted((root / "valid").glob("*.json")) + invalid_paths = sorted((root / "invalid").glob("*.json")) + assert valid_paths + assert invalid_paths + for path in valid_paths: + assert not _validate_payload(contract_id, json.loads(path.read_text(encoding="utf-8"))) + for path in invalid_paths: + assert _validate_payload(contract_id, json.loads(path.read_text(encoding="utf-8"))) + + +def test_binding_schemas_disclose_semantic_invariants_and_explicit_mode_conditionals() -> None: + bundle = schema_bundle() + descriptor_invariants = {item["id"] for item in bundle["experiment-binding-descriptors-v1"]["x-aces-invariants"]} + result_invariants = {item["id"] for item in bundle["participant-configuration-result-v1"]["x-aces-invariants"]} + assert "binding-descriptors-canonical-targets-injective" in descriptor_invariants + assert "participant-configuration-digest-valid" in result_invariants + assert bundle["experiment-authoring-input-v1"]["allOf"] + + selection_schema = bundle["participant-implementation-provenance-v1"]["$defs"][ + "ParticipantImplementationSelectionModel" + ] + assert selection_schema["oneOf"] diff --git a/implementations/python/tests/test_participant_implementation_manifest.py b/implementations/python/tests/test_participant_implementation_manifest.py index 416531cae..e3f1da161 100644 --- a/implementations/python/tests/test_participant_implementation_manifest.py +++ b/implementations/python/tests/test_participant_implementation_manifest.py @@ -37,6 +37,8 @@ def _manifest_payload() -> dict[str, object]: "supported_contract_versions": [ "participant-implementation-manifest-v1", "participant-implementation-provenance-v1", + "experiment-binding-descriptors-v1", + "participant-configuration-result-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", @@ -64,6 +66,38 @@ def _manifest_payload() -> dict[str, object]: "tool_affordance_expectations": ["shell", "http-api"], "exposure_policy_kinds": ["task-statement", "observation-stream"], }, + "configuration_registry": { + "owner": { + "contract_id": "participant-configuration-result-v1", + "contract_version": "1", + "validator_id": "reference-participant-configuration", + "validator_version": "1", + }, + "targets": { + "policy.temperature": { + "target_id": "policy.temperature", + "value_type": "number", + "aliases": ["temperature"], + "allowed_value_kinds": ["literal"], + "sensitivity": "public", + "default": {"kind": "literal", "value": 0.25}, + }, + "policy.mode": { + "target_id": "policy.mode", + "value_type": "string", + "aliases": ["mode"], + "allowed_value_kinds": ["literal"], + "sensitivity": "internal", + }, + "credentials.api": { + "target_id": "credentials.api", + "value_type": "string", + "aliases": [], + "allowed_value_kinds": ["secret-reference"], + "sensitivity": "secret", + }, + }, + }, } @@ -110,7 +144,7 @@ def test_participant_implementation_manifest_roundtrip(): assert model.identity.name == "reference-red-agent" assert model.implementation_kind == "agent" - assert model.supported_contract_versions == list(PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS) + assert set(model.supported_contract_versions).issubset(PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS) assert model.model_dump(mode="json") == payload diff --git a/implementations/python/tests/test_sem_227_shared_time_model.py b/implementations/python/tests/test_sem_227_shared_time_model.py index dfac8a611..2b63b0a1b 100644 --- a/implementations/python/tests/test_sem_227_shared_time_model.py +++ b/implementations/python/tests/test_sem_227_shared_time_model.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest -from hypothesis import given +from hypothesis import given, settings from hypothesis import strategies as st from raes._errors import SDLParseError, SDLValidationError from raes.parser import parse_sdl, parse_sdl_file @@ -237,6 +237,7 @@ def test_shared_time_model_references_follow_module_namespacing(tmp_path: Path) assert constraint.subject_refs == ["nodes.shared.workstation"] +@settings(deadline=None) @given(st.lists(st.sampled_from(("advance", "reset")), min_size=1, max_size=30)) def test_clock_transition_history_is_append_only_across_generated_lifecycles( operations: list[str], diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index 21ce7b448..a7270e602 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -32,6 +32,8 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: return schemas_dir / "participant-implementation-manifest" / f"{name}.json" if name.startswith("participant-implementation-provenance-v"): return schemas_dir / "participant-implementation-provenance" / f"{name}.json" + if name.startswith("participant-configuration-result-v"): + return schemas_dir / "participant-implementation-configuration" / f"{name}.json" if name in {"concept-families-v1", "behavioral-relations-v1"}: return schemas_dir / "concept-authority" / f"{name}.json" if name == "reference-models-v1": From 19271ce7ed7eafb99d31ee8cf8180695a27053b1 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 06:28:07 +0200 Subject: [PATCH 02/55] Add supervisory participant lifecycle --- .ground-control.yaml | 1 - .../entries/runtime-snapshot-v1.json | 6 +- .../snapshots/runtime-snapshot-v1.json | 2268 +++++++++++++++-- ...run-310-supervisory-lifecycle-preflight.md | 380 +++ docs/explain/sdl/lineage.md | 29 + .../participant_control_validation.py | 27 +- .../contracts/realization_plans.py | 2 + .../participant_control_history.py | 70 + .../packages/raes_contracts/runtime_state.py | 7 + .../packages/raes_runtime/control_plane.py | 6 + .../raes_runtime/control_plane_api.py | 40 + .../raes_runtime/control_plane_api_models.py | 1 + .../raes_runtime/control_plane_security.py | 13 + .../raes_runtime/control_plane_store.py | 167 +- .../raes_runtime/control_plane_store_local.py | 167 ++ .../raes_runtime/participant_control.py | 44 + .../participant_control_intents.py | 138 + .../participant_control_mediation.py | 413 +++ .../participant_control_occurrences.py | 165 ++ .../participant_control_targets.py | 239 ++ .../participant_result_contracts.py | 14 + ...api_409_participant_control_occurrences.py | 39 + .../test_run_310_supervisory_lifecycle.py | 922 +++++++ .../tests/test_sem_227_shared_time_model.py | 3 +- 24 files changed, 4864 insertions(+), 297 deletions(-) create mode 100644 docs/decisions/issue-255-run-310-supervisory-lifecycle-preflight.md create mode 100644 implementations/python/packages/raes_contracts/participant_control_history.py create mode 100644 implementations/python/packages/raes_runtime/control_plane_store_local.py create mode 100644 implementations/python/packages/raes_runtime/participant_control_intents.py create mode 100644 implementations/python/packages/raes_runtime/participant_control_mediation.py create mode 100644 implementations/python/packages/raes_runtime/participant_control_occurrences.py create mode 100644 implementations/python/packages/raes_runtime/participant_control_targets.py create mode 100644 implementations/python/tests/test_run_310_supervisory_lifecycle.py diff --git a/.ground-control.yaml b/.ground-control.yaml index 35764b894..eb852fb8f 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -25,7 +25,6 @@ requirements: routing: enabled: true default_provider: claude - default_fallback: parent stages: {} sonarcloud: project_key: Brad-Edwards_aces diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index 51061278a..3ca616461 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": "8117be2036ef71aa51cbd5529121ec50d5354ef688d1cc54134f768ce7524df8", + "content_hash": "ce74f9bb39d44c18badc1451d7f4d363d58e3938de8e360dc020ea2d1e4bcdf7", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "8117be2036ef71aa51cbd5529121ec50d5354ef688d1cc54134f768ce7524df8" + "summary": "Added first-class append-only API-409 participant control history for RUN-310 supervisory lifecycle persistence and replay.", + "content_hash": "ce74f9bb39d44c18badc1451d7f4d363d58e3938de8e360dc020ea2d1e4bcdf7" } } diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index 35ab3b805..d6cd31459 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -599,6 +599,172 @@ "title": "ParticipantAdmissionDisposition", "type": "string" }, + "ParticipantApprovalOccurrenceModel": { + "additionalProperties": false, + "description": "Approval of exactly one proposal revision, before action admission.", + "properties": { + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" + }, + "controlled_scope_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Controlled Scope Refs", + "type": "array" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", + "type": "string" + }, + "decision_ref": { + "minLength": 1, + "title": "Decision Ref", + "type": "string" + }, + "decision_revision": { + "minimum": 1, + "title": "Decision Revision", + "type": "integer" + }, + "declaration_ref": { + "minLength": 1, + "title": "Declaration Ref", + "type": "string" + }, + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "kind": { + "const": "approval", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitation Refs", + "type": "array" + }, + "mixed_control_policy_ref": { + "minLength": 1, + "title": "Mixed Control Policy Ref", + "type": "string" + }, + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", + "type": "integer" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "proposal_ref": { + "minLength": 1, + "title": "Proposal Ref", + "type": "string" + }, + "proposal_revision": { + "minimum": 1, + "title": "Proposal Revision", + "type": "integer" + }, + "reason_code": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Code" + }, + "reason_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Ref" + }, + "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", + "proposal_ref", + "proposal_revision", + "decision_ref", + "decision_revision" + ], + "title": "ParticipantApprovalOccurrenceModel", + "type": "object" + }, "ParticipantAttributionCandidateKind": { "description": "Portable candidate classes for participant attribution edges.", "enum": [ @@ -1229,60 +1395,107 @@ "title": "ParticipantBehaviorHistoryEventType", "type": "string" }, - "ParticipantEffectClass": { - "description": "SEM-211 effect classes for participant action results.", + "ParticipantCancellationEffect": { + "description": "What remained cancellable when the occurrence was recorded.", "enum": [ - "intended_effect", - "side_effect", - "observation_effect", - "visibility_effect", - "detection_effect", - "evidence_effect", - "no_effect", - "unknown_effect" + "prevented", + "partial-limitation", + "too-late" ], - "title": "ParticipantEffectClass", + "title": "ParticipantCancellationEffect", "type": "string" }, - "ParticipantEpisodeHistoryEventModel": { + "ParticipantCancellationOccurrenceModel": { "additionalProperties": false, + "description": "A cancellation with an explicit non-retroactive effect.", "properties": { - "control_action": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Control Action" + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" }, - "details": { - "additionalProperties": true, - "title": "Details", - "type": "object" + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" }, - "episode_id": { - "title": "Episode Id", + "cancellation_effect": { + "$ref": "#/$defs/ParticipantCancellationEffect" + }, + "controlled_scope_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Controlled Scope Refs", + "type": "array" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", "type": "string" }, - "event_type": { - "title": "Event Type", + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", "type": "string" }, - "participant_address": { - "title": "Participant Address", + "declaration_ref": { + "minLength": 1, + "title": "Declaration Ref", "type": "string" }, - "sequence_number": { - "title": "Sequence Number", + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", "type": "integer" }, - "terminal_reason": { + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "kind": { + "const": "cancellation", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitation Refs", + "type": "array" + }, + "mixed_control_policy_ref": { + "minLength": 1, + "title": "Mixed Control Policy Ref", + "type": "string" + }, + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", + "type": "integer" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "reason_code": { "anyOf": [ { + "minLength": 1, "type": "string" }, { @@ -1290,71 +1503,1296 @@ } ], "default": null, - "title": "Terminal Reason" + "title": "Reason Code" }, - "timestamp": { - "title": "Timestamp", + "reason_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Ref" + }, + "target_kind": { + "enum": [ + "proposal", + "decision", + "admitted-action", + "attempt" + ], + "title": "Target Kind", + "type": "string" + }, + "target_ref": { + "minLength": 1, + "title": "Target Ref", "type": "string" + }, + "target_revision": { + "minimum": 1, + "title": "Target 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": [ - "event_type", - "timestamp", - "participant_address", - "episode_id", - "sequence_number" + "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", + "target_kind", + "target_ref", + "target_revision", + "cancellation_effect" ], - "title": "ParticipantEpisodeHistoryEventModel", + "title": "ParticipantCancellationOccurrenceModel", "type": "object" }, - "ParticipantEpisodeStateModel": { + "ParticipantControlDisposition": { + "description": "Realized disposition of one portable control occurrence.", + "enum": [ + "recorded", + "accepted", + "rejected", + "limited", + "superseded", + "cancelled" + ], + "title": "ParticipantControlDisposition", + "type": "string" + }, + "ParticipantControlOccurrenceModel": { "additionalProperties": false, + "description": "Closed participant-runtime carrier for one API-409 control fact.", "properties": { - "episode_id": { - "title": "Episode Id", + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", "type": "string" }, - "initialized_at": { - "title": "Initialized At", + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", "type": "string" }, - "last_control_action": { - "title": "Last Control Action", + "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" }, - "participant_address": { - "title": "Participant Address", + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "event_type": { + "const": "participant-control-occurrence", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "extension_policy": { + "const": "closed", + "title": "Extension Policy", + "type": "string" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "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" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "object_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "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": { + "discriminator": { + "mapping": { + "approval": "#/$defs/ParticipantApprovalOccurrenceModel", + "cancellation": "#/$defs/ParticipantCancellationOccurrenceModel", + "denial": "#/$defs/ParticipantDenialOccurrenceModel", + "external-direction": "#/$defs/ParticipantExternalDirectionOccurrenceModel", + "handoff": "#/$defs/ParticipantHandoffOccurrenceModel", + "intervention": "#/$defs/ParticipantInterventionOccurrenceModel", + "override": "#/$defs/ParticipantOverrideOccurrenceModel", + "proposal": "#/$defs/ParticipantProposalOccurrenceModel" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/ParticipantProposalOccurrenceModel" + }, + { + "$ref": "#/$defs/ParticipantApprovalOccurrenceModel" + }, + { + "$ref": "#/$defs/ParticipantDenialOccurrenceModel" + }, + { + "$ref": "#/$defs/ParticipantExternalDirectionOccurrenceModel" + }, + { + "$ref": "#/$defs/ParticipantInterventionOccurrenceModel" + }, + { + "$ref": "#/$defs/ParticipantHandoffOccurrenceModel" + }, + { + "$ref": "#/$defs/ParticipantOverrideOccurrenceModel" + }, + { + "$ref": "#/$defs/ParticipantCancellationOccurrenceModel" + } + ], + "title": "Occurrence" + }, + "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" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "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": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "schema_name": { + "const": "participant-control-occurrence", + "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_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" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + } + }, + "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", + "provenance_refs", + "evidence_refs", + "object_marking_refs", + "authorization_scope", + "occurrence" + ], + "title": "ParticipantControlOccurrenceModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Every occurrence must resolve one matching compiled ACT-617 declaration and preserve participant, episode, controller, authority, policy revision, order, proposal, target, and semantic identity joins.", + "id": "participant-control-occurrence-context-agreement", + "inputs": [ + { + "contract_id": "participant-control-occurrence-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.validate_participant_control_occurrence_context" + }, + { + "description": "Proposal, approval, denial, direction, intervention, handoff, override, and cancellation remain distinct from action admission, execution, delivery, observation, and audit evidence.", + "id": "participant-control-occurrence-not-lifecycle-evidence", + "inputs": [ + { + "contract_id": "participant-control-occurrence-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantControlOccurrenceModel" + } + ] + }, + "ParticipantDenialOccurrenceModel": { + "additionalProperties": false, + "description": "Denial of exactly one proposal revision.", + "properties": { + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" + }, + "controlled_scope_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Controlled Scope Refs", + "type": "array" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", + "type": "string" + }, + "decision_ref": { + "minLength": 1, + "title": "Decision Ref", + "type": "string" + }, + "decision_revision": { + "minimum": 1, + "title": "Decision Revision", + "type": "integer" + }, + "declaration_ref": { + "minLength": 1, + "title": "Declaration Ref", + "type": "string" + }, + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "kind": { + "const": "denial", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitation Refs", + "type": "array" + }, + "mixed_control_policy_ref": { + "minLength": 1, + "title": "Mixed Control Policy Ref", + "type": "string" + }, + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", + "type": "integer" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "proposal_ref": { + "minLength": 1, + "title": "Proposal Ref", + "type": "string" + }, + "proposal_revision": { + "minimum": 1, + "title": "Proposal Revision", + "type": "integer" + }, + "reason_code": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Code" + }, + "reason_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Ref" + }, + "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", + "proposal_ref", + "proposal_revision", + "decision_ref", + "decision_revision" + ], + "title": "ParticipantDenialOccurrenceModel", + "type": "object" + }, + "ParticipantEffectClass": { + "description": "SEM-211 effect classes for participant action results.", + "enum": [ + "intended_effect", + "side_effect", + "observation_effect", + "visibility_effect", + "detection_effect", + "evidence_effect", + "no_effect", + "unknown_effect" + ], + "title": "ParticipantEffectClass", + "type": "string" + }, + "ParticipantEpisodeHistoryEventModel": { + "additionalProperties": false, + "properties": { + "control_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Control Action" + }, + "details": { + "additionalProperties": true, + "title": "Details", + "type": "object" + }, + "episode_id": { + "title": "Episode Id", + "type": "string" + }, + "event_type": { + "title": "Event Type", + "type": "string" + }, + "participant_address": { + "title": "Participant Address", + "type": "string" + }, + "sequence_number": { + "title": "Sequence Number", + "type": "integer" + }, + "terminal_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Terminal Reason" + }, + "timestamp": { + "title": "Timestamp", + "type": "string" + } + }, + "required": [ + "event_type", + "timestamp", + "participant_address", + "episode_id", + "sequence_number" + ], + "title": "ParticipantEpisodeHistoryEventModel", + "type": "object" + }, + "ParticipantEpisodeStateModel": { + "additionalProperties": false, + "properties": { + "episode_id": { + "title": "Episode Id", + "type": "string" + }, + "initialized_at": { + "title": "Initialized At", + "type": "string" + }, + "last_control_action": { + "title": "Last Control Action", + "type": "string" + }, + "participant_address": { + "title": "Participant Address", + "type": "string" + }, + "previous_episode_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Previous Episode Id" + }, + "sequence_number": { + "title": "Sequence Number", + "type": "integer" + }, + "state_schema_version": { + "const": "participant-episode-state/v1", + "default": "participant-episode-state/v1", + "title": "State Schema Version", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "terminal_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Terminal Reason" + }, + "terminated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Terminated At" + }, + "updated_at": { + "title": "Updated At", + "type": "string" + } + }, + "required": [ + "participant_address", + "episode_id", + "sequence_number", + "status", + "initialized_at", + "updated_at", + "last_control_action" + ], + "title": "ParticipantEpisodeStateModel", + "type": "object" + }, + "ParticipantExternalDirectionOccurrenceModel": { + "additionalProperties": false, + "description": "A scoped direction that does not bypass proposal validation or admission.", + "properties": { + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" + }, + "controlled_scope_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Controlled Scope Refs", + "type": "array" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", + "type": "string" + }, + "declaration_ref": { + "minLength": 1, + "title": "Declaration Ref", + "type": "string" + }, + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "kind": { + "const": "external-direction", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitation Refs", + "type": "array" + }, + "mixed_control_policy_ref": { + "minLength": 1, + "title": "Mixed Control Policy Ref", + "type": "string" + }, + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", + "type": "integer" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "reason_code": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Code" + }, + "reason_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Ref" + }, + "target_kind": { + "enum": [ + "proposal", + "action", + "control" + ], + "title": "Target Kind", + "type": "string" + }, + "target_ref": { + "minLength": 1, + "title": "Target Ref", + "type": "string" + }, + "target_revision": { + "minimum": 1, + "title": "Target 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", + "target_kind", + "target_ref", + "target_revision" + ], + "title": "ParticipantExternalDirectionOccurrenceModel", + "type": "object" + }, + "ParticipantFailureClass": { + "description": "SEM-211 portable failure classes for participant action attempts.", + "enum": [ + "precondition_unsatisfied", + "unsupported_action", + "target_unavailable", + "authority_denied", + "resource_exhausted", + "timeout", + "interrupted", + "contention_lost", + "partial_success", + "unsafe_withheld", + "backend_error", + "unknown" + ], + "title": "ParticipantFailureClass", + "type": "string" + }, + "ParticipantHandoffOccurrenceModel": { + "additionalProperties": false, + "description": "A controller-state handoff that preserves participant identity.", + "properties": { + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" + }, + "completion_evidence_ref": { + "minLength": 1, + "title": "Completion Evidence Ref", + "type": "string" + }, + "controlled_scope_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Controlled Scope Refs", + "type": "array" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", + "type": "string" + }, + "declaration_ref": { + "minLength": 1, + "title": "Declaration Ref", + "type": "string" + }, + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "kind": { + "const": "handoff", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitation Refs", + "type": "array" + }, + "mixed_control_policy_ref": { + "minLength": 1, + "title": "Mixed Control Policy Ref", + "type": "string" + }, + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", + "type": "integer" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "prior_controller_state_ref": { + "minLength": 1, + "title": "Prior Controller State Ref", + "type": "string" + }, + "reason_code": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Code" + }, + "reason_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "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" + }, + "ParticipantInteractionClass": { + "description": "SEM-209 interaction classes for multi-participant behavior.", + "enum": [ + "coordination", + "contention", + "interference", + "shared_state_change" + ], + "title": "ParticipantInteractionClass", + "type": "string" + }, + "ParticipantInterventionOccurrenceModel": { + "additionalProperties": false, + "description": "An intervention against an existing control or action occurrence.", + "properties": { + "affected_occurrence_ref": { + "minLength": 1, + "title": "Affected Occurrence Ref", + "type": "string" + }, + "affected_revision": { + "minimum": 1, + "title": "Affected Revision", + "type": "integer" + }, + "affected_target_kind": { + "enum": [ + "action", + "control", + "attempt" + ], + "title": "Affected Target Kind", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" + }, + "controlled_scope_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Controlled Scope Refs", + "type": "array" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", + "type": "string" + }, + "declaration_ref": { + "minLength": 1, + "title": "Declaration Ref", + "type": "string" + }, + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "intervention_ref": { + "minLength": 1, + "title": "Intervention Ref", + "type": "string" + }, + "kind": { + "const": "intervention", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitation Refs", + "type": "array" + }, + "mixed_control_policy_ref": { + "minLength": 1, + "title": "Mixed Control Policy Ref", "type": "string" }, - "previous_episode_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Previous Episode Id" - }, - "sequence_number": { - "title": "Sequence Number", + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", "type": "integer" }, - "state_schema_version": { - "const": "participant-episode-state/v1", - "default": "participant-episode-state/v1", - "title": "State Schema Version", - "type": "string" - }, - "status": { - "title": "Status", + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", "type": "string" }, - "terminal_reason": { + "reason_code": { "anyOf": [ { + "minLength": 1, "type": "string" }, { @@ -1362,11 +2800,12 @@ } ], "default": null, - "title": "Terminal Reason" + "title": "Reason Code" }, - "terminated_at": { + "reason_ref": { "anyOf": [ { + "minLength": 1, "type": "string" }, { @@ -1374,55 +2813,44 @@ } ], "default": null, - "title": "Terminated At" + "title": "Reason Ref" }, - "updated_at": { - "title": "Updated At", - "type": "string" + "valid_from_order": { + "minimum": 0, + "title": "Valid From Order", + "type": "integer" + }, + "valid_until_order": { + "minimum": 0, + "title": "Valid Until Order", + "type": "integer" } }, "required": [ - "participant_address", - "episode_id", - "sequence_number", - "status", - "initialized_at", - "updated_at", - "last_control_action" + "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", + "affected_target_kind", + "affected_occurrence_ref", + "affected_revision", + "intervention_ref" ], - "title": "ParticipantEpisodeStateModel", + "title": "ParticipantInterventionOccurrenceModel", "type": "object" }, - "ParticipantFailureClass": { - "description": "SEM-211 portable failure classes for participant action attempts.", - "enum": [ - "precondition_unsatisfied", - "unsupported_action", - "target_unavailable", - "authority_denied", - "resource_exhausted", - "timeout", - "interrupted", - "contention_lost", - "partial_success", - "unsafe_withheld", - "backend_error", - "unknown" - ], - "title": "ParticipantFailureClass", - "type": "string" - }, - "ParticipantInteractionClass": { - "description": "SEM-209 interaction classes for multi-participant behavior.", - "enum": [ - "coordination", - "contention", - "interference", - "shared_state_change" - ], - "title": "ParticipantInteractionClass", - "type": "string" - }, "ParticipantJointActionAccessSetModel": { "additionalProperties": false, "description": "Read/write footprint for one member event in a joint action record.", @@ -2159,63 +3587,360 @@ "title": "Evidence Refs", "type": "array" }, - "observed_value": { + "observed_value": { + "minLength": 1, + "title": "Observed Value", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + }, + "source_id": { + "minLength": 1, + "title": "Source Id", + "type": "string" + }, + "source_layer": { + "$ref": "#/$defs/OutcomeInterpretationSourceLayer" + } + }, + "required": [ + "source_id", + "source_layer", + "ref", + "observed_value" + ], + "title": "ParticipantOutcomeSourceRecordModel", + "type": "object" + }, + "ParticipantOutcomeTargetRecordModel": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Diagnostics", + "type": "array" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "governance_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Governance Ref" + }, + "interpreted_value": { + "minLength": 1, + "title": "Interpreted Value", + "type": "string" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "target_layer": { + "$ref": "#/$defs/OutcomeInterpretationTargetLayer" + } + }, + "required": [ + "target_id", + "target_layer", + "ref", + "interpreted_value", + "evidence_refs", + "limitations" + ], + "title": "ParticipantOutcomeTargetRecordModel", + "type": "object" + }, + "ParticipantOverrideOccurrenceModel": { + "additionalProperties": false, + "description": "An override that supersedes rather than rewrites an earlier occurrence.", + "properties": { + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" + }, + "controlled_scope_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Controlled Scope Refs", + "type": "array" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", + "type": "string" + }, + "declaration_ref": { + "minLength": 1, + "title": "Declaration Ref", + "type": "string" + }, + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "kind": { + "const": "override", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitation Refs", + "type": "array" + }, + "mixed_control_policy_ref": { + "minLength": 1, + "title": "Mixed Control Policy Ref", + "type": "string" + }, + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", + "type": "integer" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "reason_code": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Code" + }, + "reason_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Ref" + }, + "replacement_ref": { "minLength": 1, - "title": "Observed Value", + "title": "Replacement Ref", "type": "string" }, - "provenance_refs": { - "items": { - "minLength": 1, - "type": "string" - }, - "title": "Provenance Refs", - "type": "array" - }, - "ref": { + "superseded_occurrence_ref": { "minLength": 1, - "title": "Ref", + "title": "Superseded Occurrence Ref", "type": "string" }, - "source_id": { - "minLength": 1, - "title": "Source Id", + "superseded_revision": { + "minimum": 1, + "title": "Superseded Revision", + "type": "integer" + }, + "superseded_target_kind": { + "enum": [ + "control", + "decision" + ], + "title": "Superseded Target Kind", "type": "string" }, - "source_layer": { - "$ref": "#/$defs/OutcomeInterpretationSourceLayer" + "valid_from_order": { + "minimum": 0, + "title": "Valid From Order", + "type": "integer" + }, + "valid_until_order": { + "minimum": 0, + "title": "Valid Until Order", + "type": "integer" } }, "required": [ - "source_id", - "source_layer", - "ref", - "observed_value" + "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", + "superseded_target_kind", + "superseded_occurrence_ref", + "superseded_revision", + "replacement_ref" ], - "title": "ParticipantOutcomeSourceRecordModel", + "title": "ParticipantOverrideOccurrenceModel", "type": "object" }, - "ParticipantOutcomeTargetRecordModel": { + "ParticipantPhaseRealization": { + "description": "RUN-306 realization modes for an observable lifecycle phase.", + "enum": [ + "observed", + "runtime_mediated", + "externally_supplied", + "opaque", + "unknown", + "not_applicable", + "unsupported" + ], + "title": "ParticipantPhaseRealization", + "type": "string" + }, + "ParticipantPreconditionClass": { + "description": "SEM-211 precondition classes for participant action applicability.", + "enum": [ + "authority", + "capability", + "target", + "knowledge", + "resource", + "temporal", + "interaction", + "realization" + ], + "title": "ParticipantPreconditionClass", + "type": "string" + }, + "ParticipantProposalOccurrenceModel": { "additionalProperties": false, + "description": "A proposal fact that is neither a decision, admission, nor execution.", "properties": { - "diagnostics": { + "action_contract_ref": { + "minLength": 1, + "title": "Action Contract Ref", + "type": "string" + }, + "admission_status": { + "const": "not-admitted", + "title": "Admission Status", + "type": "string" + }, + "authority_basis_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Diagnostics", + "minItems": 1, + "title": "Authority Basis Refs", "type": "array" }, - "evidence_refs": { + "behavior_specification_ref": { + "minLength": 1, + "title": "Behavior Specification Ref", + "type": "string" + }, + "controlled_scope_refs": { "items": { "minLength": 1, "type": "string" }, "minItems": 1, - "title": "Evidence Refs", + "title": "Controlled Scope Refs", "type": "array" }, - "governance_ref": { + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "controller_state_ref": { + "minLength": 1, + "title": "Controller State Ref", + "type": "string" + }, + "decision_surface_ref": { "anyOf": [ { "minLength": 1, @@ -2226,76 +3951,206 @@ } ], "default": null, - "title": "Governance Ref" + "title": "Decision Surface Ref" }, - "interpreted_value": { + "declaration_ref": { "minLength": 1, - "title": "Interpreted Value", + "title": "Declaration Ref", "type": "string" }, - "limitations": { + "disposition": { + "$ref": "#/$defs/ParticipantControlDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "expected_state_revision": { + "minimum": 0, + "title": "Expected State Revision", + "type": "integer" + }, + "kind": { + "const": "proposal", + "title": "Kind", + "type": "string" + }, + "limitation_refs": { "items": { "minLength": 1, "type": "string" }, "minItems": 1, - "title": "Limitations", + "title": "Limitation Refs", "type": "array" }, - "ref": { + "mixed_control_policy_ref": { "minLength": 1, - "title": "Ref", + "title": "Mixed Control Policy Ref", "type": "string" }, - "target_id": { + "occurrence_revision": { + "minimum": 1, + "title": "Occurrence Revision", + "type": "integer" + }, + "payload_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": "Payload Digest" + }, + "payload_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Payload Ref" + }, + "policy_revision": { "minLength": 1, - "title": "Target Id", + "title": "Policy Revision", "type": "string" }, - "target_layer": { - "$ref": "#/$defs/OutcomeInterpretationTargetLayer" + "proposal_binding_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Proposal Binding Ref" + }, + "proposal_id": { + "minLength": 1, + "title": "Proposal Id", + "type": "string" + }, + "proposal_revision": { + "minimum": 1, + "title": "Proposal Revision", + "type": "integer" + }, + "reason_code": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Code" + }, + "reason_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason Ref" + }, + "source_proposal_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Proposal Ref" + }, + "source_proposal_revision": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Proposal Revision" + }, + "transformation_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transformation Ref" + }, + "valid_from_order": { + "minimum": 0, + "title": "Valid From Order", + "type": "integer" + }, + "valid_until_order": { + "minimum": 0, + "title": "Valid Until Order", + "type": "integer" } }, "required": [ - "target_id", - "target_layer", - "ref", - "interpreted_value", - "evidence_refs", - "limitations" + "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", + "proposal_id", + "proposal_revision", + "admission_status", + "action_contract_ref" ], - "title": "ParticipantOutcomeTargetRecordModel", + "title": "ParticipantProposalOccurrenceModel", "type": "object" }, - "ParticipantPhaseRealization": { - "description": "RUN-306 realization modes for an observable lifecycle phase.", - "enum": [ - "observed", - "runtime_mediated", - "externally_supplied", - "opaque", - "unknown", - "not_applicable", - "unsupported" - ], - "title": "ParticipantPhaseRealization", - "type": "string" - }, - "ParticipantPreconditionClass": { - "description": "SEM-211 precondition classes for participant action applicability.", - "enum": [ - "authority", - "capability", - "target", - "knowledge", - "resource", - "temporal", - "interaction", - "realization" - ], - "title": "ParticipantPreconditionClass", - "type": "string" - }, "ParticipantRuntimeLifecyclePhase": { "description": "RUN-306 observable participant runtime lifecycle phases.", "enum": [ @@ -4940,6 +6795,16 @@ "title": "Participant Behavior History", "type": "object" }, + "participant_control_history": { + "additionalProperties": { + "items": { + "$ref": "#/$defs/ParticipantControlOccurrenceModel" + }, + "type": "array" + }, + "title": "Participant Control History", + "type": "object" + }, "participant_episode_history": { "additionalProperties": { "items": { @@ -5025,5 +6890,14 @@ } }, "title": "RuntimeSnapshotEnvelopeModel", - "type": "object" + "type": "object", + "x-aces-semantic-profile": { + "contract_id": "runtime-snapshot-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } } diff --git a/docs/decisions/issue-255-run-310-supervisory-lifecycle-preflight.md b/docs/decisions/issue-255-run-310-supervisory-lifecycle-preflight.md new file mode 100644 index 000000000..2410b2652 --- /dev/null +++ b/docs/decisions/issue-255-run-310-supervisory-lifecycle-preflight.md @@ -0,0 +1,380 @@ +# Issue #255 — RUN-310 Supervisory Lifecycle Preflight + +Date: 2026-07-26 + +Issue: #255. + +Requirement: RUN-310. + +This note records architecture boundaries and implementation guardrails for the +observable mixed-control supervisory lifecycle. It is guidance only. It does +not add a route, runtime transition, store field, contract, schema, capability, +backend behavior, or conformance claim. + +## Binding Authorities + +- Accepted ADR-085 and + `specs/formal/participant-semantics/information-flow-control.md` require + authenticated caller, target authorization, participant/controller + authority, action admission, and visibility to remain separate deny-first + gates. They also require policy, controller, authority, marking, and order + revisions to be evaluated at the occurrence's declared order point. +- ACT-617 is already authored and compiled through + `ParticipantBehaviorSpecification.mixed_control` and + `ParticipantBehaviorSpecificationRuntime.controller_states` / + `control_transitions`. Those are permitted policy declarations, not live + state or evidence that a transition occurred. +- API-409 already publishes `ParticipantControlOccurrenceModel` and + `validate_participant_control_occurrence_context()`. An API-409 record is an + immutable occurrence fact and outcome, not an untrusted command DTO. +- ADR-054 owns the append-only participant runtime lifecycle, ordering, + markings, projections, and evidence boundary. Admission, execution, + delivery, observation, and control remain distinct state owners. +- ADR-009, ADR-019, and ADR-061 govern closed contract models, published + schemas, generated-bundle parity, and compatibility classification. +- `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, and + `tools/verify_all.py` own repository workflow. RUN-310 needs no issue-local + runner, registry, schema generator, or verification script. + +These authorities already settle the cross-cutting architecture. RUN-310 does +not need a new ADR unless implementation discovers a conflict with an accepted +authority. This note specializes them at the live mediation boundary. + +## Architecture Decisions And Boundaries + +### The runtime mediates commands and emits occurrence facts + +The HTTP/application input and the persisted API-409 occurrence have different +trust and ownership: + +- a request carries only caller-supplied intent, stable client correlation, + the target participant/episode/proposal or typed target, expected + controller-state and policy revisions, and the references needed to evaluate + it; +- the runtime owns event identity, recorded/ingested time, realized + disposition and reason code, resulting controller-state revision, + authorization and evidence bindings, and persistence status; and +- the resulting immutable fact is + `ParticipantControlOccurrenceModel`, validated by the existing API-409 + contextual validator before commit. + +Do not accept a caller-supplied API-409 record as already accepted, use its +`actor_ref` as authentication, or let it choose its realized disposition. +Conversely, do not publish a second portable supervisory occurrence schema. +If the HTTP adapter needs a request body, keep it as a closed, bounded +application DTO containing only client-owned fields; share API-409 component +types and vocabularies rather than duplicating its occurrence model. + +Once a caller is authenticated and bound, stale, denied, late, conflicting, +limited, superseded, and too-late attempts are observable outcomes and must +append a bounded API-409 occurrence. Requests that fail before authenticated +subject binding are security-audit denials only: untrusted input cannot create +a participant occurrence. + +### One runtime transition owner over compiled policy and recorded history + +Extend the existing `RuntimeControlPlane` / `ParticipantControlMixin` boundary; +do not add a workflow engine, participant gateway, backend method, or parallel +controller service. The mediator consumes an admitted compiled +`ParticipantBehaviorSpecificationRuntime` supplied by trusted runtime setup, +not a declaration sent in the request. It indexes the existing controller +states and transitions into +`ParticipantControlDeclarationModel` inputs for the canonical API-409 +validator. + +The transition evaluation order is: + +1. bind the authenticated control-plane identity and exact runtime target; +2. bind the path/body target to one participant and episode; +3. bind that principal to the requested participant/controller subject; +4. resolve the exact compiled ACT-617 transition, controller, authority basis, + scope, policy revision, validity interval, and expected state revision; +5. read and validate the current append-only control history; +6. evaluate duplicate, stale, revoked, late, ordered-concurrent, and conflict + disposition rules; +7. when the occurrence would lead to an action, pass the separately bound + proposal/action through the existing SEM-211 + `ParticipantActionAdmissionRequest` path; and +8. append the validated occurrence and its operation receipt atomically, then + project only the authorized history/view. + +Unknown or unresolved required coordinates fail closed. `behavior_mode`, an +operator role, bearer-token possession, implementation identity, backend +support, request arrival time, and collection order never substitute for +controller state or participant authority. + +The current implementation may support ACT-617's +`total-effective-order` only. It must reject an unsupported order strategy +rather than linearize it silently. + +### Control history is first-class snapshot state + +The authoritative live state is an append-only +`participant_control_history`, keyed by participant in the same manner as +episode and behavior histories, with episode identity retained in every +API-409 occurrence. Current controller state and revision are a deterministic +fold of the admitted compiled initial state plus that history. Do not maintain +a second mutable controller-state truth. A derived in-memory index is +permitted only when it is rebuilt and checked against the authoritative +history after restart. + +Integrate the new history into all existing snapshot carriers and validators: + +- `RuntimeSnapshot`, `RuntimeSnapshot.with_entries()`, and its allowed update + keys; +- `RuntimeSnapshotEnvelopeModel`, `_snapshot_payload()`, + `_snapshot_from_payload()`, and `_snapshot_model()`; +- `participant_runtime_state_contract_diagnostics()` for full-snapshot + validity; and +- `participant_runtime_history_transition_diagnostics()` for append-only + prefix preservation and valid head transitions. + +Do not use `RuntimeSnapshot.metadata`, participant behavior `details`, +`AuditEvent.details`, operation diagnostics, or a gateway-local file as +controller state or occurrence history. + +API-408 history/status projections may expose control occurrences only through +an explicitly governed typed projection with participant/episode scope, +visibility, markings, redaction policy, completeness, and source-snapshot +binding. An auditor/operator read role permits control-plane retrieval; it +does not make the same event participant-visible. Participant egress still +passes SEM-226 exposure and ADR-085 visibility/marking gates. + +Any change to `runtime-snapshot-v1`, `participant-history-view-v1`, or another +published schema must update its owning `ContractModel`, all serializers, +fixtures, `schema_bundle()`, publication entry/manifest hash, and ADR-061 +compatibility evidence together. The published schema remains authority; +neither a Python-only field nor a hand-edited generated schema is sufficient. + +### Commit transition, receipt, and idempotency outcome atomically + +`ControlPlaneStore`, `InMemoryControlPlaneStore`, and +`LocalControlPlaneStore` remain the persistence owners. RUN-310 must extend +that boundary with one atomic control-transition commit that checks the +expected history head/revision and durably records: + +- the appended API-409 occurrence; +- the corresponding `ControlPlaneOperationRecord`; +- its scoped idempotency key and semantic request fingerprint; and +- the safe audit correlation needed to prove who requested the operation. + +Calling the existing `save_snapshot()` and `save_record()` independently is +not sufficient: a crash between them can reapply a transition after restart +or return no receipt for an already-committed event. `LocalControlPlaneStore`'s +atomic file replacement is an incumbent building block, not a transaction +across those separate files. + +The in-process mediator must serialize compare/validate/append/commit for one +participant/episode/controller history. If the first realization supports only +one control-plane writer, declare and test that bound. File replacement alone +does not provide multi-process compare-and-swap. A later distributed store must +implement the same expected-head atomic commit seam; RUN-310 must not claim +distributed linearizability without that evidence. + +Audit remains an append-only security/operations trail and carries safe ids, +operation ids, dispositions, and reason codes only. The API-409 occurrence and +its evidence/provenance references are the portable supervisory fact. An audit +event is not a substitute for lifecycle evidence or participant visibility. + +### Idempotency is scoped and state-bound + +Reuse the existing `Idempotency-Key`, request-fingerprint, +`ControlPlaneOperationRecord`, and store lookup pattern, with RUN-310's stronger +scope. The effective lookup scope is: + +```text +(target, authenticated principal, operation kind, + participant, episode, client idempotency key) +``` + +The semantic fingerprint covers the canonical parsed request plus the bound +declaration, controller, authority/scope, policy revision, expected state +revision, typed target revision, and order coordinates. Raw JSON byte equality +alone is insufficient, and the authenticated identity must not be omitted. + +An exact retry of an already committed occurrence returns the original receipt +and never appends or applies again. Reusing a key, client correlation, proposal +identity, decision identity, or event identity with different semantics is an +explicit conflict. A request whose policy, target, proposal, or expected state +revision no longer matches is stale and performs no state change. Exact +payload equality never collapses distinct occurrence identities. + +### Admission, execution, cancellation, and observation remain separate + +- A proposal is not selected, approved, admitted, attempted, or executed. +- Approval or external direction targets one proposal/revision but does not + call a backend. A mixed-control action must carry a stable reference to the + accepted control occurrence into the existing action-admission binding; the + admission validator must reject a missing, stale, cross-participant, or + policy-mismatched control basis. +- Denial records a decision and leaves action admission closed. +- Intervention and override append a new fact. They never edit a proposal, + approval, admission event, attempt, result, or observation. +- Cancellation resolves the typed target's actual stage. Before admission it + may prevent work; after admission or attempt it records partial limitation + or too-late effect. It cannot manufacture retroactive non-occurrence. +- Handoff completion advances controller state by exactly one declared + revision and preserves participant identity and all prior provenance. +- Execution remains in participant lifecycle/behavior history; observation + remains in participant observation/exposure carriers. Stable references and + predecessor/order relations connect them to control history. + +## Canonical Incumbents To Reuse + +| Concern | Canonical incumbent and required use | +| --- | --- | +| Authored/compiled policy | `MixedControlParticipantOperation`, `MixedControlControllerState`, `MixedControlTransition`, `ParticipantBehaviorSpecificationRuntime.controller_states` and `.control_transitions` | Treat these as trusted permitted-policy declarations; do not recreate or accept them from the caller. | +| Portable occurrence | `ParticipantControlOccurrenceModel`, its closed variants/vocabularies, `ParticipantRuntimeBaseEnvelopeModel`, and `validate_participant_control_occurrence_context()` | Emit one validated immutable fact per bound attempt; do not add a generic event/details bag or duplicate occurrence schema. | +| Runtime owner | `RuntimeControlPlane`, `ParticipantControlMixin`, `OperationReceipt`, `OperationStatus`, and `ControlPlaneOperationRecord` | Add mediation at the existing participant control-plane boundary and retain the common operation lifecycle. | +| Action admission | `ParticipantActionAdmissionRequest`, `participant_action_admission_request_violations()`, decision-surface binding, and `admit_participant_action()` | Link accepted control to admission; never dispatch directly from approval/direction. | +| Lifecycle/observation | `ParticipantLifecycleEventModel`, `ParticipantBehaviorHistoryEventModel`, `ParticipantActionResultModel`, `ParticipantObservationEnvelopeModel`, and API-408 projections | Link by stable refs and order; do not copy their state machines into control history. | +| State and validation | `RuntimeSnapshot`, `RuntimeSnapshotEnvelopeModel`, participant snapshot/transition invariant helpers, and backend contract diagnostics | Add a first-class typed append-only history and validate it on load, transition, backend result, serialization, and replay. | +| Persistence | `ControlPlaneStore`, `InMemoryControlPlaneStore`, `LocalControlPlaneStore`, atomic replacement, and existing record serialization | Extend this boundary with atomic expected-head occurrence/receipt commit; add no participant-control store. | +| Authentication | `create_control_plane_app()`, `ControlPlaneSecurityConfig.strict_defaults()`, `_ControlPlaneApiAuth`, `ControlPlaneIdentity`, `ControlPlaneRole`, bearer/verified-proxy identity, and target binding | Reuse auth and request guards, then perform a separate principal-to-participant/controller binding. | +| Request protection | `request_size_guard_response()`, closed Pydantic DTOs, `_request_fingerprint()` pattern, and `Idempotency-Key` | Bound size/shape before semantic work; use a canonical, identity- and state-scoped semantic fingerprint. | +| Diagnostics/errors | `Diagnostic`, `Severity`, operation receipts/statuses, bounded `HTTPException` details, and the redacted FastAPI exception handler | Return stable codes and safe identifiers only; add no RUN-310 exception hierarchy. | +| Audit/observability | `AuditEvent`, `operational_apparatus_summary()`, participant histories, evidence/provenance/marking refs | Correlate safe identities and outcomes without logging bodies, secrets, policies, or evidence content. | +| Contract governance | `ContractModel(extra="forbid")`, `schema_bundle()`, `contracts/schema-publication-manifest.json`, publication entries/fixtures, and schema compatibility checks | Keep model/schema/serializer parity and classify every published change. | +| Workflow | `.ground-control.yaml`, `.gc/plan-rules.md`, canonical `nox` sessions, repo policy, requirement governance, schema/publication, JSON, semantic coverage, and full verification checks | Extend the existing workflow only. | + +## Cross-Cutting Layers And Security Posture + +The intended design must pass these layers in order: + +1. **HTTP size and closed-shape gate.** The existing middleware bounds content + length and actual body bytes before parsing. A closed request DTO rejects + unknown keys, inline credentials, raw policy bodies, hidden payloads, + free-form metadata, and caller-owned result fields. +2. **Caller authentication and target gate.** Bearer or verified trusted-proxy + identity passes `_ControlPlaneApiAuth`, role authorization, and exact + `ControlPlaneIdentity.target_name` binding. Strict defaults remain empty and + fail closed. +3. **Participant/controller subject gate.** Closed subject-binding fields + supplied with the trusted `ControlPlaneIdentity` configuration map the + authenticated principal to permitted participant/controller subject refs. + The mediator checks those fields as a separate gate after role + authorization. This is operational authorization, not controller state; + role membership alone is insufficient. +4. **Compiled policy gate.** The mediator resolves the request against the + trusted compiled ACT-617 declaration and verifies participant/episode, + controller, authority basis, non-widening scope, policy revision, validity, + expected state revision, order, typed target, evidence, and provenance. +5. **Admission and visibility gates.** Actionable outcomes still pass SEM-211 + action admission. Returned or participant-facing facts separately pass + audience, visibility, marking, redaction, and completeness projection. +6. **Persistence and replay gate.** Snapshot models, API-409 contextual + validation, append-only transition validators, expected-head atomic commit, + restart load validation, and deterministic replay must agree before state is + exposed. +7. **Diagnostic/error gate.** Expected denials use bounded status/disposition + codes and value-safe diagnostics. Do not expose `ValidationError` input, + `str(exc)` from untrusted shape/authorization failures, stack traces, + controller inventories, policy content, or cross-participant existence. + Unexpected failures retain exactly `{"detail":"internal server error"}`. +8. **Audit and secret gate.** Audit stores safe ids, decisions, reason codes, + and references—not bearer tokens, headers, action bodies, rejected input, + policy material, prompts, evidence bodies, or backend objects. +9. **Configuration/OS/process gate.** RUN-310 needs no environment variable, + secret loader, CLI flag, subprocess, shell, socket, filesystem path, or + process-argument surface. Security and subject bindings remain injected + configuration. Do not put a token, policy, proposal, or participant payload + in environment variables, argv, filenames, stdout, or stderr. Any later + deployment adapter must use its existing secret/config mechanism and must + not add `shell=True` or caller-derived argv. + +## Extensibility Seam + +Keep three explicit seams: + +- the order strategy plus effective order, predecessor refs, and expected + state revision, so a later causal/partial-order implementation can be added + without changing existing occurrence meaning; +- the authenticated-principal-to-controller/participant binding carried + alongside `ControlPlaneIdentity`, so a future non-participant controller is + introduced as a closed subject-binding variant rather than an arbitrary + string or role; and +- the store's expected-head atomic append/receipt operation, so a later + multi-writer store can provide compare-and-swap without replacing the + lifecycle engine or history contract. + +These are parameters and injected dependencies of the existing runtime +boundary, not new policy engines or portable schema families. + +## Required Assurance Guardrails + +Evidence must exercise existing test families and invariant paths, including: + +- approval, denial, direction, intervention, handoff, override, cancellation, + exact retry, conflicting reuse, stale proposal/policy/state, revoked/late + authority, cross-participant targets, and unordered/unsupported order; +- restart and replay from the same append-only history, truncated/corrupt + store rejection, atomic-commit failure, duplicate recovery, and history + prefix preservation; +- unauthenticated, wrong-role, wrong-target, unbound subject, authority + widening, hidden-target probing, oversized body, and error-leakage cases; +- the separation of approval from admission, cancellation before/after + admission and attempt, and observation/visibility from audit retention; and +- published schema/model/fixture parity, snapshot/store round trips, API-408 + projection scoping, contract compatibility, repo policy, requirement + governance, and full verification. + +Finite behavioral tests are implementation evidence for RUN-310. They are not +a backend support claim, distributed-order proof, noninterference proof, +refinement, simulation, or bisimulation result. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating `behavior_mode`, authenticated caller, operator/auditor/backend + role, token, OS account, backend process, participant implementation, or + actor/producer field as controller state or participant authority; +- accepting a caller-completed API-409 fact, trusting a requested disposition, + or using an authored transition identity as an occurrence identity; +- a generic supervisory event, nullable command/result union, free-form + `details`/`metadata` map, duplicate control-kind enum, or second validation + stack; +- reading declarations, authority, policy revision, or conflict rules from the + request, environment, snapshot metadata, backend response, or current wall + clock; +- last-writer-wins, arrival-time/list-order semantics, silently linearizing a + partial order, or returning a cached idempotent result across a different + identity/target/policy/controller/state binding; +- snapshot plus receipt writes with a crash window, relying on file rename as + multi-process locking, or mutating/replacing an earlier occurrence; +- calling a backend directly from approval/direction, carrying approval across + proposal transformation, or equating admission with execution; +- retroactive cancellation, deletion on revocation/concealment, or rewriting + prior controller, action, observation, evidence, or provenance history; +- returning control history without visibility/marking projection or treating + audit retention as participant disclosure; +- raw credentials, headers, tokens, prompts, action payloads, policy bodies, + hidden content, rejected records, evidence bodies, backend objects, or + exception text in contracts, snapshots, diagnostics, logs, audits, fixtures, + environment, or argv; +- adding a new gateway, workflow engine, store, audit channel, logger, + exception hierarchy, schema registry, compatibility process, or verification + script; and +- updating the lineage ledger/source audit merely because RUN-310 delivery + status changes. + +## Non-Goals And Implementation Boundaries + +- No new SDL syntax, authored controller semantics, API-409 occurrence family, + information-flow relation, participant gateway, human-control UI, identity + provider, event bus, workflow engine, or persistence product. +- No backend-specific supervisory method, capability declaration, universal + backend support claim, or automatic weakening when support is absent. +- No replacement of SEM-211 admission, participant lifecycle/action results, + SEM-226 exposure, API-408 retrieval, or evidence/provenance ownership. +- No participant-internal reasoning, prompt/answer capture, chain-of-thought, + hidden world state, or policy body in the supervisory record. +- No distributed ordering or multi-writer guarantee unless the selected store + proves the expected-head atomic commit contract. +- No retroactive erasure and no claim that approval proves admission, + admission proves execution, delivery proves observation, or audit proves + participant visibility. +- The implementation must update the participant section of + `docs/explain/sdl/lineage.md` with actual delivery evidence and explicit + nonclaims. `contracts/provenance/sdl-lineage-ledger-v1.json` and the source + audit change only if normative derivation or compatibility claims change. diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index 4f6139b39..fde9c64dc 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -1039,6 +1039,35 @@ which dynamic queue/log/config details remain evidence or bounded settings. information-flow proof, and API-423 crossing-policy realization are explicit nonclaims. The lineage ledger and source audit remain unchanged because this delivery adds no normative external derivation or compatibility claim. +- RUN-310 composes the same participant-interface, append-only event-history, + mixed-control, information-flow, and access-control lineage into live + supervisory mediation; it introduces no new external semantic source. The + exact RAES/ACES SDL mapping is trusted + `ParticipantBehaviorSpecificationRuntime.controller_states` and + `control_transitions` for policy authority, closed + `Participant*ControlIntent` models for caller-owned intent, + `ControlPlaneIdentity.participant_control_subjects` for the separate + principal-to-participant/controller binding, `RuntimeControlPlane` and + `ParticipantControlMixin` for mediation, API-409 + `ParticipantControlOccurrenceModel` for immutable outcomes, + `RuntimeSnapshot.participant_control_history` for append-only state, and + `ControlPlaneStore.commit_control_transition()` for the expected-head atomic + occurrence/receipt/idempotency/audit commit. The existing HTTP request-size, + authentication, role/target, redacted-error, SEM-211 admission, and API-408 + visibility boundaries remain separate gates. +- RUN-310 delivery evidence is the `runtime-snapshot-v1` schema and publication + entry, the in-memory and local-store restart/replay implementation, and + `implementations/python/tests/test_run_310_supervisory_lifecycle.py`, which + covers every control kind, negative subject binding, stale state, scoped + idempotency conflict, append-only integrity, atomic failure, restart, closed + HTTP input, and denial without participant occurrence. This does not claim + that approval proves admission, execution, delivery, observation, or + participant visibility; it does not rewrite prior action or controller + history; and it makes no backend-support, multi-process CAS, distributed or + partial-order, noninterference, refinement, simulation, bisimulation, UI, or + participant-internal-reasoning claim. The lineage ledger and source audit + remain unchanged because RUN-310 adds no normative derivation or + compatibility claim. - CALDERA adversary-emulation research informs the action semantics: cyber actions can change foothold, knowledge, observations, detection surface, and downstream outcomes under uncertainty. diff --git a/implementations/python/packages/raes_contracts/contracts/participant_control_validation.py b/implementations/python/packages/raes_contracts/contracts/participant_control_validation.py index a6ec9ea8a..bacca141a 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_control_validation.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_control_validation.py @@ -8,6 +8,7 @@ ParticipantApprovalOccurrenceModel, ParticipantCancellationOccurrenceModel, ParticipantControlDeclarationModel, + ParticipantControlDisposition, ParticipantControlOccurrenceModel, ParticipantControlTargetContextModel, ParticipantControlTargetKind, @@ -20,6 +21,7 @@ ) TargetIndex = dict[tuple[ParticipantControlTargetKind, str], ParticipantControlTargetContextModel] +DeclarationIndex = dict[tuple[str, str, str], ParticipantControlDeclarationModel] def validate_participant_control_occurrence_context( @@ -43,10 +45,15 @@ def validate_participant_control_occurrence_context( def _index_declarations( declarations: Sequence[ParticipantControlDeclarationModel], -) -> dict[str, ParticipantControlDeclarationModel]: - declarations_by_ref: dict[str, ParticipantControlDeclarationModel] = {} +) -> DeclarationIndex: + declarations_by_ref: DeclarationIndex = {} for declaration in declarations: - existing = declarations_by_ref.setdefault(declaration.declaration_ref, declaration) + key = ( + declaration.declaration_ref, + declaration.participant_address, + declaration.episode_id, + ) + existing = declarations_by_ref.setdefault(key, declaration) if existing != declaration: raise ValueError("declaration identity was reused with different semantics") return declarations_by_ref @@ -86,6 +93,8 @@ def _register_record_targets( target_contexts: TargetIndex, ) -> None: occurrence = record.occurrence + if occurrence.disposition is ParticipantControlDisposition.REJECTED: + return _register_target( target_contexts, ParticipantControlTargetContextModel( @@ -125,16 +134,24 @@ def _register_record_targets( def _validate_record( record: ParticipantControlOccurrenceModel, *, - declarations_by_ref: dict[str, ParticipantControlDeclarationModel], + declarations_by_ref: DeclarationIndex, proposal_records: dict[str, ParticipantControlOccurrenceModel], target_contexts: TargetIndex, ) -> None: occurrence = record.occurrence - declaration = declarations_by_ref.get(occurrence.declaration_ref) + declaration = declarations_by_ref.get( + ( + occurrence.declaration_ref, + record.participant_address, + record.episode_id, + ) + ) if declaration is None: raise ValueError("declaration reference must resolve") if not _declaration_agrees(record, declaration): raise ValueError("occurrence and declaration coordinates disagree") + if occurrence.disposition is ParticipantControlDisposition.REJECTED: + return if isinstance(occurrence, ParticipantProposalOccurrenceModel): _validate_transformed_proposal(record, proposal_records) elif isinstance(occurrence, (ParticipantApprovalOccurrenceModel, ParticipantDenialOccurrenceModel)): diff --git a/implementations/python/packages/raes_contracts/contracts/realization_plans.py b/implementations/python/packages/raes_contracts/contracts/realization_plans.py index ebf6a1676..3ffe03584 100644 --- a/implementations/python/packages/raes_contracts/contracts/realization_plans.py +++ b/implementations/python/packages/raes_contracts/contracts/realization_plans.py @@ -18,6 +18,7 @@ WorkflowExecutionStateModel, WorkflowHistoryEventModel, ) +from .participant_control import ParticipantControlOccurrenceModel from .participant_envelopes import ( ParticipantJointActionRecordModel, ParticipantSharedStateRecordModel, @@ -165,6 +166,7 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): participant_episode_results: dict[str, ParticipantEpisodeStateModel] = Field(default_factory=dict) participant_episode_history: dict[str, list[ParticipantEpisodeHistoryEventModel]] = Field(default_factory=dict) participant_behavior_history: dict[str, list[ParticipantBehaviorHistoryEventModel]] = Field(default_factory=dict) + participant_control_history: dict[str, list[ParticipantControlOccurrenceModel]] = Field(default_factory=dict) participant_autonomous_execution_states: dict[str, ParticipantAutonomousExecutionStateModel] = Field( default_factory=dict ) diff --git a/implementations/python/packages/raes_contracts/participant_control_history.py b/implementations/python/packages/raes_contracts/participant_control_history.py new file mode 100644 index 000000000..85d5203bc --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_control_history.py @@ -0,0 +1,70 @@ +"""RUN-310 append-only participant control-history invariants.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +from pydantic import ValidationError + +from .contracts import ParticipantControlOccurrenceModel + + +def iter_participant_control_history_snapshot_violations( + history: Mapping[str, Sequence[dict[str, object]]], +) -> list[tuple[str, str]]: + """Return value-safe violations for one complete control-history snapshot.""" + + violations: list[tuple[str, str]] = [] + seen_event_ids: set[str] = set() + for participant_address, events in history.items(): + address = f"runtime.snapshot.participant-control-history.{participant_address}" + for index, payload in enumerate(events): + try: + event = ParticipantControlOccurrenceModel.model_validate(payload) + except (TypeError, ValidationError): + violations.append((address, "participant control history event is not a valid API-409 occurrence")) + continue + if event.participant_address != participant_address: + violations.append( + ( + address, + "participant control history map key must equal the embedded participant_address", + ) + ) + expected_revision = index + 1 + if event.occurrence.occurrence_revision != expected_revision: + violations.append( + ( + address, + "participant control occurrence_revision must be contiguous and match append order", + ) + ) + if event.event_id in seen_event_ids: + violations.append((address, "participant control event identity must be globally unique")) + seen_event_ids.add(event.event_id) + return violations + + +def iter_participant_control_history_transition_violations( + previous: Mapping[str, Sequence[dict[str, object]]], + next_history: Mapping[str, Sequence[dict[str, object]]], +) -> list[tuple[str, str]]: + """Return violations when a control-history transition rewrites prior facts.""" + + violations: list[tuple[str, str]] = [] + for participant_address, previous_events in previous.items(): + next_events = next_history.get(participant_address, ()) + if list(next_events[: len(previous_events)]) != list(previous_events): + violations.append( + ( + f"runtime.snapshot.participant-control-history.{participant_address}", + "participant control history must preserve its append-only prefix", + ) + ) + return violations + + +__all__ = ( + "iter_participant_control_history_snapshot_violations", + "iter_participant_control_history_transition_violations", +) diff --git a/implementations/python/packages/raes_contracts/runtime_state.py b/implementations/python/packages/raes_contracts/runtime_state.py index 63b930289..40f3b396a 100644 --- a/implementations/python/packages/raes_contracts/runtime_state.py +++ b/implementations/python/packages/raes_contracts/runtime_state.py @@ -84,6 +84,7 @@ class RuntimeSnapshot: participant_episode_results: dict[str, dict[str, Any]] = field(default_factory=dict) participant_episode_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) 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_autonomous_execution_states: dict[str, dict[str, Any]] = field(default_factory=dict) shared_state_records: dict[str, dict[str, Any]] = field(default_factory=dict) shared_state_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) @@ -149,6 +150,11 @@ def with_entries( "participant_behavior_history", self.participant_behavior_history, ), + participant_control_history=_history_update( + updates, + "participant_control_history", + self.participant_control_history, + ), participant_autonomous_execution_states=_mapping_update( updates, "participant_autonomous_execution_states", @@ -202,6 +208,7 @@ def with_entries( "participant_episode_results", "participant_episode_history", "participant_behavior_history", + "participant_control_history", "participant_autonomous_execution_states", "shared_state_records", "shared_state_history", diff --git a/implementations/python/packages/raes_runtime/control_plane.py b/implementations/python/packages/raes_runtime/control_plane.py index 691a287b3..d4070c514 100644 --- a/implementations/python/packages/raes_runtime/control_plane.py +++ b/implementations/python/packages/raes_runtime/control_plane.py @@ -8,7 +8,9 @@ from __future__ import annotations +from collections.abc import Mapping from datetime import UTC, datetime +from threading import RLock from uuid import uuid4 from raes_backend_protocols.backend_manifest import BackendManifest @@ -41,6 +43,7 @@ WorkflowHistoryEventType, WorkflowStatus, ) +from raes_processor.models import ParticipantBehaviorSpecificationRuntime from .backend_calls import _call_backend_diagnostics from .control_plane_execution import ( @@ -209,11 +212,14 @@ def __init__( *, initial_snapshot: RuntimeSnapshot | None = None, store: ControlPlaneStore | None = None, + behavior_specifications: Mapping[str, ParticipantBehaviorSpecificationRuntime] | None = None, ) -> None: self._target = target self._store = store or InMemoryControlPlaneStore(initial_snapshot) self._snapshot = initial_snapshot if initial_snapshot is not None else self._store.load_snapshot() self._operations: dict[str, ControlPlaneOperationRecord] = self._store.load_records() + self._behavior_specifications = dict(behavior_specifications or {}) + self._participant_control_lock = RLock() @property def snapshot(self) -> RuntimeSnapshot: diff --git a/implementations/python/packages/raes_runtime/control_plane_api.py b/implementations/python/packages/raes_runtime/control_plane_api.py index a064fbcff..9102bd53d 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api.py +++ b/implementations/python/packages/raes_runtime/control_plane_api.py @@ -42,6 +42,7 @@ ControlPlaneRole, ControlPlaneSecurityConfig, ) +from .participant_control_intents import ParticipantControlIntent _CONFLICT_RESPONSES = {409: {"description": "Conflict"}} _NOT_FOUND_RESPONSES = {404: {"description": "Not found"}} @@ -181,6 +182,7 @@ def create_control_plane_app( _register_operation_routes(app, control_plane) _register_workflow_routes(app, control_plane) _register_participant_episode_routes(app, control_plane) + _register_participant_control_routes(app, control_plane) register_participant_retrieval_routes(app, control_plane) return app @@ -423,6 +425,44 @@ def _register_participant_episode_routes( _register_participant_episode_end_routes(app, control_plane) +def _register_participant_control_routes( + app: FastAPI, + control_plane: RuntimeControlPlane, +) -> None: + @app.post( + "/participants/{participant_address}/control-occurrences", + responses=_BAD_REQUEST_CONFLICT_RESPONSES, + ) + async def record_participant_control( + participant_address: str, + request: Request, + body: ParticipantControlIntent, + identity: _MutatingIdentity, + ) -> OperationReceiptModel: + try: + receipt = control_plane.record_participant_control( + participant_address, + body, + identity=identity, + idempotency_key=request.headers.get("idempotency-key", ""), + ) + except PermissionError as exc: + control_plane.record_audit( + action="record_participant_control", + identity=identity.identity, + allowed=False, + target=participant_address, + reason="forbidden-subject", + ) + raise HTTPException(status_code=403, detail="forbidden") from exc + except ValueError as exc: + raise HTTPException( + status_code=409, + detail="control intent conflicts with runtime state", + ) from exc + return _receipt_response(receipt) + + def _register_participant_episode_start_routes( app: FastAPI, control_plane: RuntimeControlPlane, 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 1d1de0a81..036c60703 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_models.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_models.py @@ -156,6 +156,7 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "participant_episode_results": dict(snapshot.participant_episode_results), "participant_episode_history": dict(snapshot.participant_episode_history), "participant_behavior_history": dict(snapshot.participant_behavior_history), + "participant_control_history": dict(snapshot.participant_control_history), "participant_autonomous_execution_states": dict(snapshot.participant_autonomous_execution_states), "shared_state_records": dict(snapshot.shared_state_records), "shared_state_history": dict(snapshot.shared_state_history), diff --git a/implementations/python/packages/raes_runtime/control_plane_security.py b/implementations/python/packages/raes_runtime/control_plane_security.py index ca5cfe195..fea491caa 100644 --- a/implementations/python/packages/raes_runtime/control_plane_security.py +++ b/implementations/python/packages/raes_runtime/control_plane_security.py @@ -14,6 +14,18 @@ class ControlPlaneRole(str, Enum): AUDITOR = "auditor" +@dataclass(frozen=True) +class ParticipantControlSubjectBinding: + """One authenticated principal-to-participant/controller binding.""" + + participant_address: str + controller_ref: str + + def __post_init__(self) -> None: + if not self.participant_address or not self.controller_ref: + raise ValueError("participant control subject binding fields must be non-empty") + + @dataclass(frozen=True) class ControlPlaneIdentity: """Authenticated control-plane principal.""" @@ -21,6 +33,7 @@ class ControlPlaneIdentity: identity: str roles: frozenset[ControlPlaneRole] = field(default_factory=frozenset) target_name: str | None = None + participant_control_subjects: tuple[ParticipantControlSubjectBinding, ...] = () @dataclass(frozen=True) diff --git a/implementations/python/packages/raes_runtime/control_plane_store.py b/implementations/python/packages/raes_runtime/control_plane_store.py index 9385836c0..be2a6e65c 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store.py +++ b/implementations/python/packages/raes_runtime/control_plane_store.py @@ -2,12 +2,8 @@ from __future__ import annotations -import json import os -import tempfile -from contextlib import suppress -from dataclasses import asdict, dataclass, field -from pathlib import Path +from dataclasses import dataclass, field from typing import Any, Protocol from raes_contracts.contracts import RealizationEnvelopeIdentityModel @@ -72,6 +68,33 @@ def append_audit(self, event: AuditEvent) -> None: ... def read_audit(self) -> list[AuditEvent]: ... + def commit_control_transition( + self, + *, + participant_address: str, + expected_head: str | None, + snapshot: RuntimeSnapshot, + record: ControlPlaneOperationRecord, + audit_event: AuditEvent, + ) -> None: ... + + +def _control_history_head(snapshot: RuntimeSnapshot, participant_address: str) -> str | None: + events = snapshot.participant_control_history.get(participant_address, ()) + 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 + + +def _require_expected_control_head( + snapshot: RuntimeSnapshot, + participant_address: str, + expected_head: str | None, +) -> None: + if _control_history_head(snapshot, participant_address) != expected_head: + raise ValueError("expected control history head does not match durable state") + def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: require_participant_autonomous_runtime_snapshot(snapshot) @@ -103,6 +126,10 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: participant_address: list(events) for participant_address, events in snapshot.participant_behavior_history.items() }, + "participant_control_history": { + participant_address: list(events) + for participant_address, events in snapshot.participant_control_history.items() + }, "participant_autonomous_execution_states": dict(snapshot.participant_autonomous_execution_states), "shared_state_records": dict(snapshot.shared_state_records), "shared_state_history": { @@ -165,6 +192,10 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: participant_address: list(events) for participant_address, events in payload.get("participant_behavior_history", {}).items() }, + participant_control_history={ + participant_address: list(events) + for participant_address, events in payload.get("participant_control_history", {}).items() + }, participant_autonomous_execution_states=dict(payload.get("participant_autonomous_execution_states", {})), shared_state_records=dict(payload.get("shared_state_records", {})), shared_state_history={ @@ -283,6 +314,19 @@ def _record_from_payload(payload: dict[str, Any]) -> ControlPlaneOperationRecord ) +def _audit_event_from_payload(payload: dict[str, Any]) -> AuditEvent: + return AuditEvent( + timestamp=str(payload.get("timestamp", "")), + action=str(payload.get("action", "")), + identity=str(payload.get("identity", "")), + allowed=bool(payload.get("allowed", False)), + target=str(payload.get("target", "")), + operation_id=str(payload.get("operation_id", "")), + reason=str(payload.get("reason", "")), + details=dict(payload.get("details", {})), + ) + + class InMemoryControlPlaneStore: """Simple in-memory store.""" @@ -322,91 +366,34 @@ def append_audit(self, event: AuditEvent) -> None: def read_audit(self) -> list[AuditEvent]: return list(self._audit) - -class LocalControlPlaneStore: - """Filesystem-backed control-plane durability.""" - - def __init__(self, base_dir: Path) -> None: - self._base_dir = base_dir - self._base_dir.mkdir(parents=True, exist_ok=True) - self._snapshot_path = self._base_dir / "snapshot.json" - self._operations_path = self._base_dir / "operations.json" - self._audit_path = self._base_dir / "audit.jsonl" - - @staticmethod - def _atomic_write(path: Path, content: str) -> None: - """Write content atomically via a temporary file and os.replace.""" - fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp") - try: - with os.fdopen(fd, "w", encoding="utf-8") as handle: - handle.write(content) - os.replace(tmp, path) - except BaseException: - with suppress(OSError): - os.unlink(tmp) - raise - - def load_snapshot(self) -> RuntimeSnapshot: - if not self._snapshot_path.exists(): - return RuntimeSnapshot() - payload = json.loads(self._snapshot_path.read_text(encoding="utf-8")) - return _snapshot_from_payload(payload) - - def save_snapshot(self, snapshot: RuntimeSnapshot) -> None: - content = json.dumps(_snapshot_payload(snapshot), indent=2, sort_keys=True) + "\n" - self._atomic_write(self._snapshot_path, content) - - def load_records(self) -> dict[str, ControlPlaneOperationRecord]: - if not self._operations_path.exists(): - return {} - payload = json.loads(self._operations_path.read_text(encoding="utf-8")) - return { - operation_id: _record_from_payload(record_payload) - for operation_id, record_payload in payload.items() - if isinstance(record_payload, dict) - } - - def save_record(self, record: ControlPlaneOperationRecord) -> None: - records = self.load_records() - records[record.receipt.operation_id] = record - payload = { - operation_id: _record_payload(operation_record) for operation_id, operation_record in records.items() - } - content = json.dumps(payload, indent=2, sort_keys=True) + "\n" - self._atomic_write(self._operations_path, content) - - def find_by_idempotency( + def commit_control_transition( self, - key: str, - ) -> ControlPlaneOperationRecord | None: - for record in self.load_records().values(): - if record.idempotency_key == key: - return record - return None + *, + participant_address: str, + expected_head: str | None, + snapshot: RuntimeSnapshot, + record: ControlPlaneOperationRecord, + audit_event: AuditEvent, + ) -> None: + _require_expected_control_head(self._snapshot, participant_address, expected_head) + require_participant_autonomous_runtime_snapshot(snapshot) + records = {**self._records, record.receipt.operation_id: record} + idempotency = dict(self._idempotency) + if record.idempotency_key: + idempotency[record.idempotency_key] = record.receipt.operation_id + self._snapshot = snapshot + self._records = records + self._idempotency = idempotency + self._audit = [*self._audit, audit_event] - def append_audit(self, event: AuditEvent) -> None: - self._audit_path.parent.mkdir(parents=True, exist_ok=True) - with self._audit_path.open("a", encoding="utf-8") as handle: - handle.write(json.dumps(asdict(event), sort_keys=True) + "\n") - def read_audit(self) -> list[AuditEvent]: - if not self._audit_path.exists(): - return [] - events: list[AuditEvent] = [] - for line in self._audit_path.read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - payload = json.loads(line) - events.append( - AuditEvent( - timestamp=str(payload.get("timestamp", "")), - action=str(payload.get("action", "")), - identity=str(payload.get("identity", "")), - allowed=bool(payload.get("allowed", False)), - target=str(payload.get("target", "")), - operation_id=str(payload.get("operation_id", "")), - reason=str(payload.get("reason", "")), - details=dict(payload.get("details", {})), - ) - ) - return events +from .control_plane_store_local import LocalControlPlaneStore # noqa: E402 + +__all__ = ( + "AuditEvent", + "ControlPlaneOperationRecord", + "ControlPlaneStore", + "InMemoryControlPlaneStore", + "LocalControlPlaneStore", + "os", +) diff --git a/implementations/python/packages/raes_runtime/control_plane_store_local.py b/implementations/python/packages/raes_runtime/control_plane_store_local.py new file mode 100644 index 000000000..1e24ee7da --- /dev/null +++ b/implementations/python/packages/raes_runtime/control_plane_store_local.py @@ -0,0 +1,167 @@ +"""Filesystem-backed runtime control-plane persistence.""" + +from __future__ import annotations + +import json +import tempfile +from contextlib import suppress +from dataclasses import asdict +from pathlib import Path +from typing import Any + +from raes_contracts.participant_autonomous_state import require_participant_autonomous_runtime_snapshot +from raes_contracts.runtime_state import RuntimeSnapshot + +from .control_plane_store import ( + AuditEvent, + ControlPlaneOperationRecord, + _audit_event_from_payload, + _record_from_payload, + _record_payload, + _require_expected_control_head, + _snapshot_from_payload, + _snapshot_payload, + os, +) + + +class LocalControlPlaneStore: + """Filesystem-backed control-plane durability.""" + + def __init__(self, base_dir: Path) -> None: + self._base_dir = base_dir + self._base_dir.mkdir(parents=True, exist_ok=True) + self._snapshot_path = self._base_dir / "snapshot.json" + self._operations_path = self._base_dir / "operations.json" + self._audit_path = self._base_dir / "audit.jsonl" + self._control_state_path = self._base_dir / "control-transition-state.json" + + @staticmethod + def _atomic_write(path: Path, content: str) -> None: + """Write content atomically via a temporary file and os.replace.""" + + fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(content) + os.replace(tmp, path) + except BaseException: + with suppress(OSError): + os.unlink(tmp) + raise + + def load_snapshot(self) -> RuntimeSnapshot: + legacy_snapshot = RuntimeSnapshot() + if self._snapshot_path.exists(): + payload = json.loads(self._snapshot_path.read_text(encoding="utf-8")) + legacy_snapshot = _snapshot_from_payload(payload) + control_state = self._load_control_state() + if control_state is None: + return legacy_snapshot + committed_snapshot = _snapshot_from_payload(dict(control_state.get("snapshot", {}))) + legacy_count = sum(len(events) for events in legacy_snapshot.participant_control_history.values()) + committed_count = sum(len(events) for events in committed_snapshot.participant_control_history.values()) + return committed_snapshot if committed_count > legacy_count else legacy_snapshot + + def save_snapshot(self, snapshot: RuntimeSnapshot) -> None: + content = json.dumps(_snapshot_payload(snapshot), indent=2, sort_keys=True) + "\n" + self._atomic_write(self._snapshot_path, content) + + def load_records(self) -> dict[str, ControlPlaneOperationRecord]: + records: dict[str, ControlPlaneOperationRecord] = {} + control_state = self._load_control_state() + if control_state is not None: + records.update( + { + operation_id: _record_from_payload(record_payload) + for operation_id, record_payload in dict(control_state.get("records", {})).items() + if isinstance(record_payload, dict) + } + ) + if not self._operations_path.exists(): + return records + payload = json.loads(self._operations_path.read_text(encoding="utf-8")) + records.update( + { + operation_id: _record_from_payload(record_payload) + for operation_id, record_payload in payload.items() + if isinstance(record_payload, dict) + } + ) + return records + + def save_record(self, record: ControlPlaneOperationRecord) -> None: + records = self.load_records() + records[record.receipt.operation_id] = record + payload = { + operation_id: _record_payload(operation_record) for operation_id, operation_record in records.items() + } + content = json.dumps(payload, indent=2, sort_keys=True) + "\n" + self._atomic_write(self._operations_path, content) + + def find_by_idempotency( + self, + key: str, + ) -> ControlPlaneOperationRecord | None: + for record in self.load_records().values(): + if record.idempotency_key == key: + return record + return None + + def append_audit(self, event: AuditEvent) -> None: + self._audit_path.parent.mkdir(parents=True, exist_ok=True) + with self._audit_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(asdict(event), sort_keys=True) + "\n") + + def read_audit(self) -> list[AuditEvent]: + events: list[AuditEvent] = [] + control_state = self._load_control_state() + if control_state is not None: + events.extend( + _audit_event_from_payload(payload) + for payload in control_state.get("audit", []) + if isinstance(payload, dict) + ) + if not self._audit_path.exists(): + return events + for line in self._audit_path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + event = _audit_event_from_payload(json.loads(line)) + if event not in events: + events.append(event) + return events + + def _load_control_state(self) -> dict[str, Any] | None: + if not self._control_state_path.exists(): + return None + payload = json.loads(self._control_state_path.read_text(encoding="utf-8")) + return payload if isinstance(payload, dict) else None + + def commit_control_transition( + self, + *, + participant_address: str, + expected_head: str | None, + snapshot: RuntimeSnapshot, + record: ControlPlaneOperationRecord, + audit_event: AuditEvent, + ) -> None: + current_snapshot = self.load_snapshot() + _require_expected_control_head(current_snapshot, participant_address, expected_head) + require_participant_autonomous_runtime_snapshot(snapshot) + records = self.load_records() + records[record.receipt.operation_id] = record + audits = [*self.read_audit(), audit_event] + payload = { + "snapshot": _snapshot_payload(snapshot), + "records": { + operation_id: _record_payload(operation_record) for operation_id, operation_record in records.items() + }, + "audit": [asdict(event) for event in audits], + } + content = json.dumps(payload, indent=2, sort_keys=True) + "\n" + self._atomic_write(self._control_state_path, content) + + +__all__ = ("LocalControlPlaneStore",) diff --git a/implementations/python/packages/raes_runtime/participant_control.py b/implementations/python/packages/raes_runtime/participant_control.py index 9c8de66eb..9da7a1938 100644 --- a/implementations/python/packages/raes_runtime/participant_control.py +++ b/implementations/python/packages/raes_runtime/participant_control.py @@ -21,6 +21,18 @@ from raes_processor.models import ParticipantBehaviorRuntime from .control_plane_execution import execute_participant_action +from .participant_control_intents import ( + ParticipantApprovalControlIntent, + ParticipantCancellationControlIntent, + ParticipantControlIntent, + ParticipantDenialControlIntent, + ParticipantExternalDirectionControlIntent, + ParticipantHandoffControlIntent, + ParticipantInterventionControlIntent, + ParticipantOverrideControlIntent, + ParticipantProposalControlIntent, +) +from .participant_control_mediation import record_participant_control _NO_PARTICIPANT_RUNTIME_MESSAGE = "Target does not provide a participant runtime." _PARTICIPANT_BINDING_REJECTED = "runtime.participant-binding.rejected" @@ -205,6 +217,24 @@ def _participant_binding_request_diagnostics( class ParticipantControlMixin: """Participant runtime methods for the shared runtime control plane.""" + def record_participant_control( + self, + participant_address: str, + intent: ParticipantControlIntent, + *, + identity: object, + idempotency_key: str = "", + ) -> OperationReceipt: + """Mediate and durably append one supervisory control occurrence.""" + + return record_participant_control( + self, + participant_address=participant_address, + intent=intent, + identity=identity, + idempotency_key=idempotency_key, + ) + def initialize_participant_episode( self, participant_address: str, @@ -404,3 +434,17 @@ def admit_participant_decision_surface_selection( idempotency_key=idempotency_key, request_fingerprint=request_fingerprint, ) + + +__all__ = ( + "ParticipantApprovalControlIntent", + "ParticipantCancellationControlIntent", + "ParticipantControlIntent", + "ParticipantControlMixin", + "ParticipantDenialControlIntent", + "ParticipantExternalDirectionControlIntent", + "ParticipantHandoffControlIntent", + "ParticipantInterventionControlIntent", + "ParticipantOverrideControlIntent", + "ParticipantProposalControlIntent", +) diff --git a/implementations/python/packages/raes_runtime/participant_control_intents.py b/implementations/python/packages/raes_runtime/participant_control_intents.py new file mode 100644 index 000000000..c1660433b --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_control_intents.py @@ -0,0 +1,138 @@ +"""Closed caller-intent models for RUN-310 supervisory mediation.""" + +from __future__ import annotations + +from typing import Annotated, Literal, TypeAlias + +from pydantic import BaseModel, ConfigDict, Field, model_validator +from raes_contracts.contracts.participant_control import ParticipantControlTargetKind + + +class ParticipantControlIntentBase(BaseModel): + """Caller-owned coordinates shared by all supervisory intents.""" + + model_config = ConfigDict(extra="forbid") + + declaration_ref: str = Field(min_length=1) + episode_id: str = Field(min_length=1) + client_correlation_id: str = Field(min_length=1) + policy_revision: str = Field(min_length=1) + expected_state_revision: int = Field(ge=0) + provenance_refs: list[str] = Field(min_length=1) + evidence_refs: list[str] = Field(min_length=1) + object_marking_refs: list[str] = Field(min_length=1) + limitation_refs: list[str] = Field(min_length=1) + + +class ParticipantProposalControlIntent(ParticipantControlIntentBase): + kind: Literal["proposal"] = "proposal" + proposal_id: str = Field(min_length=1) + proposal_revision: int = Field(gt=0) + action_contract_ref: str = Field(min_length=1) + decision_surface_ref: str | None = None + proposal_binding_ref: str | None = None + payload_ref: str | None = None + payload_digest: str | None = None + source_proposal_ref: str | None = None + source_proposal_revision: int | None = Field(default=None, gt=0) + transformation_ref: str | None = None + + @model_validator(mode="after") + def _require_one_payload_source(self) -> ParticipantProposalControlIntent: + if (self.payload_ref is None) == (self.payload_digest is None): + raise ValueError("proposal intent requires exactly one payload reference or digest") + return self + + +class ParticipantApprovalControlIntent(ParticipantControlIntentBase): + kind: Literal["approval"] = "approval" + proposal_ref: str = Field(min_length=1) + proposal_revision: int = Field(gt=0) + decision_ref: str = Field(min_length=1) + decision_revision: int = Field(gt=0) + + +class ParticipantDenialControlIntent(ParticipantControlIntentBase): + kind: Literal["denial"] = "denial" + proposal_ref: str = Field(min_length=1) + proposal_revision: int = Field(gt=0) + decision_ref: str = Field(min_length=1) + decision_revision: int = Field(gt=0) + + +class ParticipantExternalDirectionControlIntent(ParticipantControlIntentBase): + kind: Literal["external-direction"] = "external-direction" + target_kind: Literal[ + ParticipantControlTargetKind.PROPOSAL, + ParticipantControlTargetKind.ACTION, + ParticipantControlTargetKind.CONTROL, + ] + target_ref: str = Field(min_length=1) + target_revision: int = Field(gt=0) + + +class ParticipantInterventionControlIntent(ParticipantControlIntentBase): + kind: Literal["intervention"] = "intervention" + affected_target_kind: Literal[ + ParticipantControlTargetKind.ACTION, + ParticipantControlTargetKind.CONTROL, + ParticipantControlTargetKind.ATTEMPT, + ] + affected_occurrence_ref: str = Field(min_length=1) + affected_revision: int = Field(gt=0) + intervention_ref: str = Field(min_length=1) + + +class ParticipantHandoffControlIntent(ParticipantControlIntentBase): + kind: Literal["handoff"] = "handoff" + completion_evidence_ref: str = Field(min_length=1) + + +class ParticipantOverrideControlIntent(ParticipantControlIntentBase): + kind: Literal["override"] = "override" + superseded_target_kind: Literal[ + ParticipantControlTargetKind.CONTROL, + ParticipantControlTargetKind.DECISION, + ] + superseded_occurrence_ref: str = Field(min_length=1) + superseded_revision: int = Field(gt=0) + replacement_ref: str = Field(min_length=1) + + +class ParticipantCancellationControlIntent(ParticipantControlIntentBase): + kind: Literal["cancellation"] = "cancellation" + target_kind: Literal[ + ParticipantControlTargetKind.PROPOSAL, + ParticipantControlTargetKind.DECISION, + ParticipantControlTargetKind.ADMITTED_ACTION, + ParticipantControlTargetKind.ATTEMPT, + ] + target_ref: str = Field(min_length=1) + target_revision: int = Field(gt=0) + + +ParticipantControlIntent: TypeAlias = Annotated[ + ParticipantProposalControlIntent + | ParticipantApprovalControlIntent + | ParticipantDenialControlIntent + | ParticipantExternalDirectionControlIntent + | ParticipantInterventionControlIntent + | ParticipantHandoffControlIntent + | ParticipantOverrideControlIntent + | ParticipantCancellationControlIntent, + Field(discriminator="kind"), +] + + +__all__ = ( + "ParticipantApprovalControlIntent", + "ParticipantCancellationControlIntent", + "ParticipantControlIntent", + "ParticipantControlIntentBase", + "ParticipantDenialControlIntent", + "ParticipantExternalDirectionControlIntent", + "ParticipantHandoffControlIntent", + "ParticipantInterventionControlIntent", + "ParticipantOverrideControlIntent", + "ParticipantProposalControlIntent", +) diff --git a/implementations/python/packages/raes_runtime/participant_control_mediation.py b/implementations/python/packages/raes_runtime/participant_control_mediation.py new file mode 100644 index 000000000..ef5642c59 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_control_mediation.py @@ -0,0 +1,413 @@ +"""RUN-310 live supervisory mediation over compiled ACT-617 policy.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import asdict +from uuid import uuid4 + +from raes_contracts.contracts import ( + ParticipantControlDeclarationModel, + ParticipantControlOccurrenceModel, + validate_participant_control_occurrence_context, +) +from raes_contracts.contracts.participant_control import ( + ParticipantControlDisposition, + ParticipantControlTargetContextModel, +) +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.planning import RuntimeDomain +from raes_contracts.runtime_state import OperationReceipt, OperationState, OperationStatus +from raes_processor.models import ( + MixedControlControllerStateRuntime, + MixedControlTransitionRuntime, + ParticipantBehaviorSpecificationRuntime, +) + +from .control_plane_security import ControlPlaneIdentity +from .control_plane_store import AuditEvent, ControlPlaneOperationRecord +from .participant_control_intents import ParticipantControlIntent +from .participant_control_occurrences import build_participant_control_occurrence +from .participant_control_targets import ( + participant_control_target_contexts, + resolve_participant_control_target, +) + +_ORDER_STRATEGY = "total-effective-order" + + +def record_participant_control( + control_plane: object, + *, + participant_address: str, + intent: ParticipantControlIntent, + identity: object, + idempotency_key: str, +) -> OperationReceipt: + """Bind, validate, and atomically append one supervisory occurrence.""" + + if not isinstance(identity, ControlPlaneIdentity): + raise PermissionError("participant control requires an authenticated identity") + if identity.target_name is not None and identity.target_name != control_plane.target_name: + raise PermissionError("participant control identity is not authorized for this target") + _require_participant_binding(identity, participant_address) + specification = _specification_for_participant(control_plane, participant_address, identity) + transition = _transition_for_intent(specification, intent, identity) + state = _state_by_address(specification, transition.from_state_address) + semantic_fingerprint = _semantic_fingerprint( + control_plane, + participant_address, + intent, + identity, + specification, + transition, + ) + scoped_key = _scoped_idempotency_key( + control_plane, + participant_address, + intent, + identity, + idempotency_key, + ) + + with control_plane._participant_control_lock: + existing = control_plane._store.find_by_idempotency(scoped_key) if scoped_key else None + if existing is not None: + if existing.request_fingerprint != semantic_fingerprint: + raise ValueError("Idempotency-Key was reused with different semantics.") + control_plane._operations[existing.receipt.operation_id] = existing + return existing.receipt + + history = list(control_plane._snapshot.participant_control_history.get(participant_address, ())) + current_state, current_revision = _fold_controller_state( + specification, + history, + episode_id=intent.episode_id, + ) + resolved_target, target_rejection_reason = resolve_participant_control_target( + control_plane._snapshot, + intent, + participant_address=participant_address, + ) + rejection_reason = _rejection_reason( + specification, + transition, + state, + intent, + current_state=current_state, + current_revision=current_revision, + target_rejection_reason=target_rejection_reason, + ) + accepted = rejection_reason is None + occurrence = build_participant_control_occurrence( + control_plane, + participant_address, + intent, + specification, + transition, + state, + history, + resolved_target=resolved_target, + accepted=accepted, + rejection_reason=rejection_reason, + ) + candidate_history = [*history, occurrence.model_dump(mode="json")] + _validate_candidate_history( + specification, + candidate_history, + known_targets=participant_control_target_contexts(control_plane._snapshot), + ) + next_snapshot = control_plane._snapshot.with_entries( + dict(control_plane._snapshot.entries), + participant_control_history={ + **control_plane._snapshot.participant_control_history, + participant_address: candidate_history, + }, + ) + operation_id = str(uuid4()) + submitted_at = occurrence.recorded_at + diagnostics = [] if accepted else [_rejection_diagnostic(participant_address, rejection_reason)] + receipt = OperationReceipt( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + submitted_at=submitted_at, + accepted=accepted, + diagnostics=diagnostics, + ) + status = OperationStatus( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + state=OperationState.SUCCEEDED if accepted else OperationState.FAILED, + submitted_at=submitted_at, + updated_at=submitted_at, + diagnostics=diagnostics, + changed_addresses=[participant_address], + ) + record = ControlPlaneOperationRecord( + receipt=receipt, + status=status, + request_fingerprint=semantic_fingerprint, + idempotency_key=scoped_key, + ) + audit_event = AuditEvent( + timestamp=submitted_at, + action="record_participant_control", + identity=identity.identity, + allowed=accepted, + target=participant_address, + operation_id=operation_id, + reason=rejection_reason or "accepted", + details={ + "episode_id": intent.episode_id, + "kind": intent.kind, + "event_id": occurrence.event_id, + }, + ) + expected_head = _history_head(history) + control_plane._store.commit_control_transition( + participant_address=participant_address, + expected_head=expected_head, + snapshot=next_snapshot, + record=record, + audit_event=audit_event, + ) + control_plane._snapshot = next_snapshot + control_plane._operations[operation_id] = record + return receipt + + +def _specification_for_participant( + control_plane: object, + participant_address: str, + identity: ControlPlaneIdentity, +) -> ParticipantBehaviorSpecificationRuntime: + authorized_controllers = _authorized_controller_refs(identity, participant_address) + candidates = [ + specification + for specification in control_plane._behavior_specifications.values() + if specification.mixed_control_participant_address == participant_address + and any(state.controller_address in authorized_controllers for state in specification.controller_states) + ] + if len(candidates) != 1: + raise ValueError("participant must resolve exactly one trusted mixed-control specification") + return candidates[0] + + +def _transition_for_intent( + specification: ParticipantBehaviorSpecificationRuntime, + intent: ParticipantControlIntent, + identity: ControlPlaneIdentity, +) -> MixedControlTransitionRuntime: + authorized_controllers = _authorized_controller_refs( + identity, + specification.mixed_control_participant_address, + ) + states = {state.address: state for state in specification.controller_states} + candidates = [ + transition + for transition in specification.control_transitions + if transition.address == intent.declaration_ref + and transition.transition_kind == intent.kind + and transition.from_state_address in states + and states[transition.from_state_address].controller_address in authorized_controllers + ] + if len(candidates) != 1: + raise ValueError("control intent must resolve exactly one compiled transition") + return candidates[0] + + +def _state_by_address( + specification: ParticipantBehaviorSpecificationRuntime, + address: str, +) -> MixedControlControllerStateRuntime: + candidates = [state for state in specification.controller_states if state.address == address] + if len(candidates) != 1: + raise ValueError("compiled controller state must resolve exactly once") + return candidates[0] + + +def _require_participant_binding( + identity: ControlPlaneIdentity, + participant_address: str, +) -> None: + if not _authorized_controller_refs(identity, participant_address): + raise PermissionError("participant control subject is not authorized") + + +def _authorized_controller_refs( + identity: ControlPlaneIdentity, + participant_address: str, +) -> frozenset[str]: + return frozenset( + binding.controller_ref + for binding in identity.participant_control_subjects + if binding.participant_address == participant_address + ) + + +def _fold_controller_state( + specification: ParticipantBehaviorSpecificationRuntime, + history: list[dict[str, object]], + *, + episode_id: str, +) -> tuple[str, int]: + state_address = specification.mixed_control_initial_state_address + revision = 0 + transitions = {transition.address: transition for transition in specification.control_transitions} + for payload in history: + event = ParticipantControlOccurrenceModel.model_validate(payload) + if event.episode_id != episode_id: + continue + if event.occurrence.disposition is not ParticipantControlDisposition.ACCEPTED: + continue + transition = transitions.get(event.occurrence.declaration_ref) + if transition is None: + raise ValueError("control history references an unknown compiled transition") + if transition.from_state_address != state_address or transition.expected_state_revision != revision: + raise ValueError("control history does not replay from compiled controller state") + state_address = transition.to_state_address + revision = transition.resulting_state_revision + return state_address, revision + + +def _rejection_reason( + specification: ParticipantBehaviorSpecificationRuntime, + transition: MixedControlTransitionRuntime, + state: MixedControlControllerStateRuntime, + intent: ParticipantControlIntent, + *, + current_state: str, + current_revision: int, + target_rejection_reason: str | None, +) -> str | None: + if specification.mixed_control_order_strategy != _ORDER_STRATEGY: + return "unsupported-order-strategy" + if intent.policy_revision != specification.mixed_control_policy_revision: + return "stale-policy" + if ( + intent.expected_state_revision != current_revision + or transition.expected_state_revision != current_revision + or transition.from_state_address != current_state + ): + return "stale-state" + if transition.policy_revision != specification.mixed_control_policy_revision: + return "stale-policy" + if target_rejection_reason is not None: + return target_rejection_reason + if state.authority_status != "active": + return "revoked-authority" + if not state.valid_from_order <= transition.effective_order <= state.valid_until_order: + return "late-authority" + if not transition.valid_from_order <= transition.effective_order <= transition.valid_until_order: + return "late-authority" + return None + + +def _history_head(history: list[dict[str, object]]) -> str | None: + if not history: + return None + value = history[-1].get("event_id") + return value if isinstance(value, str) and value else None + + +def _declaration( + specification: ParticipantBehaviorSpecificationRuntime, + transition: MixedControlTransitionRuntime, + episode_id: str, +) -> ParticipantControlDeclarationModel: + state = _state_by_address(specification, transition.from_state_address) + return ParticipantControlDeclarationModel.model_validate( + { + "declaration_ref": transition.address, + "kind": transition.transition_kind, + "participant_address": specification.mixed_control_participant_address, + "episode_id": episode_id, + "controller_ref": state.controller_address, + "controller_state_ref": state.address, + "authority_basis_refs": list(state.authority_basis_addresses or state.authority_basis_refs), + "controlled_scope_refs": list(state.scope_addresses or state.scope_refs), + "behavior_specification_ref": specification.address, + "mixed_control_policy_ref": specification.address, + "policy_revision": transition.policy_revision, + "expected_state_revision": transition.expected_state_revision, + "effective_order": transition.effective_order, + "valid_from_order": transition.valid_from_order, + "valid_until_order": transition.valid_until_order, + } + ) + + +def _validate_candidate_history( + specification: ParticipantBehaviorSpecificationRuntime, + history: list[dict[str, object]], + *, + known_targets: tuple[ParticipantControlTargetContextModel, ...], +) -> None: + records = [ParticipantControlOccurrenceModel.model_validate(payload) for payload in history] + transitions = {transition.address: transition for transition in specification.control_transitions} + declarations = [ + _declaration( + specification, + transitions[record.occurrence.declaration_ref], + record.episode_id, + ) + for record in records + ] + validate_participant_control_occurrence_context( + records, + declarations=declarations, + known_targets=known_targets, + ) + + +def _semantic_fingerprint( + control_plane: object, + participant_address: str, + intent: ParticipantControlIntent, + identity: ControlPlaneIdentity, + specification: ParticipantBehaviorSpecificationRuntime, + transition: MixedControlTransitionRuntime, +) -> str: + payload = { + "target": control_plane.target_name, + "identity": identity.identity, + "participant": participant_address, + "intent": intent.model_dump(mode="json"), + "specification": specification.address, + "transition": asdict(transition), + } + return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest() + + +def _scoped_idempotency_key( + control_plane: object, + participant_address: str, + intent: ParticipantControlIntent, + identity: ControlPlaneIdentity, + idempotency_key: str, +) -> str: + if not idempotency_key: + return "" + scope = ( + control_plane.target_name, + identity.identity, + intent.kind, + participant_address, + intent.episode_id, + idempotency_key, + ) + digest = hashlib.sha256("\x1f".join(scope).encode()).hexdigest() + return f"participant-control:{digest}" + + +def _rejection_diagnostic(participant_address: str, reason: str | None) -> Diagnostic: + return Diagnostic( + code=f"runtime.participant-control.{reason or 'rejected'}", + domain="runtime", + address=participant_address, + message="Participant supervisory control intent was rejected by the bound runtime policy.", + ) + + +__all__ = ("record_participant_control",) diff --git a/implementations/python/packages/raes_runtime/participant_control_occurrences.py b/implementations/python/packages/raes_runtime/participant_control_occurrences.py new file mode 100644 index 000000000..500b4e5b9 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_control_occurrences.py @@ -0,0 +1,165 @@ +"""API-409 occurrence construction for RUN-310 runtime mediation.""" + +from __future__ import annotations + +from datetime import UTC, datetime +from uuid import uuid4 + +from raes_contracts.contracts import ParticipantControlOccurrenceModel +from raes_contracts.contracts.participant_control import ( + ParticipantCancellationEffect, + ParticipantControlTargetKind, +) +from raes_processor.models import ( + MixedControlControllerStateRuntime, + MixedControlTransitionRuntime, + ParticipantBehaviorSpecificationRuntime, +) + +from .participant_control_intents import ( + ParticipantApprovalControlIntent, + ParticipantCancellationControlIntent, + ParticipantControlIntent, + ParticipantDenialControlIntent, + ParticipantExternalDirectionControlIntent, + ParticipantHandoffControlIntent, + ParticipantInterventionControlIntent, + ParticipantOverrideControlIntent, + ParticipantProposalControlIntent, +) +from .participant_control_targets import ResolvedParticipantControlTarget + + +def build_participant_control_occurrence( + control_plane: object, + participant_address: str, + intent: ParticipantControlIntent, + specification: ParticipantBehaviorSpecificationRuntime, + transition: MixedControlTransitionRuntime, + state: MixedControlControllerStateRuntime, + history: list[dict[str, object]], + *, + resolved_target: ResolvedParticipantControlTarget | None, + accepted: bool, + rejection_reason: str | None, +) -> ParticipantControlOccurrenceModel: + """Build one immutable runtime-owned occurrence from a caller intent.""" + + now = datetime.now(UTC).isoformat().replace("+00:00", "Z") + detail: dict[str, object] = { + "kind": intent.kind, + "declaration_ref": transition.address, + "controller_ref": state.controller_address, + "controller_state_ref": state.address, + "authority_basis_refs": list(state.authority_basis_addresses or state.authority_basis_refs), + "controlled_scope_refs": list(state.scope_addresses or state.scope_refs), + "behavior_specification_ref": specification.address, + "mixed_control_policy_ref": specification.address, + "policy_revision": transition.policy_revision, + "expected_state_revision": transition.expected_state_revision, + "effective_order": transition.effective_order, + "valid_from_order": transition.valid_from_order, + "valid_until_order": transition.valid_until_order, + "occurrence_revision": len(history) + 1, + "disposition": "accepted" if accepted else "rejected", + "reason_code": rejection_reason, + "limitation_refs": list(intent.limitation_refs), + **_kind_detail(intent, transition), + } + return ParticipantControlOccurrenceModel.model_validate( + { + "event_id": f"participant-control.{uuid4()}", + "schema_name": "participant-control-occurrence", + "schema_version": "1.0.0", + "event_type": "participant-control-occurrence", + "extension_policy": "closed", + "participant_address": participant_address, + "episode_id": intent.episode_id, + "occurred_at": now, + "recorded_at": now, + "ingested_at": now, + "clock_authority": "runtime.control-plane.clock", + "ordering_basis": "logical_clock", + "logical_order_ref": f"effective-order:{transition.effective_order}", + "predecessor_event_refs": [resolved_target.predecessor_ref] if resolved_target is not None else [], + "actor_ref": state.controller_address, + "producer_ref": f"runtime.control-plane.{control_plane.target_name}", + "provenance_refs": list(intent.provenance_refs), + "evidence_refs": list(intent.evidence_refs), + "object_marking_refs": list(intent.object_marking_refs), + "authorization_scope": (state.scope_addresses or state.scope_refs)[0], + "occurrence": detail, + } + ) + + +def _kind_detail( + intent: ParticipantControlIntent, + transition: MixedControlTransitionRuntime, +) -> dict[str, object]: + if isinstance(intent, ParticipantProposalControlIntent): + return { + "proposal_id": intent.proposal_id, + "proposal_revision": intent.proposal_revision, + "admission_status": "not-admitted", + "action_contract_ref": intent.action_contract_ref, + "decision_surface_ref": intent.decision_surface_ref, + "proposal_binding_ref": intent.proposal_binding_ref, + "payload_ref": intent.payload_ref, + "payload_digest": intent.payload_digest, + "source_proposal_ref": intent.source_proposal_ref, + "source_proposal_revision": intent.source_proposal_revision, + "transformation_ref": intent.transformation_ref, + } + if isinstance(intent, (ParticipantApprovalControlIntent, ParticipantDenialControlIntent)): + return { + "proposal_ref": intent.proposal_ref, + "proposal_revision": intent.proposal_revision, + "decision_ref": intent.decision_ref, + "decision_revision": intent.decision_revision, + } + if isinstance(intent, ParticipantExternalDirectionControlIntent): + return { + "target_kind": intent.target_kind.value, + "target_ref": intent.target_ref, + "target_revision": intent.target_revision, + } + if isinstance(intent, ParticipantInterventionControlIntent): + return { + "affected_target_kind": intent.affected_target_kind.value, + "affected_occurrence_ref": intent.affected_occurrence_ref, + "affected_revision": intent.affected_revision, + "intervention_ref": intent.intervention_ref, + } + if isinstance(intent, ParticipantHandoffControlIntent): + return { + "prior_controller_state_ref": transition.from_state_address, + "resulting_controller_state_ref": transition.to_state_address, + "resulting_state_revision": transition.resulting_state_revision, + "completion_evidence_ref": intent.completion_evidence_ref, + } + if isinstance(intent, ParticipantOverrideControlIntent): + return { + "superseded_target_kind": intent.superseded_target_kind.value, + "superseded_occurrence_ref": intent.superseded_occurrence_ref, + "superseded_revision": intent.superseded_revision, + "replacement_ref": intent.replacement_ref, + } + assert isinstance(intent, ParticipantCancellationControlIntent) + return { + "target_kind": intent.target_kind.value, + "target_ref": intent.target_ref, + "target_revision": intent.target_revision, + "cancellation_effect": _cancellation_effect(intent.target_kind).value, + } + + +def _cancellation_effect(target_kind: ParticipantControlTargetKind) -> ParticipantCancellationEffect: + if target_kind in {ParticipantControlTargetKind.PROPOSAL, ParticipantControlTargetKind.DECISION}: + return ParticipantCancellationEffect.PREVENTED + if target_kind is ParticipantControlTargetKind.ADMITTED_ACTION: + return ParticipantCancellationEffect.PARTIAL_LIMITATION + return ParticipantCancellationEffect.TOO_LATE + + +__all__ = ("build_participant_control_occurrence",) diff --git a/implementations/python/packages/raes_runtime/participant_control_targets.py b/implementations/python/packages/raes_runtime/participant_control_targets.py new file mode 100644 index 000000000..57884a0e1 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_control_targets.py @@ -0,0 +1,239 @@ +"""Authoritative target resolution for RUN-310 supervisory occurrences.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from raes_contracts.contracts import ( + ParticipantBehaviorHistoryEventModel, + ParticipantControlOccurrenceModel, +) +from raes_contracts.contracts.participant_control import ( + ParticipantApprovalOccurrenceModel, + ParticipantControlDisposition, + ParticipantControlTargetContextModel, + ParticipantControlTargetKind, + ParticipantDenialOccurrenceModel, + ParticipantProposalOccurrenceModel, +) +from raes_contracts.runtime_state import RuntimeSnapshot + +from .participant_control_intents import ( + ParticipantApprovalControlIntent, + ParticipantCancellationControlIntent, + ParticipantControlIntent, + ParticipantDenialControlIntent, + ParticipantExternalDirectionControlIntent, + ParticipantInterventionControlIntent, + ParticipantOverrideControlIntent, + ParticipantProposalControlIntent, +) + + +@dataclass(frozen=True) +class ResolvedParticipantControlTarget: + """One target whose revision and runtime scope resolved exactly.""" + + context: ParticipantControlTargetContextModel + predecessor_ref: str + + +def resolve_participant_control_target( + snapshot: RuntimeSnapshot, + intent: ParticipantControlIntent, + *, + participant_address: str, +) -> tuple[ResolvedParticipantControlTarget | None, str | None]: + """Resolve an intent target without accepting caller-supplied coordinates.""" + + requested = _intent_target(intent) + if requested is None: + return None, None + kind, reference, revision = requested + matches = [ + candidate + for candidate in participant_control_targets(snapshot) + if candidate.context.target_kind is kind + and candidate.context.target_ref == reference + and candidate.context.target_revision == revision + and candidate.context.participant_address == participant_address + and candidate.context.episode_id == intent.episode_id + ] + if len(matches) != 1: + return None, "invalid-target" + return matches[0], None + + +def participant_control_target_contexts( + snapshot: RuntimeSnapshot, +) -> tuple[ParticipantControlTargetContextModel, ...]: + """Return all lifecycle targets used by API-409 contextual validation.""" + + return tuple(candidate.context for candidate in participant_control_targets(snapshot)) + + +def participant_control_targets( + snapshot: RuntimeSnapshot, +) -> tuple[ResolvedParticipantControlTarget, ...]: + """Project control and behavior histories into typed target coordinates.""" + + targets: dict[ + tuple[ParticipantControlTargetKind, str, int, str, str], + ResolvedParticipantControlTarget, + ] = {} + for events in snapshot.participant_control_history.values(): + for payload in events: + _register_control_targets( + targets, + ParticipantControlOccurrenceModel.model_validate(payload), + ) + for events in snapshot.participant_behavior_history.values(): + for payload in events: + _register_behavior_targets( + targets, + ParticipantBehaviorHistoryEventModel.model_validate(payload), + ) + return tuple(targets.values()) + + +def _register_control_targets( + targets: dict[ + tuple[ParticipantControlTargetKind, str, int, str, str], + ResolvedParticipantControlTarget, + ], + event: ParticipantControlOccurrenceModel, +) -> None: + occurrence = event.occurrence + if occurrence.disposition is not ParticipantControlDisposition.ACCEPTED: + return + _register( + targets, + kind=ParticipantControlTargetKind.CONTROL, + reference=event.event_id, + revision=occurrence.occurrence_revision, + participant_address=event.participant_address, + episode_id=event.episode_id, + predecessor_ref=event.event_id, + ) + if isinstance(occurrence, ParticipantProposalOccurrenceModel): + _register( + targets, + kind=ParticipantControlTargetKind.PROPOSAL, + reference=occurrence.proposal_id, + revision=occurrence.proposal_revision, + participant_address=event.participant_address, + episode_id=event.episode_id, + predecessor_ref=event.event_id, + ) + elif isinstance(occurrence, (ParticipantApprovalOccurrenceModel, ParticipantDenialOccurrenceModel)): + _register( + targets, + kind=ParticipantControlTargetKind.DECISION, + reference=occurrence.decision_ref, + revision=occurrence.decision_revision, + participant_address=event.participant_address, + episode_id=event.episode_id, + predecessor_ref=event.event_id, + ) + + +def _register_behavior_targets( + targets: dict[ + tuple[ParticipantControlTargetKind, str, int, str, str], + ResolvedParticipantControlTarget, + ], + event: ParticipantBehaviorHistoryEventModel, +) -> None: + if event.event_type.value != "action_attempted": + return + phase = event.lifecycle_phase.value if event.lifecycle_phase is not None else None + _register( + targets, + kind=ParticipantControlTargetKind.ACTION, + reference=event.action_instance_id, + revision=1, + participant_address=event.participant_address, + episode_id=event.episode_id, + predecessor_ref=event.action_instance_id, + ) + admission = event.admission_disposition.value if event.admission_disposition is not None else None + if phase == "selection_or_admission" and admission == "admitted": + _register( + targets, + kind=ParticipantControlTargetKind.ADMITTED_ACTION, + reference=event.action_instance_id, + revision=1, + participant_address=event.participant_address, + episode_id=event.episode_id, + predecessor_ref=event.action_instance_id, + ) + if phase == "execution_attempt": + attempt_ref = event.operation_ref or event.action_instance_id + _register( + targets, + kind=ParticipantControlTargetKind.ATTEMPT, + reference=attempt_ref, + revision=1, + participant_address=event.participant_address, + episode_id=event.episode_id, + predecessor_ref=attempt_ref, + ) + + +def _register( + targets: dict[ + tuple[ParticipantControlTargetKind, str, int, str, str], + ResolvedParticipantControlTarget, + ], + *, + kind: ParticipantControlTargetKind, + reference: str, + revision: int, + participant_address: str, + episode_id: str, + predecessor_ref: str, +) -> None: + context = ParticipantControlTargetContextModel( + target_kind=kind, + target_ref=reference, + target_revision=revision, + participant_address=participant_address, + episode_id=episode_id, + ) + key = (kind, reference, revision, participant_address, episode_id) + candidate = ResolvedParticipantControlTarget(context=context, predecessor_ref=predecessor_ref) + existing = targets.setdefault(key, candidate) + if existing != candidate: + raise ValueError("runtime target identity is ambiguous") + + +def _intent_target( + intent: ParticipantControlIntent, +) -> tuple[ParticipantControlTargetKind, str, int] | None: + if isinstance(intent, ParticipantProposalControlIntent): + if intent.source_proposal_ref is None: + return None + assert intent.source_proposal_revision is not None + return ( + ParticipantControlTargetKind.PROPOSAL, + intent.source_proposal_ref, + intent.source_proposal_revision, + ) + if isinstance(intent, (ParticipantApprovalControlIntent, ParticipantDenialControlIntent)): + return ParticipantControlTargetKind.PROPOSAL, intent.proposal_ref, intent.proposal_revision + if isinstance(intent, ParticipantExternalDirectionControlIntent): + return intent.target_kind, intent.target_ref, intent.target_revision + if isinstance(intent, ParticipantInterventionControlIntent): + return intent.affected_target_kind, intent.affected_occurrence_ref, intent.affected_revision + if isinstance(intent, ParticipantOverrideControlIntent): + return intent.superseded_target_kind, intent.superseded_occurrence_ref, intent.superseded_revision + if isinstance(intent, ParticipantCancellationControlIntent): + return intent.target_kind, intent.target_ref, intent.target_revision + return None + + +__all__ = ( + "ResolvedParticipantControlTarget", + "participant_control_target_contexts", + "resolve_participant_control_target", +) diff --git a/implementations/python/packages/raes_runtime/participant_result_contracts.py b/implementations/python/packages/raes_runtime/participant_result_contracts.py index fbc6e1bd7..62a7f7978 100644 --- a/implementations/python/packages/raes_runtime/participant_result_contracts.py +++ b/implementations/python/packages/raes_runtime/participant_result_contracts.py @@ -11,6 +11,10 @@ iter_participant_concurrency_snapshot_violations, iter_participant_concurrency_transition_violations, ) +from raes_contracts.participant_control_history import ( + iter_participant_control_history_snapshot_violations, + iter_participant_control_history_transition_violations, +) from raes_contracts.participant_episode import iter_participant_episode_snapshot_violations from raes_contracts.participant_shared_state import ( iter_participant_shared_state_history_transition_violations, @@ -76,6 +80,9 @@ def participant_runtime_state_contract_diagnostics( shared_state_records=snapshot.shared_state_records, shared_state_history=snapshot.shared_state_history, ), + *iter_participant_control_history_snapshot_violations( + snapshot.participant_control_history, + ), ] return [ _failure_diagnostic("runtime.backend-contract-invalid", address, message) for address, message in violations @@ -114,4 +121,11 @@ def participant_runtime_history_transition_diagnostics( next_snapshot.time_management_contexts, ) ] + + [ + _failure_diagnostic("runtime.backend-contract-invalid", address, message) + for address, message in iter_participant_control_history_transition_violations( + previous_snapshot.participant_control_history, + next_snapshot.participant_control_history, + ) + ] ) diff --git a/implementations/python/tests/test_api_409_participant_control_occurrences.py b/implementations/python/tests/test_api_409_participant_control_occurrences.py index 85dc8de88..c5cac7e06 100644 --- a/implementations/python/tests/test_api_409_participant_control_occurrences.py +++ b/implementations/python/tests/test_api_409_participant_control_occurrences.py @@ -389,6 +389,45 @@ def test_typed_target_context_rejects_an_unknown_kind_and_reference_pair() -> No ) +def test_rejected_typed_target_preserves_the_invalid_attempt_without_becoming_a_target() -> None: + value = _envelope( + { + "kind": "external-direction", + "target_kind": "action", + "target_ref": "action:unknown", + "target_revision": 1, + } + ) + value["occurrence"]["disposition"] = "rejected" + value["occurrence"]["reason_code"] = "invalid-target" + direction = ParticipantControlOccurrenceModel.model_validate(value) + + validate_participant_control_occurrence_context( + [direction], + declarations=[_declaration("external-direction")], + ) + intervention = ParticipantControlOccurrenceModel.model_validate( + _envelope( + { + "kind": "intervention", + "affected_target_kind": "control", + "affected_occurrence_ref": direction.event_id, + "affected_revision": direction.occurrence.occurrence_revision, + "intervention_ref": "intervention:rejected-direction", + } + ) + ) + + with pytest.raises(ValueError, match="typed target reference and kind must resolve"): + validate_participant_control_occurrence_context( + [direction, intervention], + declarations=[ + _declaration("external-direction"), + _declaration("intervention"), + ], + ) + + def test_transformed_proposal_requires_source_provenance_and_marking_inheritance() -> None: source, transformed = _transformed_proposal() assert transformed.occurrence.admission_status == "not-admitted" diff --git a/implementations/python/tests/test_run_310_supervisory_lifecycle.py b/implementations/python/tests/test_run_310_supervisory_lifecycle.py new file mode 100644 index 000000000..200d4fa4a --- /dev/null +++ b/implementations/python/tests/test_run_310_supervisory_lifecycle.py @@ -0,0 +1,922 @@ +"""RUN-310 observable supervisory lifecycle tests.""" + +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path + +import pytest +from raes.participant_behavior_specification import MixedControlTransitionKind +from raes_backend_stubs.stubs import create_stub_target +from raes_contracts.planning import RuntimeDomain +from raes_contracts.runtime_state import OperationReceipt, OperationState, OperationStatus, RuntimeSnapshot +from raes_processor.models import ( + MixedControlControllerStateRuntime, + MixedControlDispositionRulesRuntime, + MixedControlTransitionRuntime, + ParticipantBehaviorSpecificationRuntime, +) +from raes_runtime.control_plane import RuntimeControlPlane +from raes_runtime.control_plane_api import create_control_plane_app +from raes_runtime.control_plane_security import ( + ControlPlaneIdentity, + ControlPlaneRole, + ControlPlaneSecurityConfig, + ParticipantControlSubjectBinding, +) +from raes_runtime.control_plane_store import ( + AuditEvent, + ControlPlaneOperationRecord, + InMemoryControlPlaneStore, + LocalControlPlaneStore, +) +from raes_runtime.participant_control import ( + ParticipantApprovalControlIntent, + ParticipantCancellationControlIntent, + ParticipantDenialControlIntent, + ParticipantExternalDirectionControlIntent, + ParticipantHandoffControlIntent, + ParticipantInterventionControlIntent, + ParticipantOverrideControlIntent, + ParticipantProposalControlIntent, +) +from raes_runtime.participant_result_contracts import ( + participant_runtime_history_transition_diagnostics, + participant_runtime_state_contract_diagnostics, +) +from starlette.testclient import TestClient + + +def _control_event(event_id: str, *, revision: int = 1) -> dict[str, object]: + return { + "event_id": event_id, + "schema_name": "participant-control-occurrence", + "schema_version": "1.0.0", + "event_type": "participant-control-occurrence", + "extension_policy": "closed", + "participant_address": "participant.behavior.red-agent", + "episode_id": "episode-1", + "occurred_at": "2026-07-26T10:00:00Z", + "recorded_at": "2026-07-26T10:00:00Z", + "ingested_at": "2026-07-26T10:00:00Z", + "clock_authority": "runtime.control-plane.clock", + "ordering_basis": "logical_clock", + "logical_order_ref": f"order:{revision}", + "actor_ref": "participant.behavior.supervisor", + "producer_ref": "runtime.control-plane.test", + "provenance_refs": ["provenance:test"], + "evidence_refs": ["evidence:test"], + "object_marking_refs": ["marking:test"], + "authorization_scope": "nodes.web", + "occurrence": { + "kind": "handoff", + "declaration_ref": "participant.behavior-specification.controlled.control-transition.handoff", + "controller_ref": "participant.behavior.supervisor", + "controller_state_ref": "participant.behavior-specification.controlled.controller-state.autonomous", + "authority_basis_refs": ["entities.red-team"], + "controlled_scope_refs": ["nodes.web"], + "behavior_specification_ref": "participant.behavior-specification.controlled", + "mixed_control_policy_ref": "participant.behavior-specification.controlled", + "policy_revision": "1.0.0", + "expected_state_revision": 0, + "effective_order": 1, + "valid_from_order": 0, + "valid_until_order": 10, + "occurrence_revision": revision, + "disposition": "accepted", + "limitation_refs": ["limitation:none"], + "prior_controller_state_ref": ("participant.behavior-specification.controlled.controller-state.autonomous"), + "resulting_controller_state_ref": ( + "participant.behavior-specification.controlled.controller-state.supervised" + ), + "resulting_state_revision": 1, + "completion_evidence_ref": "evidence:handoff", + }, + } + + +def _operation_record(operation_id: str = "operation-1") -> ControlPlaneOperationRecord: + return ControlPlaneOperationRecord( + receipt=OperationReceipt( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + submitted_at="2026-07-26T10:00:00Z", + ), + status=OperationStatus( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + state=OperationState.SUCCEEDED, + submitted_at="2026-07-26T10:00:00Z", + updated_at="2026-07-26T10:00:00Z", + changed_addresses=["participant.behavior.red-agent"], + ), + request_fingerprint="fingerprint-1", + idempotency_key="scope-key-1", + ) + + +def _audit_event(operation_id: str = "operation-1") -> AuditEvent: + return AuditEvent( + timestamp="2026-07-26T10:00:00Z", + action="record_participant_control", + identity="operator", + allowed=True, + target="participant.behavior.red-agent", + operation_id=operation_id, + reason="accepted", + ) + + +_PARTICIPANT = "participant.behavior.red-agent" +_SPEC_ADDRESS = "participant.behavior-specification.controlled" +_AUTONOMOUS = f"{_SPEC_ADDRESS}.controller-state.autonomous" +_SUPERVISED = f"{_SPEC_ADDRESS}.controller-state.supervised" +_CONTROLLER = "participant.behavior.supervisor" + + +def _compiled_specification() -> ParticipantBehaviorSpecificationRuntime: + autonomous = MixedControlControllerStateRuntime( + address=_AUTONOMOUS, + name="autonomous", + spec={}, + state_id="autonomous", + controller_ref="supervisor", + controller_address=_CONTROLLER, + authority_basis_refs=("red-team",), + authority_basis_addresses=("entities.red-team",), + scope_refs=("web",), + scope_addresses=("nodes.web",), + policy_revision="1.0.0", + valid_from_order=0, + valid_until_order=20, + authority_status="active", + evidence_refs=("authority-evidence",), + evidence_addresses=("evidence.authority",), + ) + supervised = replace( + autonomous, + address=_SUPERVISED, + name="supervised", + state_id="supervised", + ) + kinds = [ + MixedControlTransitionKind.PROPOSAL, + MixedControlTransitionKind.APPROVAL, + MixedControlTransitionKind.DENIAL, + MixedControlTransitionKind.EXTERNAL_DIRECTION, + MixedControlTransitionKind.INTERVENTION, + MixedControlTransitionKind.HANDOFF, + MixedControlTransitionKind.OVERRIDE, + MixedControlTransitionKind.CANCELLATION, + ] + transitions = tuple( + MixedControlTransitionRuntime( + address=f"{_SPEC_ADDRESS}.control-transition.{kind.value}", + name=kind.value, + spec={}, + transition_id=kind.value, + transition_kind=kind.value, + from_state_address=_SUPERVISED if index > 5 else _AUTONOMOUS, + to_state_address=_SUPERVISED if index >= 5 else _AUTONOMOUS, + policy_revision="1.0.0", + expected_state_revision=index, + resulting_state_revision=index + 1, + effective_order=index + 1, + valid_from_order=0, + valid_until_order=20, + proposal_address=f"{_SPEC_ADDRESS}.control-transition.proposal" if index else "", + proposal_revision=1 if index else None, + evidence_refs=("transition-evidence",), + evidence_addresses=("evidence.transition",), + completion_evidence_refs=("handoff-evidence",) if kind is MixedControlTransitionKind.HANDOFF else (), + completion_evidence_addresses=("evidence.handoff",) if kind is MixedControlTransitionKind.HANDOFF else (), + ) + for index, kind in enumerate(kinds) + ) + return ParticipantBehaviorSpecificationRuntime( + address=_SPEC_ADDRESS, + name="controlled", + spec={}, + spec_name="controlled", + participant_addresses=(_PARTICIPANT,), + behavior_mode="mixed-control", + mixed_control_participant_address=_PARTICIPANT, + mixed_control_policy_revision="1.0.0", + mixed_control_order_strategy="total-effective-order", + mixed_control_initial_state_address=_AUTONOMOUS, + mixed_control_dispositions=MixedControlDispositionRulesRuntime( + duplicate="idempotent", + stale="reject", + revoked="reject", + late="reject", + concurrent="reject", + conflict="reject", + ), + controller_states=(autonomous, supervised), + control_transitions=transitions, + ) + + +def _single_transition_specification(kind: MixedControlTransitionKind) -> ParticipantBehaviorSpecificationRuntime: + specification = _compiled_specification() + transition = next( + candidate for candidate in specification.control_transitions if candidate.transition_kind == kind.value + ) + return replace( + specification, + control_transitions=( + replace( + transition, + from_state_address=_AUTONOMOUS, + to_state_address=_AUTONOMOUS, + expected_state_revision=0, + resulting_state_revision=1, + effective_order=1, + ), + ), + ) + + +def _identity(*, bound: bool = True) -> ControlPlaneIdentity: + return ControlPlaneIdentity( + identity="operator", + roles=frozenset({ControlPlaneRole.OPERATOR}), + target_name="stub", + participant_control_subjects=( + ParticipantControlSubjectBinding( + participant_address=_PARTICIPANT, + controller_ref=_CONTROLLER, + ), + ) + if bound + else (), + ) + + +def _base_intent_fields(kind: str, expected_revision: int) -> dict[str, object]: + return { + "declaration_ref": f"{_SPEC_ADDRESS}.control-transition.{kind}", + "episode_id": "episode-1", + "client_correlation_id": f"correlation-{kind}", + "policy_revision": "1.0.0", + "expected_state_revision": expected_revision, + "provenance_refs": ["provenance:test"], + "evidence_refs": ["evidence:test"], + "object_marking_refs": ["marking:test"], + "limitation_refs": ["limitation:none"], + } + + +def test_runtime_snapshot_preserves_first_class_control_history() -> None: + event = _control_event("control-event-1") + snapshot = RuntimeSnapshot(participant_control_history={"participant.behavior.red-agent": [event]}) + + updated = snapshot.with_entries(dict(snapshot.entries)) + + assert updated.participant_control_history == {"participant.behavior.red-agent": [event]} + + +@pytest.mark.parametrize("store_kind", ["memory", "local"]) +def test_control_history_round_trips_through_control_plane_store( + store_kind: str, + tmp_path: Path, +) -> None: + store = ( + InMemoryControlPlaneStore() if store_kind == "memory" else LocalControlPlaneStore(tmp_path / "control-plane") + ) + snapshot = RuntimeSnapshot( + participant_control_history={"participant.behavior.red-agent": [_control_event("control-event-1")]} + ) + + store.save_snapshot(snapshot) + + assert store.load_snapshot().participant_control_history == snapshot.participant_control_history + + +def test_control_history_snapshot_rejects_cross_participant_and_revision_gaps() -> None: + event = _control_event("control-event-1", revision=2) + snapshot = RuntimeSnapshot(participant_control_history={"participant.behavior.other-agent": [event]}) + + diagnostics = participant_runtime_state_contract_diagnostics(snapshot) + + assert diagnostics + assert all(diagnostic.code == "runtime.backend-contract-invalid" for diagnostic in diagnostics) + assert any("map key" in diagnostic.message for diagnostic in diagnostics) + assert any("occurrence_revision" in diagnostic.message for diagnostic in diagnostics) + + +def test_control_history_transition_rejects_rewrite_of_prior_occurrence() -> None: + original = _control_event("control-event-1") + rewritten = _control_event("control-event-rewritten") + previous = RuntimeSnapshot(participant_control_history={"participant.behavior.red-agent": [original]}) + next_snapshot = RuntimeSnapshot(participant_control_history={"participant.behavior.red-agent": [rewritten]}) + + diagnostics = participant_runtime_history_transition_diagnostics(previous, next_snapshot) + + assert diagnostics + assert any("append-only prefix" in diagnostic.message for diagnostic in diagnostics) + + +@pytest.mark.parametrize("store_kind", ["memory", "local"]) +def test_atomic_control_transition_commit_checks_head_and_persists_all_outputs( + store_kind: str, + tmp_path: Path, +) -> None: + store = ( + InMemoryControlPlaneStore() if store_kind == "memory" else LocalControlPlaneStore(tmp_path / "control-plane") + ) + event = _control_event("control-event-1") + snapshot = RuntimeSnapshot(participant_control_history={"participant.behavior.red-agent": [event]}) + record = _operation_record() + audit = _audit_event() + + store.commit_control_transition( + participant_address="participant.behavior.red-agent", + expected_head=None, + snapshot=snapshot, + record=record, + audit_event=audit, + ) + + restarted = store if store_kind == "memory" else LocalControlPlaneStore(tmp_path / "control-plane") + assert restarted.load_snapshot().participant_control_history == snapshot.participant_control_history + assert restarted.load_records()[record.receipt.operation_id] == record + assert restarted.find_by_idempotency(record.idempotency_key) == record + assert restarted.read_audit() == [audit] + + conflicting = RuntimeSnapshot( + participant_control_history={ + "participant.behavior.red-agent": [ + event, + _control_event("control-event-2", revision=2), + ] + } + ) + with pytest.raises(ValueError, match="expected control history head"): + restarted.commit_control_transition( + participant_address="participant.behavior.red-agent", + expected_head=None, + snapshot=conflicting, + record=replace(record, idempotency_key="scope-key-2"), + audit_event=replace(audit, operation_id="operation-2"), + ) + + assert restarted.load_snapshot().participant_control_history == snapshot.participant_control_history + + +def test_supervisory_lifecycle_records_every_control_kind_without_dispatch() -> None: + control_plane = RuntimeControlPlane( + create_stub_target(), + behavior_specifications={_SPEC_ADDRESS: _compiled_specification()}, + ) + intents = [ + ParticipantProposalControlIntent( + **_base_intent_fields("proposal", 0), + proposal_id="proposal-1", + proposal_revision=1, + action_contract_ref="action-contract:contain-host", + payload_ref="payload:proposal-1", + ), + ParticipantApprovalControlIntent( + **_base_intent_fields("approval", 1), + proposal_ref="proposal-1", + proposal_revision=1, + decision_ref="decision-approval-1", + decision_revision=1, + ), + ParticipantDenialControlIntent( + **_base_intent_fields("denial", 2), + proposal_ref="proposal-1", + proposal_revision=1, + decision_ref="decision-denial-1", + decision_revision=1, + ), + ParticipantExternalDirectionControlIntent( + **_base_intent_fields("external-direction", 3), + target_kind="control", + target_ref="pending-control-target", + target_revision=1, + ), + ParticipantInterventionControlIntent( + **_base_intent_fields("intervention", 4), + affected_target_kind="control", + affected_occurrence_ref="pending-control-target", + affected_revision=1, + intervention_ref="intervention-1", + ), + ParticipantHandoffControlIntent( + **_base_intent_fields("handoff", 5), + completion_evidence_ref="evidence:handoff", + ), + ParticipantOverrideControlIntent( + **_base_intent_fields("override", 6), + superseded_target_kind="decision", + superseded_occurrence_ref="decision-approval-1", + superseded_revision=1, + replacement_ref="decision-override-1", + ), + ParticipantCancellationControlIntent( + **_base_intent_fields("cancellation", 7), + target_kind="decision", + target_ref="decision-denial-1", + target_revision=1, + ), + ] + + receipts = [] + for index, intent in enumerate(intents): + history = control_plane.snapshot.participant_control_history.get(_PARTICIPANT, []) + if isinstance(intent, ParticipantExternalDirectionControlIntent): + intent = intent.model_copy(update={"target_ref": history[0]["event_id"]}) + elif isinstance(intent, ParticipantInterventionControlIntent): + intent = intent.model_copy( + update={ + "affected_occurrence_ref": history[-1]["event_id"], + "affected_revision": history[-1]["occurrence"]["occurrence_revision"], + } + ) + receipts.append( + control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(), + idempotency_key=f"key-{index}", + ) + ) + + history = control_plane.snapshot.participant_control_history[_PARTICIPANT] + assert [receipt.accepted for receipt in receipts] == [True] * len(receipts) + assert [event["occurrence"]["kind"] for event in history] == [ + kind.value + for kind in ( + MixedControlTransitionKind.PROPOSAL, + MixedControlTransitionKind.APPROVAL, + MixedControlTransitionKind.DENIAL, + MixedControlTransitionKind.EXTERNAL_DIRECTION, + MixedControlTransitionKind.INTERVENTION, + MixedControlTransitionKind.HANDOFF, + MixedControlTransitionKind.OVERRIDE, + MixedControlTransitionKind.CANCELLATION, + ) + ] + assert all(event["occurrence"]["disposition"] == "accepted" for event in history) + + +def test_supervisory_control_is_subject_bound_idempotent_and_state_revision_bound() -> None: + control_plane = RuntimeControlPlane( + create_stub_target(), + behavior_specifications={_SPEC_ADDRESS: _compiled_specification()}, + ) + intent = ParticipantProposalControlIntent( + **_base_intent_fields("proposal", 0), + proposal_id="proposal-1", + proposal_revision=1, + action_contract_ref="action-contract:contain-host", + payload_ref="payload:proposal-1", + ) + + with pytest.raises(PermissionError, match="subject"): + control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(bound=False), + idempotency_key="key-1", + ) + assert not control_plane.snapshot.participant_control_history + with pytest.raises(PermissionError, match="target"): + control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=replace(_identity(), target_name="other-target"), + idempotency_key="key-target", + ) + assert not control_plane.snapshot.participant_control_history + + first = control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(), + idempotency_key="key-1", + ) + retry = control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(), + idempotency_key="key-1", + ) + assert retry.operation_id == first.operation_id + assert len(control_plane.snapshot.participant_control_history[_PARTICIPANT]) == 1 + + changed = intent.model_copy(update={"proposal_id": "proposal-2"}) + with pytest.raises(ValueError, match="different semantics"): + control_plane.record_participant_control( + _PARTICIPANT, + changed, + identity=_identity(), + idempotency_key="key-1", + ) + + stale = ParticipantApprovalControlIntent( + **_base_intent_fields("approval", 0), + proposal_ref="proposal-1", + proposal_revision=1, + decision_ref="decision-stale-1", + decision_revision=1, + ) + rejected = control_plane.record_participant_control( + _PARTICIPANT, + stale, + identity=_identity(), + idempotency_key="key-stale", + ) + assert rejected.accepted is False + assert ( + control_plane.snapshot.participant_control_history[_PARTICIPANT][-1]["occurrence"]["reason_code"] + == "stale-state" + ) + + +@pytest.mark.parametrize( + ("failure", "reason_code"), + [ + ("stale-policy", "stale-policy"), + ("revoked-authority", "revoked-authority"), + ("late-authority", "late-authority"), + ("unsupported-order", "unsupported-order-strategy"), + ], +) +def test_supervisory_control_records_bounded_policy_and_authority_rejections( + failure: str, + reason_code: str, +) -> None: + specification = _compiled_specification() + intent_fields = _base_intent_fields("proposal", 0) + if failure == "stale-policy": + intent_fields["policy_revision"] = "2.0.0" + elif failure == "revoked-authority": + state = replace(specification.controller_states[0], authority_status="revoked") + specification = replace( + specification, + controller_states=(state, *specification.controller_states[1:]), + ) + elif failure == "late-authority": + state = replace(specification.controller_states[0], valid_until_order=0) + specification = replace( + specification, + controller_states=(state, *specification.controller_states[1:]), + ) + else: + specification = replace( + specification, + mixed_control_order_strategy="causal-partial-order", + ) + control_plane = RuntimeControlPlane( + create_stub_target(), + behavior_specifications={_SPEC_ADDRESS: specification}, + ) + intent = ParticipantProposalControlIntent( + **intent_fields, + proposal_id="proposal-1", + proposal_revision=1, + action_contract_ref="action-contract:contain-host", + payload_ref="payload:proposal-1", + ) + + receipt = control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(), + idempotency_key=f"key-{failure}", + ) + + assert receipt.accepted is False + event = control_plane.snapshot.participant_control_history[_PARTICIPANT][-1] + assert event["occurrence"]["reason_code"] == reason_code + assert "2.0.0" not in str(receipt.diagnostics) + + +def test_supervisory_control_restarts_and_replays_before_the_next_transition( + tmp_path: Path, +) -> None: + store = LocalControlPlaneStore(tmp_path / "control-plane") + specification = _compiled_specification() + first = RuntimeControlPlane( + create_stub_target(), + store=store, + behavior_specifications={_SPEC_ADDRESS: specification}, + ) + proposal = ParticipantProposalControlIntent( + **_base_intent_fields("proposal", 0), + proposal_id="proposal-1", + proposal_revision=1, + action_contract_ref="action-contract:contain-host", + payload_ref="payload:proposal-1", + ) + assert first.record_participant_control( + _PARTICIPANT, + proposal, + identity=_identity(), + idempotency_key="key-proposal", + ).accepted + + restarted = RuntimeControlPlane( + create_stub_target(), + store=LocalControlPlaneStore(tmp_path / "control-plane"), + behavior_specifications={_SPEC_ADDRESS: specification}, + ) + approval = ParticipantApprovalControlIntent( + **_base_intent_fields("approval", 1), + proposal_ref="proposal-1", + proposal_revision=1, + decision_ref="decision-approval-1", + decision_revision=1, + ) + + assert restarted.record_participant_control( + _PARTICIPANT, + approval, + identity=_identity(), + idempotency_key="key-approval", + ).accepted + assert len(restarted.snapshot.participant_control_history[_PARTICIPANT]) == 2 + + +def test_controller_state_replay_is_scoped_to_one_episode() -> None: + control_plane = RuntimeControlPlane( + create_stub_target(), + behavior_specifications={_SPEC_ADDRESS: _single_transition_specification(MixedControlTransitionKind.PROPOSAL)}, + ) + first = ParticipantProposalControlIntent( + **_base_intent_fields("proposal", 0), + proposal_id="proposal-episode-1", + proposal_revision=1, + action_contract_ref="action-contract:contain-host", + payload_ref="payload:proposal-episode-1", + ) + second = first.model_copy( + update={ + "episode_id": "episode-2", + "proposal_id": "proposal-episode-2", + "payload_ref": "payload:proposal-episode-2", + } + ) + + assert control_plane.record_participant_control( + _PARTICIPANT, + first, + identity=_identity(), + idempotency_key="key-episode-1", + ).accepted + assert control_plane.record_participant_control( + _PARTICIPANT, + second, + identity=_identity(), + idempotency_key="key-episode-2", + ).accepted + + +def _behavior_target_snapshot() -> RuntimeSnapshot: + common = { + "event_type": "action_attempted", + "timestamp": "2026-07-26T10:00:00Z", + "participant_address": _PARTICIPANT, + "episode_id": "episode-1", + "action_instance_id": "action-1", + "action_contract_address": "participant.action-contract.contain-host", + "actor_provenance": "participant:red-agent", + "details": {}, + } + return RuntimeSnapshot( + participant_behavior_history={ + _PARTICIPANT: [ + { + **common, + "lifecycle_phase": "intent_or_proposal", + "phase_realization": "runtime_mediated", + }, + { + **common, + "lifecycle_phase": "selection_or_admission", + "phase_realization": "runtime_mediated", + "admission_disposition": "admitted", + }, + { + **common, + "lifecycle_phase": "execution_attempt", + "phase_realization": "runtime_mediated", + "operation_ref": "attempt-1", + "operation_state": "running", + }, + ] + } + ) + + +@pytest.mark.parametrize( + ("kind", "target_kind", "target_ref"), + [ + (MixedControlTransitionKind.EXTERNAL_DIRECTION, "action", "action-1"), + (MixedControlTransitionKind.INTERVENTION, "attempt", "attempt-1"), + (MixedControlTransitionKind.CANCELLATION, "admitted-action", "action-1"), + ], +) +def test_typed_targets_resolve_authoritative_behavior_lifecycle_stages( + kind: MixedControlTransitionKind, + target_kind: str, + target_ref: str, +) -> None: + control_plane = RuntimeControlPlane( + create_stub_target(), + initial_snapshot=_behavior_target_snapshot(), + behavior_specifications={_SPEC_ADDRESS: _single_transition_specification(kind)}, + ) + fields = _base_intent_fields(kind.value, 0) + if kind is MixedControlTransitionKind.EXTERNAL_DIRECTION: + intent = ParticipantExternalDirectionControlIntent( + **fields, + target_kind=target_kind, + target_ref=target_ref, + target_revision=1, + ) + elif kind is MixedControlTransitionKind.INTERVENTION: + intent = ParticipantInterventionControlIntent( + **fields, + affected_target_kind=target_kind, + affected_occurrence_ref=target_ref, + affected_revision=1, + intervention_ref="intervention-1", + ) + else: + intent = ParticipantCancellationControlIntent( + **fields, + target_kind=target_kind, + target_ref=target_ref, + target_revision=1, + ) + + receipt = control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(), + idempotency_key=f"key-{kind.value}", + ) + + assert receipt.accepted + event = control_plane.snapshot.participant_control_history[_PARTICIPANT][-1] + assert event["predecessor_event_refs"] == [target_ref] + + +def test_unresolved_typed_target_appends_a_bounded_rejection_without_fallback() -> None: + control_plane = RuntimeControlPlane( + create_stub_target(), + behavior_specifications={ + _SPEC_ADDRESS: _single_transition_specification(MixedControlTransitionKind.EXTERNAL_DIRECTION) + }, + ) + intent = ParticipantExternalDirectionControlIntent( + **_base_intent_fields("external-direction", 0), + target_kind="action", + target_ref="action-missing", + target_revision=1, + ) + + receipt = control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(), + idempotency_key="key-missing-target", + ) + + assert receipt.accepted is False + event = control_plane.snapshot.participant_control_history[_PARTICIPANT][-1] + assert event["occurrence"]["reason_code"] == "invalid-target" + assert event["predecessor_event_refs"] == [] + + +def test_failed_atomic_control_commit_exposes_no_partial_transition( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + store = LocalControlPlaneStore(tmp_path / "control-plane") + control_plane = RuntimeControlPlane( + create_stub_target(), + store=store, + behavior_specifications={_SPEC_ADDRESS: _compiled_specification()}, + ) + intent = ParticipantProposalControlIntent( + **_base_intent_fields("proposal", 0), + proposal_id="proposal-1", + proposal_revision=1, + action_contract_ref="action-contract:contain-host", + payload_ref="payload:proposal-1", + ) + + def fail_atomic_write(path: Path, content: str) -> None: + del path, content + raise OSError("commit failed") + + monkeypatch.setattr(store, "_atomic_write", fail_atomic_write) + with pytest.raises(OSError, match="commit failed"): + control_plane.record_participant_control( + _PARTICIPANT, + intent, + identity=_identity(), + idempotency_key="key-1", + ) + + assert not control_plane.snapshot.participant_control_history + restarted = LocalControlPlaneStore(tmp_path / "control-plane") + assert not restarted.load_snapshot().participant_control_history + assert not restarted.load_records() + assert not restarted.read_audit() + + +def _api_security(*, bound: bool = True) -> ControlPlaneSecurityConfig: + return ControlPlaneSecurityConfig( + bearer_tokens={ + "operator-token": _identity(bound=bound), + } + ) + + +def _proposal_body() -> dict[str, object]: + return { + "kind": "proposal", + **_base_intent_fields("proposal", 0), + "proposal_id": "proposal-1", + "proposal_revision": 1, + "action_contract_ref": "action-contract:contain-host", + "payload_ref": "payload:proposal-1", + } + + +def test_supervisory_http_route_is_closed_subject_bound_and_idempotent() -> None: + control_plane = RuntimeControlPlane( + create_stub_target(), + behavior_specifications={_SPEC_ADDRESS: _compiled_specification()}, + ) + app = create_control_plane_app(control_plane, security=_api_security()) + headers = { + "authorization": "Bearer operator-token", + "idempotency-key": "key-1", + } + + with TestClient(app) as client: + first = client.post( + f"/participants/{_PARTICIPANT}/control-occurrences", + json=_proposal_body(), + headers=headers, + ) + retry = client.post( + f"/participants/{_PARTICIPANT}/control-occurrences", + json=_proposal_body(), + headers=headers, + ) + smuggled = client.post( + f"/participants/{_PARTICIPANT}/control-occurrences", + json={**_proposal_body(), "disposition": "accepted"}, + headers={**headers, "idempotency-key": "key-2"}, + ) + invalid_target = client.post( + f"/participants/{_PARTICIPANT}/control-occurrences", + json={ + "kind": "external-direction", + **_base_intent_fields("external-direction", 1), + "target_kind": "decision", + "target_ref": "decision-1", + "target_revision": 1, + }, + headers={**headers, "idempotency-key": "key-3"}, + ) + + assert first.status_code == 200 + assert retry.status_code == 200 + assert retry.json()["operation_id"] == first.json()["operation_id"] + assert smuggled.status_code == 422 + assert invalid_target.status_code == 422 + assert len(control_plane.snapshot.participant_control_history[_PARTICIPANT]) == 1 + + +def test_supervisory_http_route_rejects_unbound_subject_without_occurrence() -> None: + control_plane = RuntimeControlPlane( + create_stub_target(), + behavior_specifications={_SPEC_ADDRESS: _compiled_specification()}, + ) + app = create_control_plane_app(control_plane, security=_api_security(bound=False)) + + with TestClient(app) as client: + known = client.post( + f"/participants/{_PARTICIPANT}/control-occurrences", + json=_proposal_body(), + headers={"authorization": "Bearer operator-token"}, + ) + unknown = client.post( + "/participants/participant.behavior.unknown/control-occurrences", + json=_proposal_body(), + headers={"authorization": "Bearer operator-token"}, + ) + + assert known.status_code == 403 + assert unknown.status_code == 403 + assert known.json() == unknown.json() == {"detail": "forbidden"} + assert not control_plane.snapshot.participant_control_history diff --git a/implementations/python/tests/test_sem_227_shared_time_model.py b/implementations/python/tests/test_sem_227_shared_time_model.py index dfac8a611..2b63b0a1b 100644 --- a/implementations/python/tests/test_sem_227_shared_time_model.py +++ b/implementations/python/tests/test_sem_227_shared_time_model.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest -from hypothesis import given +from hypothesis import given, settings from hypothesis import strategies as st from raes._errors import SDLParseError, SDLValidationError from raes.parser import parse_sdl, parse_sdl_file @@ -237,6 +237,7 @@ def test_shared_time_model_references_follow_module_namespacing(tmp_path: Path) assert constraint.subject_refs == ["nodes.shared.workstation"] +@settings(deadline=None) @given(st.lists(st.sampled_from(("advance", "reset")), min_size=1, max_size=30)) def test_clock_transition_history_is_append_only_across_generated_lifecycles( operations: list[str], From 31d89180563ff170ddcecf80ce3e9aad707d5ecc Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 06:28:49 +0200 Subject: [PATCH 03/55] Add participant crossing policy contracts --- .ground-control.yaml | 1 - Makefile | 4 + .../controlled-vocabularies-v1.json | 395 +++ .../valid/reference.json | 395 +++ .../invalid/policy-bag.json | 67 + .../valid/request.json | 65 + .../participant-crossing-occurrence-v1.json | 10 + .../participant-crossing-occurrence-v1.json | 2202 +++++++++++++++++ ...articipant-crossing-contracts-preflight.md | 312 +++ docs/explain/sdl/lineage.md | 34 + .../conformance/validators.py | 2 + .../raes_contracts/contracts/__init__.py | 10 +- .../packages/raes_contracts/contracts/base.py | 9 + .../raes_contracts/contracts/bundle.py | 2 + .../contracts/participant_crossing.py | 422 ++++ .../participant_crossing_validation.py | 421 ++++ .../contracts/participant_crossing_vocab.py | 121 + .../contracts/participant_occurrences.py | 14 + ..._api_423_participant_crossing_contracts.py | 962 +++++++ .../test_participant_backend_contracts.py | 2 + .../tests/test_sem_227_shared_time_model.py | 3 +- tools/generate_contract_schemas.py | 1 + 22 files changed, 5447 insertions(+), 7 deletions(-) create mode 100644 Makefile create mode 100644 contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/invalid/policy-bag.json create mode 100644 contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/valid/request.json create mode 100644 contracts/schema-publication/entries/participant-crossing-occurrence-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json create mode 100644 docs/decisions/issue-798-api-423-participant-crossing-contracts-preflight.md create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_crossing.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_crossing_vocab.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_occurrences.py create mode 100644 implementations/python/tests/test_api_423_participant_crossing_contracts.py diff --git a/.ground-control.yaml b/.ground-control.yaml index 35764b894..eb852fb8f 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -25,7 +25,6 @@ requirements: routing: enabled: true default_provider: claude - default_fallback: parent stages: {} sonarcloud: project_key: Brad-Edwards_aces diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..fa33532a4 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: policy + +policy: + uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s policy diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index dda0d30bb..24ca22534 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -1001,6 +1001,401 @@ } } }, + "participant-crossing-directions": { + "title": "Participant Crossing Directions", + "description": "Closed directions for participant-boundary crossing facts.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.direction" + ], + "extension_policy": "closed", + "terms": { + "egress": { + "title": "Egress", + "description": "A governed fact crossing toward a participant-facing audience." + }, + "ingress": { + "title": "Ingress", + "description": "A governed fact crossing from a participant-facing source toward runtime control." + } + } + }, + "participant-crossing-interaction-kinds": { + "title": "Participant Crossing Interaction Kinds", + "description": "Closed incumbent carrier meanings that may participate in a governed crossing.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.interaction_kind" + ], + "extension_policy": "closed", + "terms": { + "action-proposal": { + "title": "Action Proposal", + "description": "An action proposal crossing fact." + }, + "constrained-form-submission": { + "title": "Constrained Form Submission", + "description": "A constrained decision-surface form submission." + }, + "candidate-selection": { + "title": "Candidate Selection", + "description": "A candidate selected from a governed decision surface." + }, + "approval": { + "title": "Approval", + "description": "An approval occurrence that remains distinct from admission." + }, + "denial": { + "title": "Denial", + "description": "A denial occurrence." + }, + "external-direction": { + "title": "External Direction", + "description": "An externally directed control occurrence." + }, + "intervention": { + "title": "Intervention", + "description": "An intervention occurrence." + }, + "handoff": { + "title": "Handoff", + "description": "A controller handoff occurrence." + }, + "override": { + "title": "Override", + "description": "A non-retroactive override occurrence." + }, + "cancellation": { + "title": "Cancellation", + "description": "A non-retroactive cancellation occurrence." + }, + "participant-inject-delivery": { + "title": "Participant Inject Delivery", + "description": "A participant-directed inject delivery binding." + }, + "observation": { + "title": "Observation", + "description": "A participant observation carrier." + }, + "decision-surface-projection": { + "title": "Decision Surface Projection", + "description": "A participant decision-surface projection." + }, + "redacted-output": { + "title": "Redacted Output", + "description": "A redacted participant-facing result with distinct source identity." + }, + "disclosure": { + "title": "Disclosure", + "description": "An authorized disclosure fact that is not delivery." + }, + "delivery-receipt": { + "title": "Delivery Receipt", + "description": "A delivery occurrence or receipt that is not observation." + }, + "action-result": { + "title": "Action Result", + "description": "An incumbent participant action-result carrier." + }, + "status-projection": { + "title": "Status Projection", + "description": "A bounded participant status projection." + }, + "history-projection": { + "title": "History Projection", + "description": "A bounded participant history projection." + } + } + }, + "participant-crossing-subject-kinds": { + "title": "Participant Crossing Subject Kinds", + "description": "Closed typed subject references to incumbent participant and evidence carriers.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.subject.subject_kind" + ], + "extension_policy": "closed", + "terms": { + "participant-control-occurrence": { + "title": "Participant Control Occurrence", + "description": "An API-409 control occurrence." + }, + "participant-action-contract": { + "title": "Participant Action Contract", + "description": "A SEM-211 participant action contract." + }, + "participant-action-admission": { + "title": "Participant Action Admission", + "description": "A participant action admission decision." + }, + "participant-action-attempt": { + "title": "Participant Action Attempt", + "description": "A participant action attempt fact." + }, + "participant-action-result": { + "title": "Participant Action Result", + "description": "A participant action-result fact." + }, + "participant-lifecycle-event": { + "title": "Participant Lifecycle Event", + "description": "An API-406 participant lifecycle event." + }, + "participant-observation": { + "title": "Participant Observation", + "description": "An API-406 participant observation." + }, + "participant-decision-surface": { + "title": "Participant Decision Surface", + "description": "A SEM-220 participant decision surface." + }, + "participant-exposure": { + "title": "Participant Exposure", + "description": "A SEM-226 exposure authorization or realization." + }, + "participant-inject-delivery": { + "title": "Participant Inject Delivery", + "description": "A DSL-142 participant inject delivery binding." + }, + "participant-context-view": { + "title": "Participant Context View", + "description": "An API-406 participant context view." + }, + "participant-history-view": { + "title": "Participant History View", + "description": "An API-406 participant history view." + }, + "participant-status-view": { + "title": "Participant Status View", + "description": "An API-406 participant status view." + }, + "experiment-evidence": { + "title": "Experiment Evidence", + "description": "An experiment evidence record used only by reference." + } + } + }, + "participant-crossing-operations": { + "title": "Participant Crossing Operations", + "description": "Closed information-flow operations whose semantic effects remain independent.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.operation" + ], + "extension_policy": "closed", + "terms": { + "admission": { + "title": "Admission", + "description": "Decide whether one crossing may proceed at the current order point." + }, + "withholding": { + "title": "Withholding", + "description": "Record intentional non-release." + }, + "projection": { + "title": "Projection", + "description": "Select a participant-relative view without granting authority." + }, + "masking": { + "title": "Masking", + "description": "Mask an authorized representation without declassifying it." + }, + "redaction": { + "title": "Redaction", + "description": "Transform an authorized representation without granting access." + }, + "transformation": { + "title": "Transformation", + "description": "Create a new typed subject identity under a governed rule." + }, + "declassification": { + "title": "Declassification", + "description": "Change release authority under an explicit governed basis." + }, + "disclosure": { + "title": "Disclosure", + "description": "Record an authorized release decision without implying delivery." + }, + "delivery": { + "title": "Delivery", + "description": "Record participant-facing delivery without implying observation." + }, + "concealment": { + "title": "Concealment", + "description": "Change future projection without erasing prior knowledge." + }, + "revocation": { + "title": "Revocation", + "description": "Withdraw future authority without retroactive effect." + }, + "audit-retention": { + "title": "Audit Retention", + "description": "Retain a crossing fact for an authorized audit audience." + } + } + }, + "participant-crossing-gate-dispositions": { + "title": "Participant Crossing Gate Dispositions", + "description": "Closed results for independently evaluated deny-first crossing gates.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.gates" + ], + "extension_policy": "closed", + "terms": { + "permit": { + "title": "Permit", + "description": "The owning gate permits the exact crossing coordinates." + }, + "deny": { + "title": "Deny", + "description": "The owning gate denies the exact crossing coordinates." + }, + "not-applicable": { + "title": "Not Applicable", + "description": "The owning gate factually does not apply." + }, + "unknown": { + "title": "Unknown", + "description": "The required gate result is unresolved and therefore not permission." + }, + "unsupported": { + "title": "Unsupported", + "description": "The owning gate cannot evaluate or realize the required semantics." + } + } + }, + "participant-crossing-decision-dispositions": { + "title": "Participant Crossing Decision Dispositions", + "description": "Closed overall outcomes of deny-first crossing policy composition.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.decision.disposition" + ], + "extension_policy": "closed", + "terms": { + "permit": { + "title": "Permit", + "description": "All applicable required gates permit the untransformed crossing." + }, + "deny": { + "title": "Deny", + "description": "At least one required gate denies the crossing." + }, + "transform": { + "title": "Transform", + "description": "The crossing requires a named non-mutating transformation before realization." + }, + "withhold": { + "title": "Withhold", + "description": "The fact is intentionally not released." + }, + "unsupported": { + "title": "Unsupported", + "description": "One or more required coordinates cannot be supported or resolved." + } + } + }, + "participant-crossing-backend-postures": { + "title": "Participant Crossing Backend Postures", + "description": "Closed support postures that do not imply authorization or realization.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.backend_posture" + ], + "extension_policy": "closed", + "terms": { + "exact": { + "title": "Exact", + "description": "The declared portable semantics are supported exactly within the named scope." + }, + "bounded": { + "title": "Bounded", + "description": "Support is bounded by explicit limitations." + }, + "disclosed-weak": { + "title": "Disclosed Weak", + "description": "Only a disclosed weaker posture is available." + }, + "unsupported": { + "title": "Unsupported", + "description": "The required portable semantics are unsupported." + } + } + }, + "participant-crossing-loss-kinds": { + "title": "Participant Crossing Loss Kinds", + "description": "Closed loss and weakening disclosures that never count as successful enforcement.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.losses.kind" + ], + "extension_policy": "closed", + "terms": { + "none": { + "title": "None", + "description": "No declared fidelity loss or guarantee weakening." + }, + "fidelity-loss": { + "title": "Fidelity Loss", + "description": "Some source fidelity is unavailable in the result." + }, + "guarantee-weakening": { + "title": "Guarantee Weakening", + "description": "A stronger guarantee is unavailable and the weaker posture is explicit." + }, + "unknown": { + "title": "Unknown", + "description": "The loss or weakening state is unresolved." + }, + "unsupported": { + "title": "Unsupported", + "description": "The requested fidelity or guarantee cannot be supported." + } + } + }, + "participant-crossing-stages": { + "title": "Participant Crossing Stages", + "description": "Closed independently addressable API-423 fact stages.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.stage" + ], + "extension_policy": "closed", + "terms": { + "requested": { + "title": "Requested", + "description": "A requested crossing or produced candidate, not a decision." + }, + "decided": { + "title": "Decided", + "description": "A policy decision at one exact order point." + }, + "transformed": { + "title": "Transformed", + "description": "A non-mutating transformation into a new typed subject." + }, + "disclosed": { + "title": "Disclosed", + "description": "An authorized disclosure or declassification, not delivery." + }, + "delivery-attempted": { + "title": "Delivery Attempted", + "description": "An attempted delivery, distinct from scheduling and success." + }, + "delivered": { + "title": "Delivered", + "description": "A delivered occurrence, distinct from participant observation." + }, + "observed": { + "title": "Observed", + "description": "An observation fact backed by an incumbent observation carrier." + }, + "audited": { + "title": "Audited", + "description": "Audit or evidence retention for a prior crossing fact." + } + } + }, "observation-capture-kinds": { "title": "Observation Capture Kinds", "description": "Backend-supported evidence capture categories for experiment observation capability declarations.", 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 dda0d30bb..24ca22534 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -1001,6 +1001,401 @@ } } }, + "participant-crossing-directions": { + "title": "Participant Crossing Directions", + "description": "Closed directions for participant-boundary crossing facts.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.direction" + ], + "extension_policy": "closed", + "terms": { + "egress": { + "title": "Egress", + "description": "A governed fact crossing toward a participant-facing audience." + }, + "ingress": { + "title": "Ingress", + "description": "A governed fact crossing from a participant-facing source toward runtime control." + } + } + }, + "participant-crossing-interaction-kinds": { + "title": "Participant Crossing Interaction Kinds", + "description": "Closed incumbent carrier meanings that may participate in a governed crossing.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.interaction_kind" + ], + "extension_policy": "closed", + "terms": { + "action-proposal": { + "title": "Action Proposal", + "description": "An action proposal crossing fact." + }, + "constrained-form-submission": { + "title": "Constrained Form Submission", + "description": "A constrained decision-surface form submission." + }, + "candidate-selection": { + "title": "Candidate Selection", + "description": "A candidate selected from a governed decision surface." + }, + "approval": { + "title": "Approval", + "description": "An approval occurrence that remains distinct from admission." + }, + "denial": { + "title": "Denial", + "description": "A denial occurrence." + }, + "external-direction": { + "title": "External Direction", + "description": "An externally directed control occurrence." + }, + "intervention": { + "title": "Intervention", + "description": "An intervention occurrence." + }, + "handoff": { + "title": "Handoff", + "description": "A controller handoff occurrence." + }, + "override": { + "title": "Override", + "description": "A non-retroactive override occurrence." + }, + "cancellation": { + "title": "Cancellation", + "description": "A non-retroactive cancellation occurrence." + }, + "participant-inject-delivery": { + "title": "Participant Inject Delivery", + "description": "A participant-directed inject delivery binding." + }, + "observation": { + "title": "Observation", + "description": "A participant observation carrier." + }, + "decision-surface-projection": { + "title": "Decision Surface Projection", + "description": "A participant decision-surface projection." + }, + "redacted-output": { + "title": "Redacted Output", + "description": "A redacted participant-facing result with distinct source identity." + }, + "disclosure": { + "title": "Disclosure", + "description": "An authorized disclosure fact that is not delivery." + }, + "delivery-receipt": { + "title": "Delivery Receipt", + "description": "A delivery occurrence or receipt that is not observation." + }, + "action-result": { + "title": "Action Result", + "description": "An incumbent participant action-result carrier." + }, + "status-projection": { + "title": "Status Projection", + "description": "A bounded participant status projection." + }, + "history-projection": { + "title": "History Projection", + "description": "A bounded participant history projection." + } + } + }, + "participant-crossing-subject-kinds": { + "title": "Participant Crossing Subject Kinds", + "description": "Closed typed subject references to incumbent participant and evidence carriers.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.subject.subject_kind" + ], + "extension_policy": "closed", + "terms": { + "participant-control-occurrence": { + "title": "Participant Control Occurrence", + "description": "An API-409 control occurrence." + }, + "participant-action-contract": { + "title": "Participant Action Contract", + "description": "A SEM-211 participant action contract." + }, + "participant-action-admission": { + "title": "Participant Action Admission", + "description": "A participant action admission decision." + }, + "participant-action-attempt": { + "title": "Participant Action Attempt", + "description": "A participant action attempt fact." + }, + "participant-action-result": { + "title": "Participant Action Result", + "description": "A participant action-result fact." + }, + "participant-lifecycle-event": { + "title": "Participant Lifecycle Event", + "description": "An API-406 participant lifecycle event." + }, + "participant-observation": { + "title": "Participant Observation", + "description": "An API-406 participant observation." + }, + "participant-decision-surface": { + "title": "Participant Decision Surface", + "description": "A SEM-220 participant decision surface." + }, + "participant-exposure": { + "title": "Participant Exposure", + "description": "A SEM-226 exposure authorization or realization." + }, + "participant-inject-delivery": { + "title": "Participant Inject Delivery", + "description": "A DSL-142 participant inject delivery binding." + }, + "participant-context-view": { + "title": "Participant Context View", + "description": "An API-406 participant context view." + }, + "participant-history-view": { + "title": "Participant History View", + "description": "An API-406 participant history view." + }, + "participant-status-view": { + "title": "Participant Status View", + "description": "An API-406 participant status view." + }, + "experiment-evidence": { + "title": "Experiment Evidence", + "description": "An experiment evidence record used only by reference." + } + } + }, + "participant-crossing-operations": { + "title": "Participant Crossing Operations", + "description": "Closed information-flow operations whose semantic effects remain independent.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.operation" + ], + "extension_policy": "closed", + "terms": { + "admission": { + "title": "Admission", + "description": "Decide whether one crossing may proceed at the current order point." + }, + "withholding": { + "title": "Withholding", + "description": "Record intentional non-release." + }, + "projection": { + "title": "Projection", + "description": "Select a participant-relative view without granting authority." + }, + "masking": { + "title": "Masking", + "description": "Mask an authorized representation without declassifying it." + }, + "redaction": { + "title": "Redaction", + "description": "Transform an authorized representation without granting access." + }, + "transformation": { + "title": "Transformation", + "description": "Create a new typed subject identity under a governed rule." + }, + "declassification": { + "title": "Declassification", + "description": "Change release authority under an explicit governed basis." + }, + "disclosure": { + "title": "Disclosure", + "description": "Record an authorized release decision without implying delivery." + }, + "delivery": { + "title": "Delivery", + "description": "Record participant-facing delivery without implying observation." + }, + "concealment": { + "title": "Concealment", + "description": "Change future projection without erasing prior knowledge." + }, + "revocation": { + "title": "Revocation", + "description": "Withdraw future authority without retroactive effect." + }, + "audit-retention": { + "title": "Audit Retention", + "description": "Retain a crossing fact for an authorized audit audience." + } + } + }, + "participant-crossing-gate-dispositions": { + "title": "Participant Crossing Gate Dispositions", + "description": "Closed results for independently evaluated deny-first crossing gates.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.gates" + ], + "extension_policy": "closed", + "terms": { + "permit": { + "title": "Permit", + "description": "The owning gate permits the exact crossing coordinates." + }, + "deny": { + "title": "Deny", + "description": "The owning gate denies the exact crossing coordinates." + }, + "not-applicable": { + "title": "Not Applicable", + "description": "The owning gate factually does not apply." + }, + "unknown": { + "title": "Unknown", + "description": "The required gate result is unresolved and therefore not permission." + }, + "unsupported": { + "title": "Unsupported", + "description": "The owning gate cannot evaluate or realize the required semantics." + } + } + }, + "participant-crossing-decision-dispositions": { + "title": "Participant Crossing Decision Dispositions", + "description": "Closed overall outcomes of deny-first crossing policy composition.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.decision.disposition" + ], + "extension_policy": "closed", + "terms": { + "permit": { + "title": "Permit", + "description": "All applicable required gates permit the untransformed crossing." + }, + "deny": { + "title": "Deny", + "description": "At least one required gate denies the crossing." + }, + "transform": { + "title": "Transform", + "description": "The crossing requires a named non-mutating transformation before realization." + }, + "withhold": { + "title": "Withhold", + "description": "The fact is intentionally not released." + }, + "unsupported": { + "title": "Unsupported", + "description": "One or more required coordinates cannot be supported or resolved." + } + } + }, + "participant-crossing-backend-postures": { + "title": "Participant Crossing Backend Postures", + "description": "Closed support postures that do not imply authorization or realization.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.backend_posture" + ], + "extension_policy": "closed", + "terms": { + "exact": { + "title": "Exact", + "description": "The declared portable semantics are supported exactly within the named scope." + }, + "bounded": { + "title": "Bounded", + "description": "Support is bounded by explicit limitations." + }, + "disclosed-weak": { + "title": "Disclosed Weak", + "description": "Only a disclosed weaker posture is available." + }, + "unsupported": { + "title": "Unsupported", + "description": "The required portable semantics are unsupported." + } + } + }, + "participant-crossing-loss-kinds": { + "title": "Participant Crossing Loss Kinds", + "description": "Closed loss and weakening disclosures that never count as successful enforcement.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.losses.kind" + ], + "extension_policy": "closed", + "terms": { + "none": { + "title": "None", + "description": "No declared fidelity loss or guarantee weakening." + }, + "fidelity-loss": { + "title": "Fidelity Loss", + "description": "Some source fidelity is unavailable in the result." + }, + "guarantee-weakening": { + "title": "Guarantee Weakening", + "description": "A stronger guarantee is unavailable and the weaker posture is explicit." + }, + "unknown": { + "title": "Unknown", + "description": "The loss or weakening state is unresolved." + }, + "unsupported": { + "title": "Unsupported", + "description": "The requested fidelity or guarantee cannot be supported." + } + } + }, + "participant-crossing-stages": { + "title": "Participant Crossing Stages", + "description": "Closed independently addressable API-423 fact stages.", + "kind": "enumeration", + "governed_scopes": [ + "participant-crossing-occurrence.stage" + ], + "extension_policy": "closed", + "terms": { + "requested": { + "title": "Requested", + "description": "A requested crossing or produced candidate, not a decision." + }, + "decided": { + "title": "Decided", + "description": "A policy decision at one exact order point." + }, + "transformed": { + "title": "Transformed", + "description": "A non-mutating transformation into a new typed subject." + }, + "disclosed": { + "title": "Disclosed", + "description": "An authorized disclosure or declassification, not delivery." + }, + "delivery-attempted": { + "title": "Delivery Attempted", + "description": "An attempted delivery, distinct from scheduling and success." + }, + "delivered": { + "title": "Delivered", + "description": "A delivered occurrence, distinct from participant observation." + }, + "observed": { + "title": "Observed", + "description": "An observation fact backed by an incumbent observation carrier." + }, + "audited": { + "title": "Audited", + "description": "Audit or evidence retention for a prior crossing fact." + } + } + }, "observation-capture-kinds": { "title": "Observation Capture Kinds", "description": "Backend-supported evidence capture categories for experiment observation capability declarations.", diff --git a/contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/invalid/policy-bag.json b/contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/invalid/policy-bag.json new file mode 100644 index 000000000..983cec285 --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/invalid/policy-bag.json @@ -0,0 +1,67 @@ +{ + "event_id": "crossing-occurrence.requested.invalid", + "schema_name": "participant-crossing-occurrence", + "schema_version": "1.0.0", + "event_type": "participant-crossing-occurrence", + "extension_policy": "closed", + "participant_address": "participants.red.operator", + "episode_id": "episode-1", + "occurred_at": "2026-07-26T08:00:00Z", + "recorded_at": "2026-07-26T08:00:01Z", + "ingested_at": "2026-07-26T08:00:02Z", + "clock_authority": "clock.logical", + "ordering_basis": "logical_clock", + "actor_ref": "controller.human.red", + "producer_ref": "participant-runtime.red", + "provenance_refs": [ + "provenance:crossing-invalid" + ], + "evidence_refs": [ + "evidence:crossing-invalid" + ], + "object_marking_refs": [ + "marking:participant-control" + ], + "authorization_scope": "scope:red-team", + "occurrence": { + "direction": "ingress", + "interaction_kind": "action-proposal", + "audience_scope_ref": "audience:red-operator", + "subject": { + "subject_kind": "participant-control-occurrence", + "contract_id": "participant-control-occurrence-v1", + "subject_ref": "control-occurrence.proposal.invalid", + "subject_revision": "1", + "participant_address": "participants.red.operator", + "episode_id": "episode-1" + }, + "controller_ref": "controller.human.red", + "authority_basis_refs": [ + "authority:red-team" + ], + "policy": { + "policy_id": "participant-crossing-policy:red", + "policy_revision": "revision-3", + "policy_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "effective_order": 8, + "valid_from_order": 8, + "valid_until_order": 20 + }, + "effective_order": 10, + "order_model": "logical_clock", + "backend_posture": "exact", + "loss_and_limitations": [ + "limitation:contract-only" + ], + "stage": "requested", + "request_id": "crossing-request.invalid", + "requested_operation": "admission", + "action_or_projection_ref": "action-contract:contain-host", + "required_evidence_refs": [ + "evidence-requirement:crossing-decision" + ], + "payload": { + "raw_secret": "must-not-cross" + } + } +} diff --git a/contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/valid/request.json b/contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/valid/request.json new file mode 100644 index 000000000..5481e1bfe --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-crossing-occurrence-v1/valid/request.json @@ -0,0 +1,65 @@ +{ + "event_id": "crossing-occurrence.requested.1", + "schema_name": "participant-crossing-occurrence", + "schema_version": "1.0.0", + "event_type": "participant-crossing-occurrence", + "extension_policy": "closed", + "participant_address": "participants.red.operator", + "episode_id": "episode-1", + "occurred_at": "2026-07-26T08:00:00Z", + "recorded_at": "2026-07-26T08:00:01Z", + "ingested_at": "2026-07-26T08:00:02Z", + "clock_authority": "clock.logical", + "ordering_basis": "logical_clock", + "logical_order_ref": "order:10", + "actor_ref": "controller.human.red", + "producer_ref": "participant-runtime.red", + "provenance_refs": [ + "provenance:crossing-1" + ], + "evidence_refs": [ + "evidence:crossing-1" + ], + "object_marking_refs": [ + "marking:participant-control" + ], + "authorization_scope": "scope:red-team", + "occurrence": { + "direction": "ingress", + "interaction_kind": "action-proposal", + "audience_scope_ref": "audience:red-operator", + "subject": { + "subject_kind": "participant-control-occurrence", + "contract_id": "participant-control-occurrence-v1", + "subject_ref": "control-occurrence.proposal.1", + "subject_revision": "1", + "participant_address": "participants.red.operator", + "episode_id": "episode-1" + }, + "controller_ref": "controller.human.red", + "authority_basis_refs": [ + "authority:red-team" + ], + "policy": { + "policy_id": "participant-crossing-policy:red", + "policy_revision": "revision-3", + "policy_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "effective_order": 8, + "valid_from_order": 8, + "valid_until_order": 20 + }, + "effective_order": 10, + "order_model": "logical_clock", + "backend_posture": "exact", + "loss_and_limitations": [ + "limitation:contract-only" + ], + "stage": "requested", + "request_id": "crossing-request.1", + "requested_operation": "admission", + "action_or_projection_ref": "action-contract:contain-host", + "required_evidence_refs": [ + "evidence-requirement:crossing-decision" + ] + } +} diff --git a/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json b/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json new file mode 100644 index 000000000..e766e97c3 --- /dev/null +++ b/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-crossing-occurrence-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json", + "stability": "draft", + "content_hash": "392ab485349bef35463a2fd0e843133a8adff2614ffb0ca456c880d7c1d4f0d4", + "last_change": { + "summary": "Published the API-423 closed participant-crossing policy, transformation, disclosure, realization, evidence, and provenance occurrence family.", + "content_hash": "392ab485349bef35463a2fd0e843133a8adff2614ffb0ca456c880d7c1d4f0d4" + } +} diff --git a/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json b/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json new file mode 100644 index 000000000..35d6100e0 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json @@ -0,0 +1,2202 @@ +{ + "$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" + }, + "ParticipantCrossingAuditModel": { + "additionalProperties": false, + "description": "Audit/evidence retention for a prior crossing fact, not participant egress.", + "properties": { + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "audit_record_ref": { + "minLength": 1, + "title": "Audit Record Ref", + "type": "string" + }, + "audited_event_ref": { + "minLength": 1, + "title": "Audited Event Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "retained_evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Retained Evidence Refs", + "type": "array" + }, + "stage": { + "const": "audited", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "audited_event_ref", + "audit_record_ref", + "retained_evidence_refs" + ], + "title": "ParticipantCrossingAuditModel", + "type": "object" + }, + "ParticipantCrossingBackendPosture": { + "description": "Bounded backend-support posture without a realization claim.", + "enum": [ + "exact", + "bounded", + "disclosed-weak", + "unsupported" + ], + "title": "ParticipantCrossingBackendPosture", + "type": "string" + }, + "ParticipantCrossingDecisionDisposition": { + "description": "Overall policy disposition for one crossing request.", + "enum": [ + "permit", + "deny", + "transform", + "withhold", + "unsupported" + ], + "title": "ParticipantCrossingDecisionDisposition", + "type": "string" + }, + "ParticipantCrossingDecisionGatesModel": { + "additionalProperties": false, + "description": "Independent deny-first gates for one crossing decision.", + "properties": { + "action_admission": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "backend_support": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "caller_authorization": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "declassification": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "marking_authorization": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "participant_authority": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "target_authorization": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "transformation_validity": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + }, + "visibility": { + "$ref": "#/$defs/ParticipantCrossingGateDisposition" + } + }, + "required": [ + "caller_authorization", + "target_authorization", + "participant_authority", + "action_admission", + "visibility", + "marking_authorization", + "declassification", + "backend_support", + "transformation_validity" + ], + "title": "ParticipantCrossingDecisionGatesModel", + "type": "object" + }, + "ParticipantCrossingDecisionModel": { + "additionalProperties": false, + "description": "The deny-first policy decision for one exact crossing request.", + "properties": { + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "decision_id": { + "minLength": 1, + "title": "Decision Id", + "type": "string" + }, + "decision_revision": { + "minimum": 1, + "title": "Decision Revision", + "type": "integer" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "disposition": { + "$ref": "#/$defs/ParticipantCrossingDecisionDisposition" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "gates": { + "$ref": "#/$defs/ParticipantCrossingDecisionGatesModel" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "reason_code": { + "minLength": 1, + "title": "Reason Code", + "type": "string" + }, + "request_ref": { + "minLength": 1, + "title": "Request Ref", + "type": "string" + }, + "required_evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Required Evidence Refs", + "type": "array" + }, + "required_operation": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantCrossingOperation" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "const": "decided", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "request_ref", + "decision_id", + "decision_revision", + "gates", + "disposition", + "reason_code", + "required_evidence_refs" + ], + "title": "ParticipantCrossingDecisionModel", + "type": "object" + }, + "ParticipantCrossingDeliveryAttemptModel": { + "additionalProperties": false, + "description": "An attempted participant delivery, distinct from scheduling and success.", + "properties": { + "attempt_id": { + "minLength": 1, + "title": "Attempt Id", + "type": "string" + }, + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "decision_ref": { + "minLength": 1, + "title": "Decision Ref", + "type": "string" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "disposition": { + "enum": [ + "attempted", + "failed", + "withheld", + "unsupported" + ], + "title": "Disposition", + "type": "string" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "owning_occurrence_ref": { + "minLength": 1, + "title": "Owning Occurrence Ref", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "stage": { + "const": "delivery-attempted", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + }, + "transformation_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transformation Ref" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "decision_ref", + "attempt_id", + "owning_occurrence_ref", + "disposition" + ], + "title": "ParticipantCrossingDeliveryAttemptModel", + "type": "object" + }, + "ParticipantCrossingDeliveryModel": { + "additionalProperties": false, + "description": "A delivered participant-facing occurrence, distinct from observation.", + "properties": { + "attempt_ref": { + "minLength": 1, + "title": "Attempt Ref", + "type": "string" + }, + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "decision_ref": { + "minLength": 1, + "title": "Decision Ref", + "type": "string" + }, + "delivery_id": { + "minLength": 1, + "title": "Delivery Id", + "type": "string" + }, + "delivery_order": { + "minimum": 0, + "title": "Delivery Order", + "type": "integer" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "disposition": { + "enum": [ + "delivered", + "failed", + "unknown", + "unsupported" + ], + "title": "Disposition", + "type": "string" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "owning_occurrence_ref": { + "minLength": 1, + "title": "Owning Occurrence Ref", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "stage": { + "const": "delivered", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "decision_ref", + "attempt_ref", + "delivery_id", + "owning_occurrence_ref", + "delivery_order", + "disposition" + ], + "title": "ParticipantCrossingDeliveryModel", + "type": "object" + }, + "ParticipantCrossingDirection": { + "description": "Closed participant-boundary directions.", + "enum": [ + "ingress", + "egress" + ], + "title": "ParticipantCrossingDirection", + "type": "string" + }, + "ParticipantCrossingDisclosureModel": { + "additionalProperties": false, + "description": "An authorized disclosure or declassification decision, not delivery.", + "properties": { + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "decision_ref": { + "minLength": 1, + "title": "Decision Ref", + "type": "string" + }, + "declassification_basis_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declassification Basis Ref" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "disclosure_id": { + "minLength": 1, + "title": "Disclosure Id", + "type": "string" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "operation": { + "enum": [ + "disclosure", + "declassification" + ], + "title": "Operation", + "type": "string" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "result_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Result Marking Refs", + "type": "array" + }, + "source_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Source Marking Refs", + "type": "array" + }, + "stage": { + "const": "disclosed", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + }, + "transformation_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transformation Ref" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "decision_ref", + "disclosure_id", + "operation", + "source_marking_refs", + "result_marking_refs" + ], + "title": "ParticipantCrossingDisclosureModel", + "type": "object" + }, + "ParticipantCrossingGateDisposition": { + "description": "One deny-first decision-gate result.", + "enum": [ + "permit", + "deny", + "not-applicable", + "unknown", + "unsupported" + ], + "title": "ParticipantCrossingGateDisposition", + "type": "string" + }, + "ParticipantCrossingInteractionKind": { + "description": "Closed incumbent carrier kinds that may cross a participant boundary.", + "enum": [ + "action-proposal", + "constrained-form-submission", + "candidate-selection", + "approval", + "denial", + "external-direction", + "intervention", + "handoff", + "override", + "cancellation", + "participant-inject-delivery", + "observation", + "decision-surface-projection", + "redacted-output", + "disclosure", + "delivery-receipt", + "action-result", + "status-projection", + "history-projection" + ], + "title": "ParticipantCrossingInteractionKind", + "type": "string" + }, + "ParticipantCrossingLossKind": { + "description": "Disclosed loss or guarantee weakening for a crossing fact.", + "enum": [ + "none", + "fidelity-loss", + "guarantee-weakening", + "unknown", + "unsupported" + ], + "title": "ParticipantCrossingLossKind", + "type": "string" + }, + "ParticipantCrossingLossModel": { + "additionalProperties": false, + "description": "One explicit fidelity loss or guarantee weakening.", + "properties": { + "affected_ref": { + "minLength": 1, + "title": "Affected Ref", + "type": "string" + }, + "basis_ref": { + "minLength": 1, + "title": "Basis Ref", + "type": "string" + }, + "kind": { + "$ref": "#/$defs/ParticipantCrossingLossKind" + }, + "limitation_ref": { + "minLength": 1, + "title": "Limitation Ref", + "type": "string" + } + }, + "required": [ + "kind", + "basis_ref", + "affected_ref", + "limitation_ref" + ], + "title": "ParticipantCrossingLossModel", + "type": "object" + }, + "ParticipantCrossingObservationModel": { + "additionalProperties": false, + "description": "Participant observation of a delivered fact through an incumbent carrier.", + "properties": { + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "decision_ref": { + "minLength": 1, + "title": "Decision Ref", + "type": "string" + }, + "delivery_ref": { + "minLength": 1, + "title": "Delivery Ref", + "type": "string" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "observation_id": { + "minLength": 1, + "title": "Observation Id", + "type": "string" + }, + "observation_order": { + "minimum": 0, + "title": "Observation Order", + "type": "integer" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "owning_observation_ref": { + "minLength": 1, + "title": "Owning Observation Ref", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "stage": { + "const": "observed", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "decision_ref", + "delivery_ref", + "observation_id", + "owning_observation_ref", + "observation_order" + ], + "title": "ParticipantCrossingObservationModel", + "type": "object" + }, + "ParticipantCrossingOperation": { + "description": "Semantically independent participant information-flow operations.", + "enum": [ + "admission", + "withholding", + "projection", + "masking", + "redaction", + "transformation", + "declassification", + "disclosure", + "delivery", + "concealment", + "revocation", + "audit-retention" + ], + "title": "ParticipantCrossingOperation", + "type": "string" + }, + "ParticipantCrossingPolicyReferenceModel": { + "additionalProperties": false, + "description": "Exact revision and order interval for the policy used at a crossing.", + "properties": { + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "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" + }, + "policy_id": { + "minLength": 1, + "title": "Policy Id", + "type": "string" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "valid_from_order": { + "minimum": 0, + "title": "Valid From Order", + "type": "integer" + }, + "valid_until_order": { + "minimum": 0, + "title": "Valid Until Order", + "type": "integer" + } + }, + "required": [ + "policy_id", + "policy_revision", + "policy_digest", + "effective_order", + "valid_from_order", + "valid_until_order" + ], + "title": "ParticipantCrossingPolicyReferenceModel", + "type": "object" + }, + "ParticipantCrossingRequestModel": { + "additionalProperties": false, + "description": "A requested crossing or produced egress candidate, not a decision.", + "properties": { + "action_or_projection_ref": { + "minLength": 1, + "title": "Action Or Projection Ref", + "type": "string" + }, + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "request_id": { + "minLength": 1, + "title": "Request Id", + "type": "string" + }, + "requested_operation": { + "$ref": "#/$defs/ParticipantCrossingOperation" + }, + "required_evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Required Evidence Refs", + "type": "array" + }, + "stage": { + "const": "requested", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "request_id", + "requested_operation", + "action_or_projection_ref", + "required_evidence_refs" + ], + "title": "ParticipantCrossingRequestModel", + "type": "object" + }, + "ParticipantCrossingSubjectKind": { + "description": "Closed typed references to incumbent participant and evidence carriers.", + "enum": [ + "participant-control-occurrence", + "participant-action-contract", + "participant-action-admission", + "participant-action-attempt", + "participant-action-result", + "participant-lifecycle-event", + "participant-observation", + "participant-decision-surface", + "participant-exposure", + "participant-inject-delivery", + "participant-context-view", + "participant-history-view", + "participant-status-view", + "experiment-evidence" + ], + "title": "ParticipantCrossingSubjectKind", + "type": "string" + }, + "ParticipantCrossingSubjectReferenceModel": { + "additionalProperties": false, + "anyOf": [ + { + "properties": { + "subject_revision": { + "type": "string" + } + }, + "required": [ + "subject_revision" + ] + }, + { + "properties": { + "subject_digest": { + "type": "string" + } + }, + "required": [ + "subject_digest" + ] + } + ], + "description": "Typed identity for an existing carrier without copying its payload.", + "properties": { + "contract_id": { + "minLength": 1, + "title": "Contract Id", + "type": "string" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "subject_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": "Subject Digest" + }, + "subject_kind": { + "$ref": "#/$defs/ParticipantCrossingSubjectKind" + }, + "subject_ref": { + "minLength": 1, + "title": "Subject Ref", + "type": "string" + }, + "subject_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Subject Revision" + } + }, + "required": [ + "subject_kind", + "contract_id", + "subject_ref", + "participant_address", + "episode_id" + ], + "title": "ParticipantCrossingSubjectReferenceModel", + "type": "object" + }, + "ParticipantCrossingTransformationModel": { + "additionalProperties": false, + "description": "A non-mutating transformation from one typed subject to a new subject.", + "properties": { + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authority Basis Refs", + "type": "array" + }, + "backend_posture": { + "$ref": "#/$defs/ParticipantCrossingBackendPosture" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "decision_ref": { + "minLength": 1, + "title": "Decision Ref", + "type": "string" + }, + "declassification_basis_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declassification Basis Ref" + }, + "direction": { + "$ref": "#/$defs/ParticipantCrossingDirection" + }, + "effective_order": { + "minimum": 0, + "title": "Effective Order", + "type": "integer" + }, + "interaction_kind": { + "$ref": "#/$defs/ParticipantCrossingInteractionKind" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "losses": { + "items": { + "$ref": "#/$defs/ParticipantCrossingLossModel" + }, + "title": "Losses", + "type": "array" + }, + "operation": { + "enum": [ + "projection", + "masking", + "redaction", + "transformation", + "declassification" + ], + "title": "Operation", + "type": "string" + }, + "order_model": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Order Model", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/ParticipantCrossingPolicyReferenceModel" + }, + "result_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Result Marking Refs", + "type": "array" + }, + "result_subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + }, + "rule_ref": { + "minLength": 1, + "title": "Rule Ref", + "type": "string" + }, + "rule_revision": { + "minLength": 1, + "title": "Rule Revision", + "type": "string" + }, + "source_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Source Marking Refs", + "type": "array" + }, + "source_subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + }, + "stage": { + "const": "transformed", + "title": "Stage", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/ParticipantCrossingSubjectReferenceModel" + }, + "transformation_id": { + "minLength": 1, + "title": "Transformation Id", + "type": "string" + } + }, + "required": [ + "direction", + "interaction_kind", + "audience_scope_ref", + "subject", + "controller_ref", + "authority_basis_refs", + "policy", + "effective_order", + "order_model", + "backend_posture", + "loss_and_limitations", + "stage", + "decision_ref", + "transformation_id", + "operation", + "source_subject", + "result_subject", + "rule_ref", + "rule_revision", + "source_marking_refs", + "result_marking_refs" + ], + "title": "ParticipantCrossingTransformationModel", + "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://aces.dev/schemas/participant-crossing-occurrence-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Closed participant-runtime carrier for one API-423 crossing fact.", + "properties": { + "actor_ref": { + "minLength": 1, + "title": "Actor 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-crossing-occurrence", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "extension_policy": { + "const": "closed", + "title": "Extension Policy", + "type": "string" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "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" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "object_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "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": { + "discriminator": { + "mapping": { + "audited": "#/$defs/ParticipantCrossingAuditModel", + "decided": "#/$defs/ParticipantCrossingDecisionModel", + "delivered": "#/$defs/ParticipantCrossingDeliveryModel", + "delivery-attempted": "#/$defs/ParticipantCrossingDeliveryAttemptModel", + "disclosed": "#/$defs/ParticipantCrossingDisclosureModel", + "observed": "#/$defs/ParticipantCrossingObservationModel", + "requested": "#/$defs/ParticipantCrossingRequestModel", + "transformed": "#/$defs/ParticipantCrossingTransformationModel" + }, + "propertyName": "stage" + }, + "oneOf": [ + { + "$ref": "#/$defs/ParticipantCrossingRequestModel" + }, + { + "$ref": "#/$defs/ParticipantCrossingDecisionModel" + }, + { + "$ref": "#/$defs/ParticipantCrossingTransformationModel" + }, + { + "$ref": "#/$defs/ParticipantCrossingDisclosureModel" + }, + { + "$ref": "#/$defs/ParticipantCrossingDeliveryAttemptModel" + }, + { + "$ref": "#/$defs/ParticipantCrossingDeliveryModel" + }, + { + "$ref": "#/$defs/ParticipantCrossingObservationModel" + }, + { + "$ref": "#/$defs/ParticipantCrossingAuditModel" + } + ], + "title": "Occurrence" + }, + "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" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "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": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "schema_name": { + "const": "participant-crossing-occurrence", + "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_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" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + } + }, + "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", + "provenance_refs", + "evidence_refs", + "object_marking_refs", + "authorization_scope", + "occurrence" + ], + "title": "ParticipantCrossingOccurrenceModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Every crossing fact must resolve typed subjects, exact policy revisions, predecessor stages, evidence, markings, and order coordinates without retroactive authorization or identity reuse.", + "id": "participant-crossing-context-agreement", + "inputs": [ + { + "contract_id": "participant-crossing-occurrence-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.validate_participant_crossing_occurrence_context" + }, + { + "description": "Requested, decided, transformed, disclosed, attempted, delivered, observed, and audited facts remain independently addressable and do not imply one another.", + "id": "participant-crossing-stage-separation", + "inputs": [ + { + "contract_id": "participant-crossing-occurrence-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantCrossingOccurrenceModel" + } + ], + "x-aces-semantic-profile": { + "contract_id": "participant-crossing-occurrence-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/docs/decisions/issue-798-api-423-participant-crossing-contracts-preflight.md b/docs/decisions/issue-798-api-423-participant-crossing-contracts-preflight.md new file mode 100644 index 000000000..ff9aa35e5 --- /dev/null +++ b/docs/decisions/issue-798-api-423-participant-crossing-contracts-preflight.md @@ -0,0 +1,312 @@ +# Issue #798 — API-423 Participant Crossing Contract Preflight + +Date: 2026-07-26 + +Issue: #798. + +Requirement: API-423. + +This note records architecture boundaries and implementation guardrails for +portable participant-crossing policy, transformation, disclosure, realization, +evidence, and provenance contracts. It is guidance only. It does not publish a +model or schema, add runtime enforcement or persistence, change backend +capabilities, or claim that a crossing was authorized, delivered, observed, or +audited. + +## Binding Authorities + +- Accepted ADR-085 and + `specs/formal/participant-semantics/information-flow-control.md` define the + common crossing coordinates, deny-first decision composition, order and + policy-revision semantics, information-flow operations, and claim limits. +- ADR-054 and ADR-060 define the participant-runtime carrier discipline. + `ParticipantRuntimeBaseEnvelopeModel` already owns event/schema identity, + participant and episode scope, timestamps and order, actor/producer/source + identity, evidence/provenance, markings, redaction, and authorization scope. +- API-406 carriers remain authoritative for lifecycle, observation, + participant history, shared state, and outcome facts. API-409 + `ParticipantControlOccurrenceModel` remains authoritative for proposal, + approval/denial, direction, intervention, handoff, override, and cancellation + occurrences. +- SEM-226 exposure selectors and occurrence/realization records own governed + participant projection and exposure agreement. DSL-142 participant inject + delivery bindings retain the original DSL-111 inject and narrative occurrence + identity. API-423 composes these artifacts by reference; it does not replace + them. +- ADR-009 and ADR-061 make schemas under `contracts/schemas/` the + hand-governed normative authority and `schema_bundle()` output compatibility + evidence. Publication entries, fixtures, reference parity, and compatibility + classification move together. +- `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, and the existing + policy/verification scripts own workflow. The implementation branch does not + contain a requirement UID, so implementation work must set + `ACES_REQUIREMENT_UID=API-423`. + +These authorities settle the architecture. API-423 needs no new ADR unless +implementation discovers a conflict that cannot be resolved by composition. + +## Architecture Decisions And Boundaries + +### Publish relation records, not another participant carrier + +API-423 belongs to the existing `participant-runtime` contract family. A +top-level crossing occurrence must reuse `ParticipantRuntimeBaseEnvelopeModel` +unchanged and exactly once. API-423-specific coordinates belong on focused +records or shared components private to this contract family; adding them to +the base would tighten or expand every API-406/API-409 carrier. + +The contract family must preserve independently addressable facts for: + +- a crossing request or produced egress candidate; +- the policy decision at one effective order point; +- each transformation, projection, redaction, or marking result; +- each declassification or disclosure authorization where applicable; +- attempted or realized delivery; +- participant observation; and +- audit/evidence retention. + +These facts may share closed components and may be published as separate +schemas or a closed discriminated family, but each fact needs its own stable +identity and typed predecessor/subject references. A nullable object whose +meaning depends on whichever optional fields happen to be present is not +acceptable. Requested, decided, transformed, delivered, observed, and audited +must never be inferred from one another. + +### Reference the semantic subject without copying its payload + +Every crossing binds one closed direction and interaction kind to one typed +existing subject, such as: + +- an API-409 proposal/control occurrence; +- a SEM-211 action contract, admission, attempt, or result; +- an API-406 observation, lifecycle, context/history/status projection, or + outcome carrier; +- a SEM-220/226 decision-surface or exposure item; +- a DSL-142 participant-directed inject delivery binding; or +- a governed intervention/control target. + +The subject relation must carry enough identity to reject a kind/contract/ref, +revision, participant, or episode mismatch. It must not contain an arbitrary +`payload`, `message`, `metadata`, `details`, or `extensions` map. Existing +payloads, hidden values, policy bodies, backend objects, and evidence content +remain out of line. A digest may supplement identity or integrity; it is not a +replacement for the typed carrier reference when that carrier must resolve. + +Do not reuse `ParticipantControlTargetKind` as a universal crossing vocabulary: +it intentionally describes API-409 control targets and does not own +observations, projections, inject deliveries, or evidence. Define any genuinely +new crossing direction/kind vocabulary once, keep it closed, and bind it to the +concept-authority surface rather than copying local enums across packages. + +### Keep decision, operation, and realization coordinates independent + +A decision binds the request/candidate identity, participant and episode, +direction and interaction kind, actor/controller/authority basis, exact policy +identity/revision/digest, effective order and order model, markings, +backend-support posture, disposition and safe reason code, required evidence, +limitations, and predecessor references. + +The following remain different claims: + +- caller authorization, participant/controller authority, action admission, + audience visibility, marking authorization, and declassification authority; +- withholding, projection/masking, redaction, transformation, + declassification, disclosure, concealment/revocation, loss, and weakening; +- admitted, attempted, delivered, observed, persisted, and audited; and +- schema-valid, supported, implemented, tested, runtime-realized, and proved. + +Decision composition is deny-first. Missing or unresolved required coordinates +reject or report unsupported/unknown under the owning vocabulary; they never +default to permit. A later policy revision, controller state, or +declassification cannot authorize an earlier crossing. + +A transformation records source and result identity/digest, rule identity and +revision, actor/authority, source and result markings, evidence/provenance, and +loss. It never mutates its source. A transformed action or proposal receives a +new identity and fresh structural, semantic, and admission validation; it does +not inherit approval, admission, execution, delivery, or idempotency state. + +### Link realization to existing occurrence evidence + +An API-423 realization relation may record a bounded realization disposition +and link decision, attempt, delivery, observation, audit, evidence, and +provenance identities. It must not recreate the state machines or payloads of: + +- `ParticipantLifecycleEventModel`, + `ParticipantBehaviorHistoryEventModel`, or + `ParticipantActionResultModel`; +- `ParticipantObservationEnvelopeModel`; +- `ParticipantControlOccurrenceModel`; +- SEM-226 exposure occurrence/realization records; +- experiment evidence records; or +- downstream `AuditEvent` and append-only runtime history. + +Scheduling is not attempted delivery; attempted delivery is not delivery; +delivery is not observation; observation is not action; and audit retention is +not participant egress. A realization status that claims one of these facts +must cite the owning record and pass participant/episode, policy/order, +marking, and subject agreement checks. + +### Validate each invariant at one owning layer + +Closed shape, bounded scalar/list constraints, unique references, +branch-specific requiredness, and single-record cross-field rules belong in +`ContractModel` descendants and equivalent JSON Schema constraints. + +Cross-record agreement belongs in one contract-level semantic validator +following `validate_participant_control_occurrence_context()` and +`validate_participant_decision_surface_context()`. Its resolution context is +the seam for known typed subjects, policy revisions, control occurrences, +transformations, lifecycle/delivery/observation facts, evidence, and audit +references. Do not repeat those joins in model validators, API handlers, +runtime services, repositories, backends, and conformance runners. + +Non-schema-expressible obligations must be published through the existing +`x-aces-invariants` / `aces-semantic-invariants-v1` mechanism. The shared +validator must fail closed on unknown or mismatched typed refs, identity reuse +with different semantics, participant/episode disagreement, stale policy or +subject revision, contradictory direction/kind/disposition, invalid order, +unauthorized marking weakening, transformation cycles or source mutation, +realization without the required owning occurrence, and required evidence that +does not resolve. + +## Canonical Incumbents To Reuse + +| Concern | Canonical incumbent and required use | +| --- | --- | +| Contract closure and primitives | `ContractModel(extra="forbid")`, `NonEmptyString`, `PrefixedDigestString`, `Rfc3339DateTimeString`, non-negative/positive integer aliases, and existing participant-runtime literals. Add no DTO base or primitive package. | +| Runtime envelope | `ParticipantRuntimeBaseEnvelopeModel` and its event/source/raw-integrity components. Reuse it unchanged and do not restate evidence, provenance, marking, redaction, time, or participant/episode fields. | +| Control and action subjects | API-409 `ParticipantControlOccurrenceModel` plus its contextual validator; SEM-211 action contracts/admission; lifecycle, behavior-history, and action-result records. Link by typed ref and retain each owning lifecycle. | +| Projection and observation | `ParticipantObservationEnvelopeModel`, `ParticipantContextViewModel`, `ParticipantHistoryViewModel`, `ParticipantStatusViewModel`, `ParticipantDecisionSurfaceModel`, and SEM-226 exposure selectors/realizations. Current API-408 retrieval authorization is not participant-safe egress authority. | +| Participant injects | DSL-142 `ParticipantInjectDelivery`, its compiled stable addresses, original DSL-111 inject/event/script/story identity, temporal constraints, and evidence requirements. Required evidence is not produced evidence or a delivery receipt. | +| Evidence, provenance, and claims | Base-envelope refs, `ExperimentEvidenceRecordModel`, `BehavioralClaimBindingModel`, existing loss/limitation fields, and append-only evidence/history semantics. Reference these; do not create an API-423 evidence store or proof vocabulary. | +| Validation | Pydantic model validators, focused `validate_*_context()` helpers, `_add_aces_invariant()`, and `aces-semantic-invariants-v1`. One resolver-backed join validator owns cross-record agreement. | +| Concepts and capability | `controlled-vocabularies-v1`, `concept-families-v1`, existing concept binding validators, API-407 participant feature-support declarations, and the exact/bounded/disclosed-weak/unsupported scale. Capability does not grant authority or prove realization. | +| Publication and compatibility | `contracts/schemas/participant-runtime/`, `contracts/fixtures/participant-runtime/`, `contracts/schema-publication/entries/`, `schema_bundle()`, explicit generator routing, `tools/check_generated_schemas.py`, and `tools/check_schema_publication.py --base-rev`. | +| Diagnostics and errors | `Diagnostic`, `DiagnosticModel`, `Severity`, operation result envelopes, bounded HTTP details, and the redacted unexpected-error envelope. Add no API-423 exception hierarchy or logger. | +| Downstream persistence/audit | `RuntimeSnapshot`, `ControlPlaneStore`, and `AuditEvent`. API-423 publishes portable relations only; RUN-319 owns append-only enforcement and persistence. | +| Lineage | The participant section of `docs/explain/sdl/lineage.md`, `SDLLineageLedgerModel`, its source audit, and `tools/check_sdl_lineage.py`. Update delivery status/evidence/nonclaims; change the ledger or source audit only for a changed normative derivation or compatibility claim. | +| Workflow | `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, repo/requirement policy, authority, schema, fixture, concept, lineage, compatibility, and documentation checks. Add no issue-local runner or publication ledger. | + +## Cross-Cutting Layers And Security Posture + +1. **Contract and published-shape gate.** Closed hand-governed schemas and + matching `ContractModel` descendants reject unknown keys, unbounded bags, + illegal stage combinations, malformed ids/digests/times, and missing + security coordinates. Valid/invalid fixtures must pass both Draft 2020-12 + schema validation and Python model validation. +2. **Reference and policy gate.** The shared context validator resolves typed + carrier, policy/revision, participant/episode, order, marking, + transformation, realization, evidence, and audit joins. Unknown required + refs, contradictory decisions, stale revisions, and unsupported required + posture fail closed. +3. **Secret and hidden-content gate.** Contracts, schemas, fixtures, + diagnostics, audit details, logs, lineage, and issue evidence carry safe + ids, refs, digests, controlled codes, markings, classifications, and bounded + limitations only. They exclude credentials/tokens/keys, prompts or private + memory, hidden answers/state, raw rejected input, policy bodies, environment + dumps, backend objects, and raw evidence/payload content. +4. **Diagnostic and error-envelope gate.** Validation messages are + value-independent and bounded. Public code must not stringify rejected + records or Pydantic `ValidationError` input into diagnostics, responses, + audit, or logs. Expected failures use existing `Diagnostic`/disposition + contracts; unexpected HTTP failures retain + `{"detail":"internal server error"}`. +5. **Publication and compatibility gate.** Every new schema needs an explicit + participant-runtime path, valid/invalid fixtures, a publication entry with + current hash and `last_change`, identical `schema_bundle()` output, and + base-revision compatibility classification. Existing carriers and + `ParticipantRuntimeBaseEnvelopeModel` must not be tightened incidentally. +6. **Concept and claim gate.** Direction, interaction, disposition, + transformation, disclosure, loss, weakening, and support terms have one + authority. Structural validity is not reported as authorization, + enforcement, backend support, delivery, observation, noninterference, or + proof. + +API-423 itself adds no HTTP route, authentication mechanism, configuration or +environment-binding shape, secret loader, CLI argument, subprocess, daemon, +socket, filesystem store, or process-argument surface. Adding one is a scope +breach. Consequently, no API-423 value belongs in environment variables, +process argv, filenames, stdout/stderr, shell strings, or host logs. + +A later runtime adapter must enter through `create_control_plane_app()`, +`ControlPlaneSecurityConfig.strict_defaults()`, verified bearer/proxy identity, +role and target binding, request-size guards, request fingerprints, idempotency +keys, and `AuditEvent`. It must then separately bind the authenticated caller +to actor, controller, participant, audience, authority basis, and policy. +Operator or auditor role cannot grant participant visibility, action authority, +or declassification. Future persistence belongs in first-class append-only +`RuntimeSnapshot`/`ControlPlaneStore` fields, not snapshot `metadata`, generic +history `details`, logs, audit bodies, or a gateway-local database. + +## Extensibility Seam + +The stable seam is a closed stage record plus a resolver-backed typed subject +relation. It is parameterized by participant, episode, audience, direction, +interaction kind, policy identity/revision/effective order, order model, +actor/controller/authority, subject kind/ref/revision, markings, +transformation/declassification rule, backend posture, disposition, +evidence/provenance, loss/weakening, and predecessor/realization refs. + +The resolution context, rather than the published payload, supplies the known +carrier indexes and policy history. This allows the next reasonable changes—a +new governed carrier kind, streaming or multi-part realization, partial-order +delivery, another audience, or RUN-319 persistence—to add a closed variant or +resolver capability without modifying existing payload carriers, the runtime +base envelope, or earlier records. New wire meaning still requires compatible +contract evolution; an open metadata extension is not the seam. + +## Gotchas And Anti-Patterns + +Avoid: + +- a generic message, payload, policy, evidence, metadata, details, or extension + bag, or an optional-field union that collapses all crossing stages; +- copying action, observation, lifecycle, context, inject, control, + evidence/provenance, or audit payloads into API-423 records; +- reusing an API-409 target enum as a universal carrier registry or creating + duplicate direction/disposition/transformation vocabularies in multiple + packages; +- treating authenticated caller, actor, controller, authority, participant, + producer, backend, and auditor as one identity; +- treating approval as admission, admission as attempt, schedule as delivery, + delivery as observation, observation as action, or audit as disclosure; +- treating redaction, masking, hashing, summarization, loss, or weakening as + authorization or declassification; +- applying current policy to past order points, using wall-clock/list/receipt + order, last-writer-wins, mutating prior history, or retroactively revoking + participant knowledge; +- mutating a source during transformation or carrying its approval/admission/ + realization state to the result; +- validating cross-record joins independently in schemas, model validators, + route handlers, stores, backends, and tests; +- raw secrets, hidden payloads, policy bodies, rejected values, backend + objects, exception text, tracebacks, or environment/process data in any + portable or observable surface; +- modifying the shared participant envelope for convenience; +- inferring backend support or runtime realization from contract publication, + model methods, passing fixtures, or capability booleans; +- a second schema registry, publication ledger, concept catalog, validator + stack, fixture runner, exception hierarchy, persistence store, audit channel, + logger, or workflow branch; and +- changing the lineage ledger/source audit solely because delivery status + changes. + +## Non-Goals And Implementation Boundaries + +- No participant gateway, transport, endpoint, UI, policy engine or expression + language, provider integration, credential broker, or human-control service. +- No generic participant message DTO or second action, observation, lifecycle, + context, intervention, inject, evidence, provenance, or audit carrier. +- No runtime enforcement, action admission, transformation execution, + delivery, observation, persistence, replay, backend realization, capability + implementation, or migration logic. +- No policy body, participant prompt/chain-of-thought/private memory, hidden + state, credential, raw rejected input, or backend-private object in portable + records. +- No claim that schema/model/fixture validity proves authorization, delivery, + observation, runtime realization, backend conformance, noninterference, + trace equivalence, simulation, refinement, epistemic equivalence, or + bisimulation. +- No lineage-ledger/source-audit change unless implementation changes a + normative external derivation or compatibility claim. diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index 4f6139b39..9ce8b6fc3 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -1039,6 +1039,40 @@ which dynamic queue/log/config details remain evidence or bounded settings. information-flow proof, and API-423 crossing-policy realization are explicit nonclaims. The lineage ledger and source audit remain unchanged because this delivery adds no normative external derivation or compatibility claim. +- API-423 composes the already adopted SEM-230/ADR-085 information-flow, + API-406 participant-runtime, API-409 control-occurrence, SEM-211 action, + SEM-220/226 decision-surface and exposure, DSL-142 participant-inject, + experiment-evidence, provenance, marking, and visible-order authorities. The + exact RAES mapping is the closed + `participant-crossing-occurrence-v1` family and its unchanged + `ParticipantRuntimeBaseEnvelopeModel`: each requested, decided, transformed, + disclosed, delivery-attempted, delivered, observed, or audited fact has its + own event identity and typed incumbent subject reference. The focused + occurrence details bind direction and interaction kind, audience, + actor/controller/authority, exact policy identity/revision/digest and + effective order, markings, deny-first gate results and disposition, + transformation/declassification basis, backend posture, evidence, + provenance, and explicit loss/weakening without copying a carrier payload or + policy body. `validate_participant_crossing_occurrence_context()` is the + single resolver-backed join: it fails closed on unknown or mismatched typed + subjects, stale or future policy revisions, contradictory decisions, + identity reuse, transformation cycles, marking weakening without explicit + declassification, missing evidence, invalid predecessor order, and + realization claims without their owning fact. +- API-423 delivery evidence is the hand-governed + `participant-crossing-occurrence-v1` schema and publication entry, valid and + invalid participant-runtime fixtures, the matching `schema_bundle()` output, + the closed participant-crossing concept vocabularies, and + `implementations/python/tests/test_api_423_participant_crossing_contracts.py`. + This delivers portable policy-decision and evidence relations only. It does + not implement a gateway, transport, policy engine, authentication or + authorization service, action admission, transformation execution, + participant delivery or observation, persistence, audit storage, backend + realization, migration, universal noninterference, trace equivalence, + refinement, simulation, bisimulation, epistemic equivalence, or proof. The + lineage ledger and source audit remain unchanged because API-423 reuses the + recorded SEM-230 derivation and changes no normative external derivation or + compatibility claim. - CALDERA adversary-emulation research informs the action semantics: cyber actions can change foothold, knowledge, observations, detection surface, and downstream outcomes under uncertainty. diff --git a/implementations/python/packages/raes_conformance/conformance/validators.py b/implementations/python/packages/raes_conformance/conformance/validators.py index 3477dfcc2..d154fb443 100644 --- a/implementations/python/packages/raes_conformance/conformance/validators.py +++ b/implementations/python/packages/raes_conformance/conformance/validators.py @@ -20,6 +20,7 @@ OperationStatusModel, OrchestrationPlanModel, ParticipantBehaviorHistoryEventModel, + ParticipantCrossingOccurrenceModel, ParticipantEpisodeHistoryEventModel, ParticipantEpisodeStateModel, ParticipantImplementationManifestModel, @@ -63,6 +64,7 @@ "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_validate, "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_validate, "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_validate, + "participant-crossing-occurrence-v1": ParticipantCrossingOccurrenceModel.model_validate, "experiment-capture-spec-v1": ExperimentCaptureSpecModel.model_validate, "experiment-evidence-record-v1": ExperimentEvidenceRecordModel.model_validate, "experiment-derived-measure-v1": ExperimentDerivedMeasureModel.model_validate, diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index f3ddb889d..141478576 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -188,8 +188,6 @@ ) from .manifests import CleanupCapabilitiesModel as CleanupCapabilitiesModel from .participant_context import ParticipantContextViewModel -from .participant_control import ParticipantControlDeclarationModel, ParticipantControlOccurrenceModel -from .participant_control_validation import validate_participant_control_occurrence_context from .participant_decision_surface import ( ParticipantDecisionSurfaceActionEntryModel, ParticipantDecisionSurfaceCandidateSetFormModel, @@ -228,6 +226,7 @@ ParticipantImplementationProvenanceModel, ParticipantImplementationSelectionModel, ) +from .participant_occurrences import * # noqa: F403 from .participant_runtime import ( ParticipantActionEffectResultModel, ParticipantActionPreconditionResultModel, @@ -437,9 +436,10 @@ "ParticipantAttributionCandidateModel", "ParticipantAttributionEdgeModel", "ParticipantAttributionEvidenceBasisModel", "ParticipantAttributionOrderingBasisModel", "ParticipantAutonomousExecutionStateModel", "ParticipantBehaviorHistoryEventModel", - "ParticipantContextViewModel", "ParticipantControlDeclarationModel", - "ParticipantControlOccurrenceModel", "validate_participant_control_occurrence_context", - "ParticipantDecisionSurfaceActionEntryModel", "ParticipantDecisionSurfaceCandidateSetFormModel", + "ParticipantContextViewModel", "ParticipantControlDeclarationModel", "ParticipantControlOccurrenceModel", # noqa: F405 + "ParticipantCrossingOccurrenceModel", "validate_participant_control_occurrence_context", # noqa: F405 + "validate_participant_crossing_occurrence_context", "ParticipantDecisionSurfaceActionEntryModel", # noqa: F405 + "ParticipantDecisionSurfaceCandidateSetFormModel", "ParticipantDecisionSurfaceConstrainedFormModel", "ParticipantDecisionSurfaceExposureBindingModel", "ParticipantDecisionSurfaceExposureRealizationModel", "ParticipantDecisionSurfaceModel", "ParticipantDecisionSurfaceOpenEndedFormModel", "ParticipantDecisionSurfaceSelectionModel", diff --git a/implementations/python/packages/raes_contracts/contracts/base.py b/implementations/python/packages/raes_contracts/contracts/base.py index ba1feeedb..d6fcc91ba 100644 --- a/implementations/python/packages/raes_contracts/contracts/base.py +++ b/implementations/python/packages/raes_contracts/contracts/base.py @@ -196,6 +196,15 @@ def _validate_claim_strength(self) -> BehavioralClaimBindingModel: "capabilities.orchestrator.supported_workflow_state_predicates", "workflows.steps.fact_binding_refs", "random_streams.draw_purpose", + "participant-crossing-occurrence.direction", + "participant-crossing-occurrence.interaction_kind", + "participant-crossing-occurrence.subject.subject_kind", + "participant-crossing-occurrence.operation", + "participant-crossing-occurrence.gates", + "participant-crossing-occurrence.decision.disposition", + "participant-crossing-occurrence.backend_posture", + "participant-crossing-occurrence.losses.kind", + "participant-crossing-occurrence.stage", *_BACKEND_CONCEPT_BINDING_SCOPES, *_PARTICIPANT_IMPLEMENTATION_CONCEPT_BINDING_SCOPES, } diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index a7e062e8f..eaf2cde68 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -32,6 +32,7 @@ from .manifests import ProcessorManifestV2Model from .participant_context import ParticipantContextViewModel from .participant_control import ParticipantControlOccurrenceModel +from .participant_crossing import ParticipantCrossingOccurrenceModel from .participant_decision_surface import ParticipantDecisionSurfaceModel from .participant_envelopes import ( ParticipantJointActionRecordModel, @@ -186,6 +187,7 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "participant-joint-action-record-v1": ParticipantJointActionRecordModel.model_json_schema(), "participant-time-management-context-v1": ParticipantTimeManagementContextModel.model_json_schema(), "participant-control-occurrence-v1": ParticipantControlOccurrenceModel.model_json_schema(), + "participant-crossing-occurrence-v1": ParticipantCrossingOccurrenceModel.model_json_schema(), "participant-outcome-report-v1": ParticipantOutcomeReportModel.model_json_schema(), "participant-status-view-v1": ParticipantStatusViewModel.model_json_schema(), "participant-history-view-v1": ParticipantHistoryViewModel.model_json_schema(), diff --git a/implementations/python/packages/raes_contracts/contracts/participant_crossing.py b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py new file mode 100644 index 000000000..84a7e7d20 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py @@ -0,0 +1,422 @@ +"""API-423 portable participant-crossing policy and evidence contracts.""" + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import Field, GetJsonSchemaHandler, field_validator, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from .base import ( + ContractModel, + NonEmptyString, + NonNegativeInteger, + PositiveInteger, + PrefixedDigestString, +) +from .participant_crossing_vocab import ( + ParticipantCrossingBackendPosture, + ParticipantCrossingDecisionDisposition, + ParticipantCrossingDirection, + ParticipantCrossingGateDisposition, + ParticipantCrossingInteractionKind, + ParticipantCrossingLossKind, + ParticipantCrossingOperation, + ParticipantCrossingSubjectKind, +) +from .participant_envelopes import ParticipantRuntimeBaseEnvelopeModel +from .participant_runtime import ParticipantRuntimeOrderingBasis +from .schema_invariants import _add_aces_invariant + + +class ParticipantCrossingSubjectReferenceModel(ContractModel): + """Typed identity for an existing carrier without copying its payload.""" + + subject_kind: ParticipantCrossingSubjectKind + contract_id: NonEmptyString + subject_ref: NonEmptyString + subject_revision: NonEmptyString | None = None + subject_digest: PrefixedDigestString | None = None + participant_address: NonEmptyString + episode_id: NonEmptyString + + @model_validator(mode="after") + def _require_revision_or_digest(self) -> ParticipantCrossingSubjectReferenceModel: + if self.subject_revision is None and self.subject_digest is None: + raise ValueError("participant crossing subject requires a revision or digest") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("anyOf", []).extend( + [ + {"required": ["subject_revision"], "properties": {"subject_revision": {"type": "string"}}}, + {"required": ["subject_digest"], "properties": {"subject_digest": {"type": "string"}}}, + ] + ) + return json_schema + + +class ParticipantCrossingPolicyReferenceModel(ContractModel): + """Exact revision and order interval for the policy used at a crossing.""" + + policy_id: NonEmptyString + policy_revision: NonEmptyString + policy_digest: PrefixedDigestString + effective_order: NonNegativeInteger + valid_from_order: NonNegativeInteger + valid_until_order: NonNegativeInteger + + @model_validator(mode="after") + def _validate_order_interval(self) -> ParticipantCrossingPolicyReferenceModel: + if self.valid_until_order < self.valid_from_order: + raise ValueError("participant crossing policy interval must not be inverted") + if not self.valid_from_order <= self.effective_order <= self.valid_until_order: + raise ValueError("participant crossing policy effective order must fall within its validity interval") + return self + + +class ParticipantCrossingLossModel(ContractModel): + """One explicit fidelity loss or guarantee weakening.""" + + kind: ParticipantCrossingLossKind + basis_ref: NonEmptyString + affected_ref: NonEmptyString + limitation_ref: NonEmptyString + + +class ParticipantCrossingDecisionGatesModel(ContractModel): + """Independent deny-first gates for one crossing decision.""" + + caller_authorization: ParticipantCrossingGateDisposition + target_authorization: ParticipantCrossingGateDisposition + participant_authority: ParticipantCrossingGateDisposition + action_admission: ParticipantCrossingGateDisposition + visibility: ParticipantCrossingGateDisposition + marking_authorization: ParticipantCrossingGateDisposition + declassification: ParticipantCrossingGateDisposition + backend_support: ParticipantCrossingGateDisposition + transformation_validity: ParticipantCrossingGateDisposition + + def dispositions(self) -> tuple[ParticipantCrossingGateDisposition, ...]: + """Return the complete gate tuple in stable semantic order.""" + + return ( + self.caller_authorization, + self.target_authorization, + self.participant_authority, + self.action_admission, + self.visibility, + self.marking_authorization, + self.declassification, + self.backend_support, + self.transformation_validity, + ) + + +class ParticipantCrossingOccurrenceBaseModel(ContractModel): + """Coordinates shared by every independently addressable crossing fact.""" + + direction: ParticipantCrossingDirection + interaction_kind: ParticipantCrossingInteractionKind + audience_scope_ref: NonEmptyString + subject: ParticipantCrossingSubjectReferenceModel + controller_ref: NonEmptyString + authority_basis_refs: list[NonEmptyString] = Field(min_length=1) + policy: ParticipantCrossingPolicyReferenceModel + effective_order: NonNegativeInteger + order_model: ParticipantRuntimeOrderingBasis + backend_posture: ParticipantCrossingBackendPosture + loss_and_limitations: list[NonEmptyString] = Field(min_length=1) + + @field_validator("authority_basis_refs", "loss_and_limitations") + @classmethod + def _require_unique_refs(cls, values: list[str]) -> list[str]: + if len(values) != len(set(values)): + raise ValueError("participant crossing references must be unique") + return values + + @model_validator(mode="after") + def _validate_policy_order(self) -> ParticipantCrossingOccurrenceBaseModel: + if not self.policy.valid_from_order <= self.effective_order <= self.policy.valid_until_order: + raise ValueError("participant crossing order must fall within the policy validity interval") + if self.policy.effective_order > self.effective_order: + raise ValueError("participant crossing cannot use a future policy revision") + return self + + +class ParticipantCrossingRequestModel(ParticipantCrossingOccurrenceBaseModel): + """A requested crossing or produced egress candidate, not a decision.""" + + stage: Literal["requested"] + request_id: NonEmptyString + requested_operation: ParticipantCrossingOperation + action_or_projection_ref: NonEmptyString + required_evidence_refs: list[NonEmptyString] = Field(min_length=1) + + @field_validator("required_evidence_refs") + @classmethod + def _require_unique_evidence(cls, values: list[str]) -> list[str]: + if len(values) != len(set(values)): + raise ValueError("required crossing evidence references must be unique") + return values + + +class ParticipantCrossingDecisionModel(ParticipantCrossingOccurrenceBaseModel): + """The deny-first policy decision for one exact crossing request.""" + + stage: Literal["decided"] + request_ref: NonEmptyString + decision_id: NonEmptyString + decision_revision: PositiveInteger + gates: ParticipantCrossingDecisionGatesModel + disposition: ParticipantCrossingDecisionDisposition + reason_code: NonEmptyString + required_operation: ParticipantCrossingOperation | None = None + required_evidence_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_deny_first_disposition(self) -> ParticipantCrossingDecisionModel: + dispositions = set(self.gates.dispositions()) + failed = ParticipantCrossingGateDisposition.DENY in dispositions + unresolved = bool( + dispositions + & { + ParticipantCrossingGateDisposition.UNKNOWN, + ParticipantCrossingGateDisposition.UNSUPPORTED, + } + ) + if self.disposition in { + ParticipantCrossingDecisionDisposition.PERMIT, + ParticipantCrossingDecisionDisposition.TRANSFORM, + } and (failed or unresolved): + raise ValueError("permitted participant crossing decisions require every applicable gate to permit") + if self.disposition == ParticipantCrossingDecisionDisposition.DENY and not failed: + raise ValueError("denied participant crossing decisions require a denied gate") + if self.disposition == ParticipantCrossingDecisionDisposition.UNSUPPORTED and not unresolved: + raise ValueError("unsupported participant crossing decisions require an unresolved or unsupported gate") + if self.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM and self.required_operation not in { + ParticipantCrossingOperation.PROJECTION, + ParticipantCrossingOperation.MASKING, + ParticipantCrossingOperation.REDACTION, + ParticipantCrossingOperation.TRANSFORMATION, + ParticipantCrossingOperation.DECLASSIFICATION, + }: + raise ValueError("transform decisions require an explicit transformation operation") + if self.disposition != ParticipantCrossingDecisionDisposition.TRANSFORM and self.required_operation is not None: + raise ValueError("required_operation is reserved for transform decisions") + return self + + +class ParticipantCrossingTransformationModel(ParticipantCrossingOccurrenceBaseModel): + """A non-mutating transformation from one typed subject to a new subject.""" + + stage: Literal["transformed"] + decision_ref: NonEmptyString + transformation_id: NonEmptyString + operation: Literal[ + ParticipantCrossingOperation.PROJECTION, + ParticipantCrossingOperation.MASKING, + ParticipantCrossingOperation.REDACTION, + ParticipantCrossingOperation.TRANSFORMATION, + ParticipantCrossingOperation.DECLASSIFICATION, + ] + source_subject: ParticipantCrossingSubjectReferenceModel + result_subject: ParticipantCrossingSubjectReferenceModel + rule_ref: NonEmptyString + rule_revision: NonEmptyString + source_marking_refs: list[NonEmptyString] = Field(min_length=1) + result_marking_refs: list[NonEmptyString] = Field(min_length=1) + declassification_basis_ref: NonEmptyString | None = None + losses: list[ParticipantCrossingLossModel] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_transformation_shape(self) -> ParticipantCrossingTransformationModel: + if (self.source_subject.subject_kind, self.source_subject.subject_ref) == ( + self.result_subject.subject_kind, + self.result_subject.subject_ref, + ): + raise ValueError("participant crossing transformation requires a new result identity") + if self.subject != self.result_subject: + raise ValueError("participant crossing transformation subject must be its result subject") + if self.operation == ParticipantCrossingOperation.DECLASSIFICATION and self.declassification_basis_ref is None: + raise ValueError("declassification requires an explicit authority basis") + if ( + self.operation != ParticipantCrossingOperation.DECLASSIFICATION + and self.declassification_basis_ref is not None + ): + raise ValueError("declassification_basis_ref is reserved for declassification operations") + if self.declassification_basis_ref is None and not set(self.source_marking_refs).issubset( + self.result_marking_refs + ): + raise ValueError("transformation results must inherit source markings without declassification") + return self + + +class ParticipantCrossingDisclosureModel(ParticipantCrossingOccurrenceBaseModel): + """An authorized disclosure or declassification decision, not delivery.""" + + stage: Literal["disclosed"] + decision_ref: NonEmptyString + disclosure_id: NonEmptyString + operation: Literal[ + ParticipantCrossingOperation.DISCLOSURE, + ParticipantCrossingOperation.DECLASSIFICATION, + ] + transformation_ref: NonEmptyString | None = None + declassification_basis_ref: NonEmptyString | None = None + source_marking_refs: list[NonEmptyString] = Field(min_length=1) + result_marking_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_disclosure_markings(self) -> ParticipantCrossingDisclosureModel: + if self.operation == ParticipantCrossingOperation.DECLASSIFICATION and self.declassification_basis_ref is None: + raise ValueError("declassification disclosure requires an explicit authority basis") + if ( + self.operation != ParticipantCrossingOperation.DECLASSIFICATION + and self.declassification_basis_ref is not None + ): + raise ValueError("declassification_basis_ref is reserved for declassification operations") + if self.declassification_basis_ref is None and not set(self.source_marking_refs).issubset( + self.result_marking_refs + ): + raise ValueError("disclosure results must inherit source markings without declassification") + return self + + +class ParticipantCrossingDeliveryAttemptModel(ParticipantCrossingOccurrenceBaseModel): + """An attempted participant delivery, distinct from scheduling and success.""" + + stage: Literal["delivery-attempted"] + decision_ref: NonEmptyString + transformation_ref: NonEmptyString | None = None + attempt_id: NonEmptyString + owning_occurrence_ref: NonEmptyString + disposition: Literal["attempted", "failed", "withheld", "unsupported"] + + +class ParticipantCrossingDeliveryModel(ParticipantCrossingOccurrenceBaseModel): + """A delivered participant-facing occurrence, distinct from observation.""" + + stage: Literal["delivered"] + decision_ref: NonEmptyString + attempt_ref: NonEmptyString + delivery_id: NonEmptyString + owning_occurrence_ref: NonEmptyString + delivery_order: NonNegativeInteger + disposition: Literal["delivered", "failed", "unknown", "unsupported"] + + +class ParticipantCrossingObservationModel(ParticipantCrossingOccurrenceBaseModel): + """Participant observation of a delivered fact through an incumbent carrier.""" + + stage: Literal["observed"] + decision_ref: NonEmptyString + delivery_ref: NonEmptyString + observation_id: NonEmptyString + owning_observation_ref: NonEmptyString + observation_order: NonNegativeInteger + + +class ParticipantCrossingAuditModel(ParticipantCrossingOccurrenceBaseModel): + """Audit/evidence retention for a prior crossing fact, not participant egress.""" + + stage: Literal["audited"] + audited_event_ref: NonEmptyString + audit_record_ref: NonEmptyString + retained_evidence_refs: list[NonEmptyString] = Field(min_length=1) + + +ParticipantCrossingOccurrenceDetail = Annotated[ + ParticipantCrossingRequestModel + | ParticipantCrossingDecisionModel + | ParticipantCrossingTransformationModel + | ParticipantCrossingDisclosureModel + | ParticipantCrossingDeliveryAttemptModel + | ParticipantCrossingDeliveryModel + | ParticipantCrossingObservationModel + | ParticipantCrossingAuditModel, + Field(discriminator="stage"), +] + + +class ParticipantCrossingOccurrenceModel(ParticipantRuntimeBaseEnvelopeModel): + """Closed participant-runtime carrier for one API-423 crossing fact.""" + + schema_name: Literal["participant-crossing-occurrence"] + schema_version: Literal["1.0.0"] + event_type: Literal["participant-crossing-occurrence"] + extension_policy: Literal["closed"] + participant_address: NonEmptyString + episode_id: NonEmptyString + provenance_refs: list[NonEmptyString] = Field(min_length=1) + evidence_refs: list[NonEmptyString] = Field(min_length=1) + object_marking_refs: list[NonEmptyString] = Field(min_length=1) + occurrence: ParticipantCrossingOccurrenceDetail + + @model_validator(mode="after") + def _validate_subject_scope(self) -> ParticipantCrossingOccurrenceModel: + subject = self.occurrence.subject + if (subject.participant_address, subject.episode_id) != (self.participant_address, self.episode_id): + raise ValueError("participant crossing subject scope must match its occurrence") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + _add_aces_invariant( + json_schema, + "participant-crossing-context-agreement", + "Every crossing fact must resolve typed subjects, exact policy revisions, predecessor stages, " + "evidence, markings, and order coordinates without retroactive authorization or identity reuse.", + validator="raes_contracts.contracts.validate_participant_crossing_occurrence_context", + inputs=[{"contract_id": "participant-crossing-occurrence-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + json_schema, + "participant-crossing-stage-separation", + "Requested, decided, transformed, disclosed, attempted, delivered, observed, and audited facts " + "remain independently addressable and do not imply one another.", + validator="raes_contracts.contracts.ParticipantCrossingOccurrenceModel", + inputs=[{"contract_id": "participant-crossing-occurrence-v1", "instance_path": "#"}], + ) + return json_schema + + +__all__ = [ + "ParticipantCrossingAuditModel", + "ParticipantCrossingBackendPosture", + "ParticipantCrossingDecisionDisposition", + "ParticipantCrossingDecisionGatesModel", + "ParticipantCrossingDecisionModel", + "ParticipantCrossingDeliveryAttemptModel", + "ParticipantCrossingDeliveryModel", + "ParticipantCrossingDirection", + "ParticipantCrossingDisclosureModel", + "ParticipantCrossingGateDisposition", + "ParticipantCrossingInteractionKind", + "ParticipantCrossingLossKind", + "ParticipantCrossingLossModel", + "ParticipantCrossingObservationModel", + "ParticipantCrossingOccurrenceBaseModel", + "ParticipantCrossingOccurrenceDetail", + "ParticipantCrossingOccurrenceModel", + "ParticipantCrossingOperation", + "ParticipantCrossingPolicyReferenceModel", + "ParticipantCrossingRequestModel", + "ParticipantCrossingSubjectKind", + "ParticipantCrossingSubjectReferenceModel", + "ParticipantCrossingTransformationModel", +] diff --git a/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py b/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py new file mode 100644 index 000000000..07acd1597 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py @@ -0,0 +1,421 @@ +"""Cross-record validation for API-423 participant-crossing occurrences.""" + +from __future__ import annotations + +from collections.abc import Collection, Sequence + +from .participant_crossing import ( + ParticipantCrossingAuditModel, + ParticipantCrossingDecisionDisposition, + ParticipantCrossingDecisionModel, + ParticipantCrossingDeliveryAttemptModel, + ParticipantCrossingDeliveryModel, + ParticipantCrossingDisclosureModel, + ParticipantCrossingGateDisposition, + ParticipantCrossingObservationModel, + ParticipantCrossingOccurrenceModel, + ParticipantCrossingOperation, + ParticipantCrossingPolicyReferenceModel, + ParticipantCrossingRequestModel, + ParticipantCrossingSubjectReferenceModel, + ParticipantCrossingTransformationModel, +) + +SubjectKey = tuple[object, str] +PolicyKey = tuple[str, str] + + +def validate_participant_crossing_occurrence_context( + records: Sequence[ParticipantCrossingOccurrenceModel], + *, + known_subjects: Sequence[ParticipantCrossingSubjectReferenceModel], + policies: Sequence[ParticipantCrossingPolicyReferenceModel], + known_evidence_refs: Collection[str], + known_authority_basis_refs: Collection[str], +) -> None: + """Fail closed when API-423 facts disagree across subject, policy, or stage joins.""" + + subjects_by_key = _index_subjects(known_subjects) + policies_by_key = _index_policies(policies) + indexes = _index_records(records) + for record in indexes.records_by_event_id.values(): + _validate_common_context( + record, + subjects_by_key=subjects_by_key, + policies_by_key=policies_by_key, + known_evidence_refs=known_evidence_refs, + known_authority_basis_refs=known_authority_basis_refs, + ) + _validate_stage_context(record, indexes=indexes) + _validate_transformation_graph(indexes.transformations_by_id.values()) + + +class _RecordIndexes: + def __init__(self) -> None: + self.records_by_event_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + self.requests_by_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + self.decisions_by_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + self.transformations_by_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + self.disclosures_by_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + self.attempts_by_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + self.deliveries_by_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + self.observations_by_id: dict[str, ParticipantCrossingOccurrenceModel] = {} + + +def _index_subjects( + subjects: Sequence[ParticipantCrossingSubjectReferenceModel], +) -> dict[SubjectKey, ParticipantCrossingSubjectReferenceModel]: + indexed: dict[SubjectKey, ParticipantCrossingSubjectReferenceModel] = {} + for subject in subjects: + key = _subject_key(subject) + existing = indexed.setdefault(key, subject) + if existing != subject: + raise ValueError("typed subject identity was reused with different semantics") + return indexed + + +def _index_policies( + policies: Sequence[ParticipantCrossingPolicyReferenceModel], +) -> dict[PolicyKey, ParticipantCrossingPolicyReferenceModel]: + indexed: dict[PolicyKey, ParticipantCrossingPolicyReferenceModel] = {} + for policy in policies: + key = (policy.policy_id, policy.policy_revision) + existing = indexed.setdefault(key, policy) + if existing != policy: + raise ValueError("participant crossing policy revision was reused with different semantics") + return indexed + + +def _index_records(records: Sequence[ParticipantCrossingOccurrenceModel]) -> _RecordIndexes: + indexes = _RecordIndexes() + for record in records: + existing = indexes.records_by_event_id.setdefault(record.event_id, record) + if existing != record: + raise ValueError("participant crossing event identity was reused with different semantics") + if existing is not record: + continue + occurrence = record.occurrence + if isinstance(occurrence, ParticipantCrossingRequestModel): + _register_record(indexes.requests_by_id, occurrence.request_id, record, "request") + elif isinstance(occurrence, ParticipantCrossingDecisionModel): + _register_record(indexes.decisions_by_id, occurrence.decision_id, record, "decision") + elif isinstance(occurrence, ParticipantCrossingTransformationModel): + _register_record( + indexes.transformations_by_id, + occurrence.transformation_id, + record, + "transformation", + ) + elif isinstance(occurrence, ParticipantCrossingDisclosureModel): + _register_record(indexes.disclosures_by_id, occurrence.disclosure_id, record, "disclosure") + elif isinstance(occurrence, ParticipantCrossingDeliveryAttemptModel): + _register_record(indexes.attempts_by_id, occurrence.attempt_id, record, "delivery attempt") + elif isinstance(occurrence, ParticipantCrossingDeliveryModel): + _register_record(indexes.deliveries_by_id, occurrence.delivery_id, record, "delivery") + elif isinstance(occurrence, ParticipantCrossingObservationModel): + _register_record(indexes.observations_by_id, occurrence.observation_id, record, "observation") + return indexes + + +def _register_record( + index: dict[str, ParticipantCrossingOccurrenceModel], + identity: str, + record: ParticipantCrossingOccurrenceModel, + label: str, +) -> None: + existing = index.setdefault(identity, record) + if existing != record: + raise ValueError(f"participant crossing {label} identity was reused with different semantics") + + +def _validate_common_context( + record: ParticipantCrossingOccurrenceModel, + *, + subjects_by_key: dict[SubjectKey, ParticipantCrossingSubjectReferenceModel], + policies_by_key: dict[PolicyKey, ParticipantCrossingPolicyReferenceModel], + known_evidence_refs: Collection[str], + known_authority_basis_refs: Collection[str], +) -> None: + occurrence = record.occurrence + _resolve_subject(occurrence.subject, subjects_by_key) + if isinstance(occurrence, ParticipantCrossingTransformationModel): + _resolve_subject(occurrence.source_subject, subjects_by_key) + _resolve_subject(occurrence.result_subject, subjects_by_key) + policy = policies_by_key.get((occurrence.policy.policy_id, occurrence.policy.policy_revision)) + if policy is None: + raise ValueError("participant crossing policy revision must resolve") + if policy != occurrence.policy: + raise ValueError("participant crossing policy revision coordinates must match") + if not set(record.evidence_refs).issubset(known_evidence_refs): + raise ValueError("participant crossing evidence reference must resolve") + if not set(occurrence.authority_basis_refs).issubset(known_authority_basis_refs): + raise ValueError("participant crossing authority basis reference must resolve") + stage_evidence_refs = _stage_evidence_refs(occurrence) + if not set(stage_evidence_refs).issubset(known_evidence_refs): + raise ValueError("participant crossing stage-local evidence reference must resolve") + if isinstance( + occurrence, + ParticipantCrossingTransformationModel | ParticipantCrossingDisclosureModel, + ): + basis_ref = occurrence.declassification_basis_ref + if basis_ref is not None and ( + basis_ref not in known_authority_basis_refs or basis_ref not in occurrence.authority_basis_refs + ): + raise ValueError("participant crossing declassification authority basis must resolve") + + +def _stage_evidence_refs( + occurrence: object, +) -> Collection[str]: + if isinstance(occurrence, ParticipantCrossingRequestModel | ParticipantCrossingDecisionModel): + return occurrence.required_evidence_refs + if isinstance(occurrence, ParticipantCrossingAuditModel): + return occurrence.retained_evidence_refs + return () + + +def _resolve_subject( + subject: ParticipantCrossingSubjectReferenceModel, + subjects_by_key: dict[SubjectKey, ParticipantCrossingSubjectReferenceModel], +) -> None: + known = subjects_by_key.get(_subject_key(subject)) + if known is None: + raise ValueError("typed subject reference must resolve") + if known != subject: + raise ValueError("typed subject revision, digest, contract, or scope must match") + + +def _validate_stage_context( + record: ParticipantCrossingOccurrenceModel, + *, + indexes: _RecordIndexes, +) -> None: + occurrence = record.occurrence + if isinstance(occurrence, ParticipantCrossingRequestModel): + return + if isinstance(occurrence, ParticipantCrossingDecisionModel): + prior = _resolve_record(indexes.requests_by_id, occurrence.request_ref, "crossing request") + _validate_successor(record, prior, require_same_subject=True) + return + if isinstance(occurrence, ParticipantCrossingTransformationModel): + prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") + decision = prior.occurrence + assert isinstance(decision, ParticipantCrossingDecisionModel) + if decision.disposition != ParticipantCrossingDecisionDisposition.TRANSFORM: + raise ValueError("participant crossing transformation requires a transform decision") + _validate_successor(record, prior, require_same_subject=False) + if occurrence.operation != decision.required_operation: + raise ValueError("participant crossing transformation operation must match the decision requirement") + _require_declassification_gate(occurrence.operation, decision) + if _subject_key(decision.subject) != _subject_key(occurrence.source_subject): + raise ValueError("transformation source must match the decided subject") + return + if isinstance(occurrence, ParticipantCrossingDisclosureModel): + prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") + _require_permitted_decision(prior) + decision = prior.occurrence + assert isinstance(decision, ParticipantCrossingDecisionModel) + _require_declassification_gate(occurrence.operation, decision) + if occurrence.transformation_ref is not None: + transformed = _resolve_record( + indexes.transformations_by_id, + occurrence.transformation_ref, + "crossing transformation", + ) + transformed_occurrence = transformed.occurrence + assert isinstance(transformed_occurrence, ParticipantCrossingTransformationModel) + _require_same_decision( + occurrence.decision_ref, + transformed_occurrence.decision_ref, + "disclosure transformation", + ) + _validate_successor(record, transformed, require_same_subject=True) + else: + if decision.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM: + raise ValueError("transform decisions require their exact transformation before disclosure") + _validate_successor(record, prior, require_same_subject=True) + return + if isinstance(occurrence, ParticipantCrossingDeliveryAttemptModel): + prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") + _require_permitted_decision(prior) + decision = prior.occurrence + assert isinstance(decision, ParticipantCrossingDecisionModel) + if decision.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM: + if occurrence.transformation_ref is None: + raise ValueError("transform decisions require a transformation before delivery attempt") + transformed = _resolve_record( + indexes.transformations_by_id, + occurrence.transformation_ref, + "crossing transformation", + ) + transformed_occurrence = transformed.occurrence + assert isinstance(transformed_occurrence, ParticipantCrossingTransformationModel) + _require_same_decision( + occurrence.decision_ref, + transformed_occurrence.decision_ref, + "delivery-attempt transformation", + ) + _validate_successor(record, transformed, require_same_subject=True) + else: + if occurrence.transformation_ref is not None: + raise ValueError("delivery attempt transformation_ref requires a transform decision") + _validate_successor(record, prior, require_same_subject=True) + _require_subject_owner(record, occurrence.owning_occurrence_ref, "delivery attempt") + return + if isinstance(occurrence, ParticipantCrossingDeliveryModel): + decision = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") + _require_permitted_decision(decision) + attempt = _resolve_record(indexes.attempts_by_id, occurrence.attempt_ref, "delivery attempt") + attempt_occurrence = attempt.occurrence + assert isinstance(attempt_occurrence, ParticipantCrossingDeliveryAttemptModel) + _require_same_decision( + occurrence.decision_ref, + attempt_occurrence.decision_ref, + "delivery", + ) + if attempt_occurrence.disposition != "attempted": + raise ValueError("participant crossing delivery requires a successful attempt disposition") + _validate_successor(record, attempt, require_same_subject=True) + if occurrence.delivery_order > occurrence.effective_order: + raise ValueError("delivery order cannot be later than its crossing fact") + _require_subject_owner(record, occurrence.owning_occurrence_ref, "delivery") + if occurrence.owning_occurrence_ref != attempt_occurrence.owning_occurrence_ref: + raise ValueError("participant crossing delivery owner must match its attempt") + return + if isinstance(occurrence, ParticipantCrossingObservationModel): + decision = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") + _require_permitted_decision(decision) + delivery = _resolve_record(indexes.deliveries_by_id, occurrence.delivery_ref, "crossing delivery") + delivery_occurrence = delivery.occurrence + assert isinstance(delivery_occurrence, ParticipantCrossingDeliveryModel) + _require_same_decision( + occurrence.decision_ref, + delivery_occurrence.decision_ref, + "observation", + ) + if delivery_occurrence.disposition != "delivered": + raise ValueError("participant crossing observation requires a delivered disposition") + _validate_successor(record, delivery, require_same_subject=False) + if occurrence.observation_order > occurrence.effective_order: + raise ValueError("observation order cannot be later than its crossing fact") + _require_subject_owner(record, occurrence.owning_observation_ref, "observation") + return + if isinstance(occurrence, ParticipantCrossingAuditModel): + audited = _resolve_record(indexes.records_by_event_id, occurrence.audited_event_ref, "audited event") + _validate_successor(record, audited, require_same_subject=True) + return + + +def _resolve_record( + index: dict[str, ParticipantCrossingOccurrenceModel], + identity: str, + label: str, +) -> ParticipantCrossingOccurrenceModel: + record = index.get(identity) + if record is None: + raise ValueError(f"{label} reference must resolve") + return record + + +def _require_permitted_decision(record: ParticipantCrossingOccurrenceModel) -> None: + occurrence = record.occurrence + assert isinstance(occurrence, ParticipantCrossingDecisionModel) + if occurrence.disposition not in { + ParticipantCrossingDecisionDisposition.PERMIT, + ParticipantCrossingDecisionDisposition.TRANSFORM, + }: + raise ValueError("crossing realization requires a permitted or transform decision") + + +def _validate_successor( + record: ParticipantCrossingOccurrenceModel, + prior: ParticipantCrossingOccurrenceModel, + *, + require_same_subject: bool, +) -> None: + occurrence = record.occurrence + prior_occurrence = prior.occurrence + if (record.participant_address, record.episode_id) != (prior.participant_address, prior.episode_id): + raise ValueError("participant crossing successor scope must match") + if ( + occurrence.direction, + occurrence.interaction_kind, + occurrence.audience_scope_ref, + occurrence.controller_ref, + occurrence.authority_basis_refs, + occurrence.policy, + occurrence.order_model, + ) != ( + prior_occurrence.direction, + prior_occurrence.interaction_kind, + prior_occurrence.audience_scope_ref, + prior_occurrence.controller_ref, + prior_occurrence.authority_basis_refs, + prior_occurrence.policy, + prior_occurrence.order_model, + ): + raise ValueError("participant crossing successor coordinates disagree") + if require_same_subject and occurrence.subject != prior_occurrence.subject: + raise ValueError("participant crossing successor subject must match") + if occurrence.effective_order <= prior_occurrence.effective_order: + raise ValueError("participant crossing successor order must follow its predecessor") + if prior.event_id not in record.predecessor_event_refs: + raise ValueError("participant crossing successor must name its predecessor event") + + +def _require_subject_owner( + record: ParticipantCrossingOccurrenceModel, + ref: str, + label: str, +) -> None: + if ref != record.occurrence.subject.subject_ref: + raise ValueError(f"participant crossing {label} owner must match its typed subject") + + +def _require_same_decision( + successor_decision_ref: str, + predecessor_decision_ref: str, + label: str, +) -> None: + if successor_decision_ref != predecessor_decision_ref: + raise ValueError(f"participant crossing {label} decision must match its predecessor") + + +def _require_declassification_gate( + operation: ParticipantCrossingOperation, + decision: ParticipantCrossingDecisionModel, +) -> None: + if ( + operation == ParticipantCrossingOperation.DECLASSIFICATION + and decision.gates.declassification != ParticipantCrossingGateDisposition.PERMIT + ): + raise ValueError("participant crossing declassification requires a permitted decision gate") + + +def _validate_transformation_graph( + records: Collection[ParticipantCrossingOccurrenceModel], +) -> None: + next_by_source: dict[SubjectKey, SubjectKey] = {} + for record in records: + occurrence = record.occurrence + assert isinstance(occurrence, ParticipantCrossingTransformationModel) + source = _subject_key(occurrence.source_subject) + result = _subject_key(occurrence.result_subject) + existing = next_by_source.setdefault(source, result) + if existing != result: + raise ValueError("transformation source identity has conflicting results") + for start in next_by_source: + seen: set[SubjectKey] = set() + current = start + while current in next_by_source: + if current in seen: + raise ValueError("participant crossing transformation cycle is not allowed") + seen.add(current) + current = next_by_source[current] + + +def _subject_key(subject: ParticipantCrossingSubjectReferenceModel) -> SubjectKey: + return (subject.subject_kind, subject.subject_ref) + + +__all__ = ["validate_participant_crossing_occurrence_context"] diff --git a/implementations/python/packages/raes_contracts/contracts/participant_crossing_vocab.py b/implementations/python/packages/raes_contracts/contracts/participant_crossing_vocab.py new file mode 100644 index 000000000..42ab5c786 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing_vocab.py @@ -0,0 +1,121 @@ +"""Closed vocabularies for API-423 participant-crossing contracts.""" + +from enum import Enum + + +class ParticipantCrossingDirection(str, Enum): + """Closed participant-boundary directions.""" + + INGRESS = "ingress" + EGRESS = "egress" + + +class ParticipantCrossingInteractionKind(str, Enum): + """Closed incumbent carrier kinds that may cross a participant boundary.""" + + ACTION_PROPOSAL = "action-proposal" + CONSTRAINED_FORM_SUBMISSION = "constrained-form-submission" + CANDIDATE_SELECTION = "candidate-selection" + APPROVAL = "approval" + DENIAL = "denial" + EXTERNAL_DIRECTION = "external-direction" + INTERVENTION = "intervention" + HANDOFF = "handoff" + OVERRIDE = "override" + CANCELLATION = "cancellation" + PARTICIPANT_INJECT_DELIVERY = "participant-inject-delivery" + OBSERVATION = "observation" + DECISION_SURFACE_PROJECTION = "decision-surface-projection" + REDACTED_OUTPUT = "redacted-output" + DISCLOSURE = "disclosure" + DELIVERY_RECEIPT = "delivery-receipt" + ACTION_RESULT = "action-result" + STATUS_PROJECTION = "status-projection" + HISTORY_PROJECTION = "history-projection" + + +class ParticipantCrossingSubjectKind(str, Enum): + """Closed typed references to incumbent participant and evidence carriers.""" + + PARTICIPANT_CONTROL_OCCURRENCE = "participant-control-occurrence" + PARTICIPANT_ACTION_CONTRACT = "participant-action-contract" + PARTICIPANT_ACTION_ADMISSION = "participant-action-admission" + PARTICIPANT_ACTION_ATTEMPT = "participant-action-attempt" + PARTICIPANT_ACTION_RESULT = "participant-action-result" + PARTICIPANT_LIFECYCLE_EVENT = "participant-lifecycle-event" + PARTICIPANT_OBSERVATION = "participant-observation" + PARTICIPANT_DECISION_SURFACE = "participant-decision-surface" + PARTICIPANT_EXPOSURE = "participant-exposure" + PARTICIPANT_INJECT_DELIVERY = "participant-inject-delivery" + PARTICIPANT_CONTEXT_VIEW = "participant-context-view" + PARTICIPANT_HISTORY_VIEW = "participant-history-view" + PARTICIPANT_STATUS_VIEW = "participant-status-view" + EXPERIMENT_EVIDENCE = "experiment-evidence" + + +class ParticipantCrossingOperation(str, Enum): + """Semantically independent participant information-flow operations.""" + + ADMISSION = "admission" + WITHHOLDING = "withholding" + PROJECTION = "projection" + MASKING = "masking" + REDACTION = "redaction" + TRANSFORMATION = "transformation" + DECLASSIFICATION = "declassification" + DISCLOSURE = "disclosure" + DELIVERY = "delivery" + CONCEALMENT = "concealment" + REVOCATION = "revocation" + AUDIT_RETENTION = "audit-retention" + + +class ParticipantCrossingGateDisposition(str, Enum): + """One deny-first decision-gate result.""" + + PERMIT = "permit" + DENY = "deny" + NOT_APPLICABLE = "not-applicable" + UNKNOWN = "unknown" + UNSUPPORTED = "unsupported" + + +class ParticipantCrossingDecisionDisposition(str, Enum): + """Overall policy disposition for one crossing request.""" + + PERMIT = "permit" + DENY = "deny" + TRANSFORM = "transform" + WITHHOLD = "withhold" + UNSUPPORTED = "unsupported" + + +class ParticipantCrossingBackendPosture(str, Enum): + """Bounded backend-support posture without a realization claim.""" + + EXACT = "exact" + BOUNDED = "bounded" + DISCLOSED_WEAK = "disclosed-weak" + UNSUPPORTED = "unsupported" + + +class ParticipantCrossingLossKind(str, Enum): + """Disclosed loss or guarantee weakening for a crossing fact.""" + + NONE = "none" + FIDELITY_LOSS = "fidelity-loss" + GUARANTEE_WEAKENING = "guarantee-weakening" + UNKNOWN = "unknown" + UNSUPPORTED = "unsupported" + + +__all__ = [ + "ParticipantCrossingBackendPosture", + "ParticipantCrossingDecisionDisposition", + "ParticipantCrossingDirection", + "ParticipantCrossingGateDisposition", + "ParticipantCrossingInteractionKind", + "ParticipantCrossingLossKind", + "ParticipantCrossingOperation", + "ParticipantCrossingSubjectKind", +] diff --git a/implementations/python/packages/raes_contracts/contracts/participant_occurrences.py b/implementations/python/packages/raes_contracts/contracts/participant_occurrences.py new file mode 100644 index 000000000..2df686681 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_occurrences.py @@ -0,0 +1,14 @@ +"""Public participant control and crossing occurrence contract surface.""" + +from .participant_control import ParticipantControlDeclarationModel, ParticipantControlOccurrenceModel +from .participant_control_validation import validate_participant_control_occurrence_context +from .participant_crossing import ParticipantCrossingOccurrenceModel +from .participant_crossing_validation import validate_participant_crossing_occurrence_context + +__all__ = [ + "ParticipantControlDeclarationModel", + "ParticipantControlOccurrenceModel", + "ParticipantCrossingOccurrenceModel", + "validate_participant_control_occurrence_context", + "validate_participant_crossing_occurrence_context", +] diff --git a/implementations/python/tests/test_api_423_participant_crossing_contracts.py b/implementations/python/tests/test_api_423_participant_crossing_contracts.py new file mode 100644 index 000000000..398434774 --- /dev/null +++ b/implementations/python/tests/test_api_423_participant_crossing_contracts.py @@ -0,0 +1,962 @@ +"""API-423 participant-crossing policy and evidence 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.validators import validate_contract_payload +from raes_contracts.contracts import ( + ParticipantCrossingOccurrenceModel, + schema_bundle, + validate_participant_crossing_occurrence_context, +) +from raes_contracts.contracts.participant_crossing import ( + ParticipantCrossingBackendPosture, + ParticipantCrossingDecisionDisposition, + ParticipantCrossingDirection, + ParticipantCrossingGateDisposition, + ParticipantCrossingInteractionKind, + ParticipantCrossingLossKind, + ParticipantCrossingOperation, + ParticipantCrossingPolicyReferenceModel, + ParticipantCrossingSubjectKind, + ParticipantCrossingSubjectReferenceModel, +) +from raes_contracts.controlled_vocabularies import load_controlled_vocabulary_catalog + +REPO_ROOT = Path(__file__).resolve().parents[3] +CONTRACT_ID = "participant-crossing-occurrence-v1" +KNOWN_EVIDENCE_REFS = { + "evidence:crossing-1", + "evidence-requirement:crossing-decision", +} +KNOWN_AUTHORITY_BASIS_REFS = { + "authority:red-team", + "authority:declassification", +} + + +def _subject( + *, + kind: str = "participant-control-occurrence", + contract_id: str = "participant-control-occurrence-v1", + ref: str = "control-occurrence.proposal.1", + revision: str = "1", +) -> dict[str, object]: + return { + "subject_kind": kind, + "contract_id": contract_id, + "subject_ref": ref, + "subject_revision": revision, + "participant_address": "participants.red.operator", + "episode_id": "episode-1", + } + + +def _policy() -> dict[str, object]: + return { + "policy_id": "participant-crossing-policy:red", + "policy_revision": "revision-3", + "policy_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "effective_order": 8, + "valid_from_order": 8, + "valid_until_order": 20, + } + + +def _envelope(occurrence: dict[str, object]) -> dict[str, object]: + return { + "event_id": f"crossing-occurrence.{occurrence['stage']}.1", + "schema_name": "participant-crossing-occurrence", + "schema_version": "1.0.0", + "event_type": "participant-crossing-occurrence", + "extension_policy": "closed", + "participant_address": "participants.red.operator", + "episode_id": "episode-1", + "occurred_at": "2026-07-26T08:00:00Z", + "recorded_at": "2026-07-26T08:00:01Z", + "ingested_at": "2026-07-26T08:00:02Z", + "clock_authority": "clock.logical", + "ordering_basis": "logical_clock", + "logical_order_ref": "order:10", + "actor_ref": "controller.human.red", + "producer_ref": "participant-runtime.red", + "provenance_refs": ["provenance:crossing-1"], + "evidence_refs": ["evidence:crossing-1"], + "object_marking_refs": ["marking:participant-control"], + "authorization_scope": "scope:red-team", + "occurrence": { + "direction": "ingress", + "interaction_kind": "action-proposal", + "audience_scope_ref": "audience:red-operator", + "subject": _subject(), + "controller_ref": "controller.human.red", + "authority_basis_refs": ["authority:red-team"], + "policy": _policy(), + "effective_order": 10, + "order_model": "logical_clock", + "backend_posture": "exact", + "loss_and_limitations": ["limitation:contract-only"], + **occurrence, + }, + } + + +def _request() -> ParticipantCrossingOccurrenceModel: + return ParticipantCrossingOccurrenceModel.model_validate( + _envelope( + { + "stage": "requested", + "request_id": "crossing-request.1", + "requested_operation": "admission", + "action_or_projection_ref": "action-contract:contain-host", + "required_evidence_refs": ["evidence-requirement:crossing-decision"], + } + ) + ) + + +def _decision( + request: ParticipantCrossingOccurrenceModel, + *, + disposition: str = "permit", + gate_override: tuple[str, str] | None = None, + required_operation: str | None = None, +) -> ParticipantCrossingOccurrenceModel: + gates = { + "caller_authorization": "permit", + "target_authorization": "permit", + "participant_authority": "permit", + "action_admission": "permit", + "visibility": "permit", + "marking_authorization": "permit", + "declassification": "not-applicable", + "backend_support": "permit", + "transformation_validity": "not-applicable", + } + if gate_override is not None: + gates[gate_override[0]] = gate_override[1] + value = _envelope( + { + "stage": "decided", + "request_ref": "crossing-request.1", + "decision_id": "crossing-decision.1", + "decision_revision": 1, + "gates": gates, + "disposition": disposition, + "reason_code": "policy-satisfied", + "required_evidence_refs": ["evidence-requirement:crossing-decision"], + **({"required_operation": required_operation} if required_operation is not None else {}), + } + ) + value["event_id"] = "crossing-occurrence.decided.1" + value["predecessor_event_refs"] = [request.event_id] + value["occurrence"]["effective_order"] = 11 + return ParticipantCrossingOccurrenceModel.model_validate(value) + + +def _attempt( + decision: ParticipantCrossingOccurrenceModel, + *, + decision_ref: str = "crossing-decision.1", + owning_occurrence_ref: str = "control-occurrence.proposal.1", + disposition: str = "attempted", +) -> ParticipantCrossingOccurrenceModel: + value = _envelope( + { + "stage": "delivery-attempted", + "decision_ref": decision_ref, + "attempt_id": "crossing-attempt.1", + "owning_occurrence_ref": owning_occurrence_ref, + "disposition": disposition, + } + ) + value["event_id"] = "crossing-occurrence.delivery-attempted.1" + value["predecessor_event_refs"] = [decision.event_id] + value["occurrence"]["effective_order"] = 12 + return ParticipantCrossingOccurrenceModel.model_validate(value) + + +def _delivery( + attempt: ParticipantCrossingOccurrenceModel, + *, + decision_ref: str = "crossing-decision.1", + disposition: str = "delivered", +) -> ParticipantCrossingOccurrenceModel: + value = _envelope( + { + "stage": "delivered", + "decision_ref": decision_ref, + "attempt_ref": "crossing-attempt.1", + "delivery_id": "crossing-delivery.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "delivery_order": 13, + "disposition": disposition, + } + ) + value["event_id"] = "crossing-occurrence.delivered.1" + value["predecessor_event_refs"] = [attempt.event_id] + value["occurrence"]["effective_order"] = 13 + return ParticipantCrossingOccurrenceModel.model_validate(value) + + +def test_crossing_request_is_a_closed_participant_runtime_fact() -> None: + record = _request() + + assert record.occurrence.stage == "requested" + assert record.participant_address == "participants.red.operator" + assert record.occurrence.subject.subject_ref == "control-occurrence.proposal.1" + + +def test_crossing_request_rejects_payload_policy_and_secret_bags() -> None: + payload = _envelope( + { + "stage": "requested", + "request_id": "crossing-request.1", + "requested_operation": "admission", + "action_or_projection_ref": "action-contract:contain-host", + "required_evidence_refs": ["evidence-requirement:crossing-decision"], + "payload": {"prompt": "hidden"}, + "policy": {"body": "permit if secret"}, + "credentials": {"token": "secret"}, + } + ) + + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + ParticipantCrossingOccurrenceModel.model_validate(payload) + + +@pytest.mark.parametrize( + ("stage", "detail"), + [ + ( + "decided", + { + "request_ref": "crossing-request.1", + "decision_id": "crossing-decision.1", + "decision_revision": 1, + "gates": { + "caller_authorization": "permit", + "target_authorization": "permit", + "participant_authority": "permit", + "action_admission": "permit", + "visibility": "permit", + "marking_authorization": "permit", + "declassification": "not-applicable", + "backend_support": "permit", + "transformation_validity": "not-applicable", + }, + "disposition": "permit", + "reason_code": "policy-satisfied", + "required_evidence_refs": ["evidence-requirement:crossing-decision"], + }, + ), + ( + "transformed", + { + "decision_ref": "crossing-decision.1", + "transformation_id": "crossing-transformation.1", + "operation": "redaction", + "source_subject": _subject(), + "result_subject": _subject( + kind="participant-action-contract", + ref="action-contract:redacted-contain-host", + ), + "rule_ref": "redaction-rule:participant-output", + "rule_revision": "2", + "source_marking_refs": ["marking:participant-control"], + "result_marking_refs": ["marking:participant-control"], + }, + ), + ( + "disclosed", + { + "decision_ref": "crossing-decision.1", + "disclosure_id": "crossing-disclosure.1", + "operation": "disclosure", + "source_marking_refs": ["marking:participant-control"], + "result_marking_refs": ["marking:participant-control"], + }, + ), + ( + "delivery-attempted", + { + "decision_ref": "crossing-decision.1", + "attempt_id": "crossing-attempt.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "disposition": "attempted", + }, + ), + ( + "delivered", + { + "decision_ref": "crossing-decision.1", + "attempt_ref": "crossing-attempt.1", + "delivery_id": "crossing-delivery.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "delivery_order": 13, + "disposition": "delivered", + }, + ), + ( + "observed", + { + "decision_ref": "crossing-decision.1", + "delivery_ref": "crossing-delivery.1", + "observation_id": "crossing-observation.1", + "owning_observation_ref": "observation:red:13", + "observation_order": 14, + }, + ), + ( + "audited", + { + "audited_event_ref": "crossing-occurrence.delivered.1", + "audit_record_ref": "audit:participant-crossing:1", + "retained_evidence_refs": ["evidence:crossing-1"], + }, + ), + ], +) +def test_each_crossing_fact_stage_is_a_distinct_closed_variant( + stage: str, + detail: dict[str, object], +) -> None: + value = _envelope({"stage": stage, **detail}) + if stage == "transformed": + value["occurrence"]["subject"] = detail["result_subject"] + + parsed = ParticipantCrossingOccurrenceModel.model_validate(value) + + assert parsed.occurrence.stage == stage + + +def test_deny_first_decision_rejects_permit_when_a_required_gate_denies() -> None: + request = _request() + + with pytest.raises(ValidationError, match="every applicable gate"): + _decision( + request, + gate_override=("participant_authority", "deny"), + ) + + +def test_context_validator_accepts_ordered_requested_decided_and_realized_facts() -> None: + request = _request() + decision = _decision(request) + attempt_value = _envelope( + { + "stage": "delivery-attempted", + "decision_ref": "crossing-decision.1", + "attempt_id": "crossing-attempt.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "disposition": "attempted", + } + ) + attempt_value["event_id"] = "crossing-occurrence.delivery-attempted.1" + attempt_value["predecessor_event_refs"] = [decision.event_id] + attempt_value["occurrence"]["effective_order"] = 12 + attempt = ParticipantCrossingOccurrenceModel.model_validate(attempt_value) + delivery_value = _envelope( + { + "stage": "delivered", + "decision_ref": "crossing-decision.1", + "attempt_ref": "crossing-attempt.1", + "delivery_id": "crossing-delivery.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "delivery_order": 13, + "disposition": "delivered", + } + ) + delivery_value["event_id"] = "crossing-occurrence.delivered.1" + delivery_value["predecessor_event_refs"] = [attempt.event_id] + delivery_value["occurrence"]["effective_order"] = 13 + delivery = ParticipantCrossingOccurrenceModel.model_validate(delivery_value) + + validate_participant_crossing_occurrence_context( + [request, decision, attempt, delivery], + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_context_validator_rejects_unknown_subject_stale_policy_and_missing_evidence() -> None: + request = _request() + policy = ParticipantCrossingPolicyReferenceModel.model_validate(_policy()) + + with pytest.raises(ValueError, match="typed subject reference must resolve"): + validate_participant_crossing_occurrence_context( + [request], + known_subjects=[], + policies=[policy], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + with pytest.raises(ValueError, match="policy revision must resolve"): + validate_participant_crossing_occurrence_context( + [request], + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], + policies=[], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + with pytest.raises(ValueError, match="evidence reference must resolve"): + validate_participant_crossing_occurrence_context( + [request], + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], + policies=[policy], + known_evidence_refs=set(), + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_context_validator_rejects_unresolved_stage_local_evidence() -> None: + request = _request() + + with pytest.raises(ValueError, match="stage-local evidence reference must resolve"): + validate_participant_crossing_occurrence_context( + [request], + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs={"evidence:crossing-1"}, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_delivery_must_use_the_attempts_exact_decision_and_successful_disposition() -> None: + request = _request() + decision = _decision(request) + second_value = decision.model_dump(mode="json") + second_value["event_id"] = "crossing-occurrence.decided.2" + second_value["occurrence"]["decision_id"] = "crossing-decision.2" + second = ParticipantCrossingOccurrenceModel.model_validate(second_value) + attempt = _attempt(decision) + wrong_decision_delivery = _delivery(attempt, decision_ref="crossing-decision.2") + + with pytest.raises(ValueError, match="delivery decision must match its predecessor"): + validate_participant_crossing_occurrence_context( + [request, decision, second, attempt, wrong_decision_delivery], + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + failed_attempt = _attempt(decision, disposition="failed") + delivery_after_failure = _delivery(failed_attempt) + with pytest.raises(ValueError, match="delivery requires a successful attempt disposition"): + validate_participant_crossing_occurrence_context( + [request, decision, failed_attempt, delivery_after_failure], + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_delivery_ownership_must_resolve_the_exact_typed_subject() -> None: + request = _request() + decision = _decision(request) + foreign_subject = _subject( + kind="participant-observation", + contract_id="participant-observation-envelope-v1", + ref="observation:foreign", + ) + attempt = _attempt(decision, owning_occurrence_ref="observation:foreign") + + with pytest.raises(ValueError, match="owner must match its typed subject"): + validate_participant_crossing_occurrence_context( + [request, decision, attempt], + known_subjects=[ + ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), + ParticipantCrossingSubjectReferenceModel.model_validate(foreign_subject), + ], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_context_validator_rejects_delivery_without_attempt() -> None: + request = _request() + decision = _decision(request) + delivery_value = _envelope( + { + "stage": "delivered", + "decision_ref": "crossing-decision.1", + "attempt_ref": "crossing-attempt.missing", + "delivery_id": "crossing-delivery.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "delivery_order": 13, + "disposition": "delivered", + } + ) + delivery_value["event_id"] = "crossing-occurrence.delivered.1" + delivery_value["predecessor_event_refs"] = [decision.event_id] + delivery_value["occurrence"]["effective_order"] = 13 + delivery = ParticipantCrossingOccurrenceModel.model_validate(delivery_value) + + with pytest.raises(ValueError, match="delivery attempt reference must resolve"): + validate_participant_crossing_occurrence_context( + [request, decision, delivery], + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_transform_decision_delivers_the_new_subject_after_the_transformation() -> None: + request = _request() + decision = _decision( + request, + disposition="transform", + gate_override=("transformation_validity", "permit"), + required_operation="redaction", + ) + result_subject = _subject( + kind="participant-action-contract", + contract_id="participant-action-contract-v1", + ref="action-contract:redacted-contain-host", + ) + transformation_value = _envelope( + { + "stage": "transformed", + "decision_ref": "crossing-decision.1", + "transformation_id": "crossing-transformation.1", + "operation": "redaction", + "source_subject": _subject(), + "result_subject": result_subject, + "rule_ref": "redaction-rule:participant-output", + "rule_revision": "2", + "source_marking_refs": ["marking:participant-control"], + "result_marking_refs": ["marking:participant-control"], + } + ) + transformation_value["event_id"] = "crossing-occurrence.transformed.1" + transformation_value["predecessor_event_refs"] = [decision.event_id] + transformation_value["occurrence"]["effective_order"] = 12 + transformation_value["occurrence"]["subject"] = result_subject + transformation = ParticipantCrossingOccurrenceModel.model_validate(transformation_value) + attempt_value = _envelope( + { + "stage": "delivery-attempted", + "decision_ref": "crossing-decision.1", + "transformation_ref": "crossing-transformation.1", + "attempt_id": "crossing-attempt.1", + "owning_occurrence_ref": "action-contract:redacted-contain-host", + "disposition": "attempted", + } + ) + attempt_value["event_id"] = "crossing-occurrence.delivery-attempted.1" + attempt_value["predecessor_event_refs"] = [transformation.event_id] + attempt_value["occurrence"]["effective_order"] = 13 + attempt_value["occurrence"]["subject"] = result_subject + attempt = ParticipantCrossingOccurrenceModel.model_validate(attempt_value) + + validate_participant_crossing_occurrence_context( + [request, decision, transformation, attempt], + known_subjects=[ + ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), + ParticipantCrossingSubjectReferenceModel.model_validate(result_subject), + ], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_transform_must_apply_the_exact_operation_prescribed_by_its_decision() -> None: + request = _request() + decision = _decision( + request, + disposition="transform", + required_operation="redaction", + ) + result_subject = _subject( + kind="participant-action-contract", + contract_id="participant-action-contract-v1", + ref="action-contract:masked-contain-host", + ) + value = _envelope( + { + "stage": "transformed", + "decision_ref": "crossing-decision.1", + "transformation_id": "crossing-transformation.1", + "operation": "masking", + "source_subject": _subject(), + "result_subject": result_subject, + "rule_ref": "masking-rule:participant-output", + "rule_revision": "1", + "source_marking_refs": ["marking:participant-control"], + "result_marking_refs": ["marking:participant-control"], + } + ) + value["event_id"] = "crossing-occurrence.transformed.1" + value["predecessor_event_refs"] = [decision.event_id] + value["occurrence"]["effective_order"] = 12 + value["occurrence"]["subject"] = result_subject + transformation = ParticipantCrossingOccurrenceModel.model_validate(value) + + with pytest.raises(ValueError, match="operation must match the decision requirement"): + validate_participant_crossing_occurrence_context( + [request, decision, transformation], + known_subjects=[ + ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), + ParticipantCrossingSubjectReferenceModel.model_validate(result_subject), + ], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_transformation_rejects_marking_weakening_without_declassification() -> None: + result_subject = _subject( + kind="participant-action-contract", + contract_id="participant-action-contract-v1", + ref="action-contract:redacted-contain-host", + ) + value = _envelope( + { + "stage": "transformed", + "decision_ref": "crossing-decision.1", + "transformation_id": "crossing-transformation.1", + "operation": "redaction", + "source_subject": _subject(), + "result_subject": result_subject, + "rule_ref": "redaction-rule:participant-output", + "rule_revision": "2", + "source_marking_refs": ["marking:restricted"], + "result_marking_refs": ["marking:public"], + } + ) + value["occurrence"]["subject"] = result_subject + + with pytest.raises(ValidationError, match="inherit source markings"): + ParticipantCrossingOccurrenceModel.model_validate(value) + + +def test_declassification_basis_is_reserved_for_declassification_operations() -> None: + result_subject = _subject( + kind="participant-action-contract", + contract_id="participant-action-contract-v1", + ref="action-contract:redacted-contain-host", + ) + value = _envelope( + { + "stage": "transformed", + "decision_ref": "crossing-decision.1", + "transformation_id": "crossing-transformation.1", + "operation": "redaction", + "source_subject": _subject(), + "result_subject": result_subject, + "rule_ref": "redaction-rule:participant-output", + "rule_revision": "2", + "source_marking_refs": ["marking:restricted"], + "result_marking_refs": ["marking:public"], + "declassification_basis_ref": "authority:untrusted", + } + ) + value["occurrence"]["subject"] = result_subject + + with pytest.raises(ValidationError, match="reserved for declassification"): + ParticipantCrossingOccurrenceModel.model_validate(value) + + +def test_declassification_basis_must_resolve_as_declared_authority() -> None: + request = _request() + decision = _decision( + request, + disposition="transform", + gate_override=("declassification", "permit"), + required_operation="declassification", + ) + result_subject = _subject( + kind="participant-action-contract", + contract_id="participant-action-contract-v1", + ref="action-contract:declassified-contain-host", + ) + value = _envelope( + { + "stage": "transformed", + "decision_ref": "crossing-decision.1", + "transformation_id": "crossing-transformation.1", + "operation": "declassification", + "source_subject": _subject(), + "result_subject": result_subject, + "rule_ref": "declassification-rule:participant-output", + "rule_revision": "1", + "source_marking_refs": ["marking:restricted"], + "result_marking_refs": ["marking:public"], + "declassification_basis_ref": "authority:untrusted", + } + ) + value["event_id"] = "crossing-occurrence.transformed.1" + value["predecessor_event_refs"] = [decision.event_id] + value["occurrence"]["effective_order"] = 12 + value["occurrence"]["subject"] = result_subject + transformation = ParticipantCrossingOccurrenceModel.model_validate(value) + + with pytest.raises(ValueError, match="declassification authority basis must resolve"): + validate_participant_crossing_occurrence_context( + [request, decision, transformation], + known_subjects=[ + ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), + ParticipantCrossingSubjectReferenceModel.model_validate(result_subject), + ], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_crossing_rejects_a_policy_revision_that_becomes_effective_later() -> None: + value = _envelope( + { + "stage": "requested", + "request_id": "crossing-request.1", + "requested_operation": "admission", + "action_or_projection_ref": "action-contract:contain-host", + "required_evidence_refs": ["evidence-requirement:crossing-decision"], + } + ) + value["occurrence"]["policy"]["effective_order"] = 12 + + with pytest.raises(ValidationError, match="future policy revision"): + ParticipantCrossingOccurrenceModel.model_validate(value) + + +def test_observation_and_audit_require_their_own_ordered_occurrence_facts() -> None: + request = _request() + decision = _decision(request) + attempt_value = _envelope( + { + "stage": "delivery-attempted", + "decision_ref": "crossing-decision.1", + "attempt_id": "crossing-attempt.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "disposition": "attempted", + } + ) + attempt_value["event_id"] = "crossing-occurrence.delivery-attempted.1" + attempt_value["predecessor_event_refs"] = [decision.event_id] + attempt_value["occurrence"]["effective_order"] = 12 + attempt = ParticipantCrossingOccurrenceModel.model_validate(attempt_value) + delivery_value = _envelope( + { + "stage": "delivered", + "decision_ref": "crossing-decision.1", + "attempt_ref": "crossing-attempt.1", + "delivery_id": "crossing-delivery.1", + "owning_occurrence_ref": "control-occurrence.proposal.1", + "delivery_order": 13, + "disposition": "delivered", + } + ) + delivery_value["event_id"] = "crossing-occurrence.delivered.1" + delivery_value["predecessor_event_refs"] = [attempt.event_id] + delivery_value["occurrence"]["effective_order"] = 13 + delivery = ParticipantCrossingOccurrenceModel.model_validate(delivery_value) + observation_subject = _subject( + kind="participant-observation", + contract_id="participant-observation-envelope-v1", + ref="observation:red:13", + ) + observation_value = _envelope( + { + "stage": "observed", + "decision_ref": "crossing-decision.1", + "delivery_ref": "crossing-delivery.1", + "observation_id": "crossing-observation.1", + "owning_observation_ref": "observation:red:13", + "observation_order": 14, + } + ) + observation_value["event_id"] = "crossing-occurrence.observed.1" + observation_value["predecessor_event_refs"] = [delivery.event_id] + observation_value["occurrence"]["effective_order"] = 14 + observation_value["occurrence"]["subject"] = observation_subject + observation = ParticipantCrossingOccurrenceModel.model_validate(observation_value) + audit_value = _envelope( + { + "stage": "audited", + "audited_event_ref": observation.event_id, + "audit_record_ref": "audit:participant-crossing:1", + "retained_evidence_refs": ["evidence:crossing-1"], + } + ) + audit_value["event_id"] = "crossing-occurrence.audited.1" + audit_value["predecessor_event_refs"] = [observation.event_id] + audit_value["occurrence"]["effective_order"] = 15 + audit_value["occurrence"]["subject"] = observation_subject + audit = ParticipantCrossingOccurrenceModel.model_validate(audit_value) + + validate_participant_crossing_occurrence_context( + [request, decision, attempt, delivery, observation, audit], + known_subjects=[ + ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), + ParticipantCrossingSubjectReferenceModel.model_validate(observation_subject), + ], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_context_validator_rejects_a_transformation_cycle() -> None: + request_a = _request() + decision_a = _decision( + request_a, + disposition="transform", + gate_override=("transformation_validity", "permit"), + required_operation="redaction", + ) + subject_a = _subject() + subject_b = _subject( + kind="participant-action-contract", + contract_id="participant-action-contract-v1", + ref="action-contract:redacted-contain-host", + ) + transform_a_value = _envelope( + { + "stage": "transformed", + "decision_ref": "crossing-decision.1", + "transformation_id": "crossing-transformation.1", + "operation": "redaction", + "source_subject": subject_a, + "result_subject": subject_b, + "rule_ref": "redaction-rule:a-to-b", + "rule_revision": "1", + "source_marking_refs": ["marking:participant-control"], + "result_marking_refs": ["marking:participant-control"], + } + ) + transform_a_value["event_id"] = "crossing-occurrence.transformed.1" + transform_a_value["predecessor_event_refs"] = [decision_a.event_id] + transform_a_value["occurrence"]["effective_order"] = 12 + transform_a_value["occurrence"]["subject"] = subject_b + transform_a = ParticipantCrossingOccurrenceModel.model_validate(transform_a_value) + request_b_value = _envelope( + { + "stage": "requested", + "request_id": "crossing-request.2", + "requested_operation": "admission", + "action_or_projection_ref": "action-contract:redacted-contain-host", + "required_evidence_refs": ["evidence-requirement:crossing-decision"], + } + ) + request_b_value["event_id"] = "crossing-occurrence.requested.2" + request_b_value["occurrence"]["effective_order"] = 13 + request_b_value["occurrence"]["subject"] = subject_b + request_b = ParticipantCrossingOccurrenceModel.model_validate(request_b_value) + decision_b_value = _envelope( + { + "stage": "decided", + "request_ref": "crossing-request.2", + "decision_id": "crossing-decision.2", + "decision_revision": 1, + "gates": decision_a.occurrence.gates.model_dump(mode="json"), + "disposition": "transform", + "reason_code": "transformation-required", + "required_operation": "redaction", + "required_evidence_refs": ["evidence-requirement:crossing-decision"], + } + ) + decision_b_value["event_id"] = "crossing-occurrence.decided.2" + decision_b_value["predecessor_event_refs"] = [request_b.event_id] + decision_b_value["occurrence"]["effective_order"] = 14 + decision_b_value["occurrence"]["subject"] = subject_b + decision_b = ParticipantCrossingOccurrenceModel.model_validate(decision_b_value) + transform_b_value = _envelope( + { + "stage": "transformed", + "decision_ref": "crossing-decision.2", + "transformation_id": "crossing-transformation.2", + "operation": "redaction", + "source_subject": subject_b, + "result_subject": subject_a, + "rule_ref": "redaction-rule:b-to-a", + "rule_revision": "1", + "source_marking_refs": ["marking:participant-control"], + "result_marking_refs": ["marking:participant-control"], + } + ) + transform_b_value["event_id"] = "crossing-occurrence.transformed.2" + transform_b_value["predecessor_event_refs"] = [decision_b.event_id] + transform_b_value["occurrence"]["effective_order"] = 15 + transform_b = ParticipantCrossingOccurrenceModel.model_validate(transform_b_value) + + with pytest.raises(ValueError, match="transformation cycle"): + validate_participant_crossing_occurrence_context( + [request_a, decision_a, transform_a, request_b, decision_b, transform_b], + known_subjects=[ + ParticipantCrossingSubjectReferenceModel.model_validate(subject_a), + ParticipantCrossingSubjectReferenceModel.model_validate(subject_b), + ], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], + known_evidence_refs=KNOWN_EVIDENCE_REFS, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + +def test_published_schema_fixtures_bundle_and_consumer_match_the_model() -> None: + schema = schema_bundle()[CONTRACT_ID] + published_path = REPO_ROOT / "contracts" / "schemas" / "participant-runtime" / f"{CONTRACT_ID}.json" + published = json.loads(published_path.read_text(encoding="utf-8")) + assert published == schema + assert schema["additionalProperties"] is False + assert schema["x-aces-invariants"] + + fixture_root = REPO_ROOT / "contracts" / "fixtures" / "participant-runtime" / CONTRACT_ID + valid_paths = sorted((fixture_root / "valid").glob("*.json")) + invalid_paths = sorted((fixture_root / "invalid").glob("*.json")) + assert valid_paths + assert invalid_paths + validator = Draft202012Validator(schema) + for path in valid_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + validator.validate(payload) + parsed = ParticipantCrossingOccurrenceModel.model_validate(payload) + assert ParticipantCrossingOccurrenceModel.model_validate_json(parsed.model_dump_json()) == parsed + assert not validate_contract_payload(CONTRACT_ID, payload) + for path in invalid_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + assert list(validator.iter_errors(payload)) + with pytest.raises(ValidationError): + ParticipantCrossingOccurrenceModel.model_validate(payload) + + +def test_crossing_closed_vocabularies_have_one_concept_authority() -> None: + catalog = load_controlled_vocabulary_catalog() + expected = { + "participant-crossing-directions": {value.value for value in ParticipantCrossingDirection}, + "participant-crossing-interaction-kinds": {value.value for value in ParticipantCrossingInteractionKind}, + "participant-crossing-subject-kinds": {value.value for value in ParticipantCrossingSubjectKind}, + "participant-crossing-operations": {value.value for value in ParticipantCrossingOperation}, + "participant-crossing-gate-dispositions": {value.value for value in ParticipantCrossingGateDisposition}, + "participant-crossing-decision-dispositions": {value.value for value in ParticipantCrossingDecisionDisposition}, + "participant-crossing-backend-postures": {value.value for value in ParticipantCrossingBackendPosture}, + "participant-crossing-loss-kinds": {value.value for value in ParticipantCrossingLossKind}, + "participant-crossing-stages": { + "requested", + "decided", + "transformed", + "disclosed", + "delivery-attempted", + "delivered", + "observed", + "audited", + }, + } + + for vocabulary_id, terms in expected.items(): + vocabulary = catalog.vocabularies[vocabulary_id] + assert vocabulary.extension_policy == "closed" + assert set(vocabulary.terms) == terms diff --git a/implementations/python/tests/test_participant_backend_contracts.py b/implementations/python/tests/test_participant_backend_contracts.py index 2599d1b57..2d8147ea6 100644 --- a/implementations/python/tests/test_participant_backend_contracts.py +++ b/implementations/python/tests/test_participant_backend_contracts.py @@ -12,6 +12,7 @@ VIEW_SCOPE_PROJECTED_FIELDS, ParticipantBehaviorHistoryEventModel, ParticipantContextViewModel, + ParticipantCrossingOccurrenceModel, ParticipantEpisodeHistoryEventModel, ParticipantEpisodeStateModel, ParticipantHistoryViewBehaviorEventModel, @@ -37,6 +38,7 @@ "participant-shared-state-record-v1": ParticipantSharedStateRecordModel, "participant-joint-action-record-v1": ParticipantJointActionRecordModel, "participant-time-management-context-v1": ParticipantTimeManagementContextModel, + "participant-crossing-occurrence-v1": ParticipantCrossingOccurrenceModel, "participant-outcome-report-v1": ParticipantOutcomeReportModel, } CONTROL_PLANE_VIEW_FIXTURE_MODELS = { diff --git a/implementations/python/tests/test_sem_227_shared_time_model.py b/implementations/python/tests/test_sem_227_shared_time_model.py index dfac8a611..2b63b0a1b 100644 --- a/implementations/python/tests/test_sem_227_shared_time_model.py +++ b/implementations/python/tests/test_sem_227_shared_time_model.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest -from hypothesis import given +from hypothesis import given, settings from hypothesis import strategies as st from raes._errors import SDLParseError, SDLValidationError from raes.parser import parse_sdl, parse_sdl_file @@ -237,6 +237,7 @@ def test_shared_time_model_references_follow_module_namespacing(tmp_path: Path) assert constraint.subject_refs == ["nodes.shared.workstation"] +@settings(deadline=None) @given(st.lists(st.sampled_from(("advance", "reset")), min_size=1, max_size=30)) def test_clock_transition_history_is_append_only_across_generated_lifecycles( operations: list[str], diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index 21ce7b448..233c37a4e 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -71,6 +71,7 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: "participant-joint-action-record-v1", "participant-time-management-context-v1", "participant-control-occurrence-v1", + "participant-crossing-occurrence-v1", "participant-outcome-report-v1", "runtime-fact-binding-plane-v1", }: From dc3d223672c2893090ef4ef23b41bd8cee24e1fd Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 06:53:18 +0200 Subject: [PATCH 04/55] Fix SonarCloud findings (cycle 1) --- .../raes_contracts/contracts/__init__.py | 14 +- .../contracts/experiment_bindings.py | 22 +- .../contracts/experiment_spec.py | 51 ++-- .../raes_contracts/experiment_bindings.py | 3 +- .../participant_configuration.py | 32 ++- implementations/python/pyproject.toml | 1 + .../python/tests/test_experiment_bindings.py | 249 +++++++++--------- 7 files changed, 192 insertions(+), 180 deletions(-) diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index b59807c07..35e9638cc 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -1,7 +1,5 @@ """Schema-first external contract models for RAES artifact boundaries.""" -# ruff: noqa: F405 - this package intentionally re-exports subdomain __all__ surfaces - from __future__ import annotations from ..versions import ( @@ -117,9 +115,9 @@ ExperimentDerivedMeasureReferenceModel, ExperimentMeasurementChannelReferenceModel, ) -from .experiment_bindings import * # noqa: F403 - package-level contract re-export -from .experiment_capture import * # noqa: F403 - package-level contract re-export -from .experiment_disclosure import * # noqa: F403 - package-level contract re-export +from .experiment_bindings import * +from .experiment_capture import * +from .experiment_disclosure import * from .experiment_evidence import ( ExperimentDerivedMeasureMethodModel, ExperimentDerivedMeasureModel, @@ -392,7 +390,8 @@ "ControlledVocabularyTermModel", "ContractModel", "ExperimentAnalysisPlanModel", "NIST_CSF_DEFENSIVE_CATEGORIES_SOURCE_SCHEMA_VERSION", "NistCsfDefensiveCategorySourceModel", "NistCsfDefensiveCategorySourceTermModel", - "ApparatusBindingTargetModel", "BindingOwnerModel", "BindingScalarType", "ConfigurationTargetDeclarationModel", "ConfigurationTargetRegistryModel", + "ApparatusBindingTargetModel", "BindingOwnerModel", "BindingScalarType", + "ConfigurationTargetDeclarationModel", "ConfigurationTargetRegistryModel", "ExperimentApparatusCompatibilityReferenceModel", "ExperimentApparatusComponentModel", "ExperimentApparatusConstraintModel", "ExperimentApparatusContextModel", "ExperimentArtifactRefModel", "ExperimentAugmentationDisclosureModel", "ExperimentBackendReferenceModel", @@ -414,7 +413,8 @@ "ExperimentStatisticalMethodModel", "ExperimentStochasticControlModel", "ExperimentStudyFactorModel", "ExperimentStudyMembershipModel", "ExperimentStudyModel", "ExperimentTaskReferenceModel", "ExperimentTaskModel", "ExperimentUncertaintyMethodModel", "ExperimentValidityNoteModel", - "LiteralBindingValueModel", "ParticipantConfigurationModel", "ParticipantConfigurationResultModel", "ParticipantImplementationBindingTargetModel", + "LiteralBindingValueModel", "ParticipantConfigurationModel", "ParticipantConfigurationResultModel", + "ParticipantImplementationBindingTargetModel", "RealizedBindingProvenanceModel", "ScenarioBindingTargetModel", "SecretReferenceBindingValueModel", "EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION", "EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION", "EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION", diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py b/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py index 3d7137572..dbce5f081 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py @@ -379,16 +379,18 @@ def __get_pydantic_json_schema__( def _binding_value_matches_type(value: BindingScalar, value_type: BindingScalarType) -> bool: if value_type == BindingScalarType.NULL: - return value is None - if value_type == BindingScalarType.BOOLEAN: - return isinstance(value, bool) - if value_type == BindingScalarType.INTEGER: - return isinstance(value, int) and not isinstance(value, bool) - if value_type == BindingScalarType.NUMBER: - return isinstance(value, (int, float)) and not isinstance(value, bool) - if value_type == BindingScalarType.STRING: - return isinstance(value, str) - return False + matches = value is None + elif value_type == BindingScalarType.BOOLEAN: + matches = isinstance(value, bool) + elif value_type == BindingScalarType.INTEGER: + matches = isinstance(value, int) and not isinstance(value, bool) + elif value_type == BindingScalarType.NUMBER: + matches = isinstance(value, (int, float)) and not isinstance(value, bool) + elif value_type == BindingScalarType.STRING: + matches = isinstance(value, str) + else: + matches = False + return matches def _validate_realized_bindings(bindings: list[RealizedBindingProvenanceModel]) -> None: diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_spec.py b/implementations/python/packages/raes_contracts/contracts/experiment_spec.py index b61832d00..ba8d45d38 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_spec.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_spec.py @@ -12,7 +12,7 @@ from .base import BehavioralClaimBindingModel, ContractModel, NonEmptyString, PositiveInteger from .experiment_apparatus import ExperimentClockContextModel, ExperimentStochasticControlModel from .experiment_artifacts import ExperimentArtifactRefModel -from .experiment_bindings import ExperimentBindingDescriptorSetModel +from .experiment_bindings import ExperimentBindingDescriptorModel, ExperimentBindingDescriptorSetModel from .experiment_capture import ExperimentValidityNoteModel from .experiment_disclosure import ExperimentApparatusConstraintModel from .experiment_manifest_references import ExperimentCaptureSpecReferenceModel @@ -318,6 +318,30 @@ def __get_pydantic_json_schema__( return json_schema +def _validate_binding_descriptor_source( + descriptor: ExperimentBindingDescriptorModel, + factors: dict[NonEmptyString, ExperimentStudyFactorModel], + allocation: ExperimentRunAllocationPlanModel, +) -> str: + factor = factors.get(descriptor.source_factor_id) + if factor is None: + raise ValueError(f"binding source factor {descriptor.source_factor_id!r} must reference a declared factor") + if descriptor.source_factor_level_id not in factor.levels: + raise ValueError( + f"binding source factor level {descriptor.source_factor_level_id!r} must be declared " + f"by factor {descriptor.source_factor_id!r}" + ) + assignment = allocation.condition_assignments.get(descriptor.source_condition_id) + if assignment is None: + raise ValueError( + f"binding source condition {descriptor.source_condition_id!r} must reference an allocation condition" + ) + assigned_level = assignment.factor_levels.get(descriptor.source_factor_id) + if assigned_level != descriptor.source_factor_level_id: + raise ValueError("binding source factor level must match its condition assignment") + return descriptor.source_condition_id + + class ExperimentSpecModel(ContractModel): """Pre-run experiment authoring input: a design that binds a task to a run plan. @@ -377,27 +401,10 @@ def _validate_binding_descriptors(self) -> None: raise ValueError( "explicit binding semantics reject legacy required_parameters: " + ", ".join(legacy_conditions) ) - covered_conditions: set[str] = set() - for descriptor in self.binding_descriptors.descriptors: - factor = self.factors.get(descriptor.source_factor_id) - if factor is None: - raise ValueError( - f"binding source factor {descriptor.source_factor_id!r} must reference a declared factor" - ) - if descriptor.source_factor_level_id not in factor.levels: - raise ValueError( - f"binding source factor level {descriptor.source_factor_level_id!r} must be declared " - f"by factor {descriptor.source_factor_id!r}" - ) - assignment = allocation.condition_assignments.get(descriptor.source_condition_id) - if assignment is None: - raise ValueError( - f"binding source condition {descriptor.source_condition_id!r} must reference an allocation condition" - ) - assigned_level = assignment.factor_levels.get(descriptor.source_factor_id) - if assigned_level != descriptor.source_factor_level_id: - raise ValueError("binding source factor level must match its condition assignment") - covered_conditions.add(descriptor.source_condition_id) + covered_conditions = { + _validate_binding_descriptor_source(descriptor, self.factors, allocation) + for descriptor in self.binding_descriptors.descriptors + } missing_conditions = sorted(set(allocation.compared_conditions) - covered_conditions) if missing_conditions: raise ValueError( diff --git a/implementations/python/packages/raes_contracts/experiment_bindings.py b/implementations/python/packages/raes_contracts/experiment_bindings.py index 6d38fa55f..ad3e3fb15 100644 --- a/implementations/python/packages/raes_contracts/experiment_bindings.py +++ b/implementations/python/packages/raes_contracts/experiment_bindings.py @@ -66,7 +66,8 @@ def validate_experiment_binding_targets( canonical_target = _resolve_participant_target(descriptor, target, participant_manifests) elif isinstance(target, ApparatusBindingTargetModel): canonical_target = _resolve_apparatus_target(descriptor, target, apparatus_manifests) - else: # pragma: no cover - the discriminated union is closed before dispatch + # The discriminated union is closed before dispatch. + else: # pragma: no cover raise ValueError("unknown binding plane") admitted.append(descriptor.model_copy(update={"target": canonical_target})) return ExperimentBindingDescriptorSetModel( diff --git a/implementations/python/packages/raes_contracts/participant_configuration.py b/implementations/python/packages/raes_contracts/participant_configuration.py index 0d37624e5..589cfa9c9 100644 --- a/implementations/python/packages/raes_contracts/participant_configuration.py +++ b/implementations/python/packages/raes_contracts/participant_configuration.py @@ -118,20 +118,30 @@ def _validate_owner_normalization( raise ValueError("participant configuration validator must preserve the complete canonical target set") original_by_target = {entry.target_id: entry for entry in original.values} for entry in normalized.values: - original_entry = original_by_target[entry.target_id] - declaration = declarations[entry.target_id] - if entry.origin != original_entry.origin: - raise ValueError("participant configuration validator must preserve default/override origin") - if entry.value_type != declaration.value_type: - raise ValueError("participant configuration validator must preserve declared value types") - if entry.value.kind != original_entry.value.kind: - raise ValueError("participant configuration validator must preserve literal/secret-reference disposition") - if entry.value.kind == "secret-reference" and entry.value != original_entry.value: - raise ValueError("participant configuration validator must preserve secret-reference identity") - declaration.validate_value(entry.value) + _validate_normalized_entry( + entry, + original_by_target[entry.target_id], + declarations[entry.target_id], + ) return normalized +def _validate_normalized_entry( + entry: RealizedConfigurationValueModel, + original_entry: RealizedConfigurationValueModel, + declaration: ConfigurationTargetDeclarationModel, +) -> None: + if entry.origin != original_entry.origin: + raise ValueError("participant configuration validator must preserve default/override origin") + if entry.value_type != declaration.value_type: + raise ValueError("participant configuration validator must preserve declared value types") + if entry.value.kind != original_entry.value.kind: + raise ValueError("participant configuration validator must preserve literal/secret-reference disposition") + if entry.value.kind == "secret-reference" and entry.value != original_entry.value: + raise ValueError("participant configuration validator must preserve secret-reference identity") + declaration.validate_value(entry.value) + + def validate_participant_configuration_selection( selection: ParticipantImplementationSelectionModel, result: ParticipantConfigurationResultModel, diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index f12402bda..6ed8c3e94 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -166,6 +166,7 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "tests/**" = ["S101", "S105", "S106", "S108", "E402", "SIM105", "F841"] +"packages/raes_contracts/contracts/__init__.py" = ["F403", "F405"] # intentional package re-exports "packages/raes_cli/**" = ["B008"] # typer requires function calls in defaults "packages/raes_runtime/control_plane_api.py" = ["B008"] # fastapi Depends() pattern "packages/raes_runtime/control_plane.py" = ["S112"] # intentional exception suppression diff --git a/implementations/python/tests/test_experiment_bindings.py b/implementations/python/tests/test_experiment_bindings.py index 961dfa3c0..44f793ebd 100644 --- a/implementations/python/tests/test_experiment_bindings.py +++ b/implementations/python/tests/test_experiment_bindings.py @@ -405,6 +405,16 @@ def test_participant_selection_joins_to_authoritative_configuration_result_diges def test_participant_configuration_rejects_duplicate_canonical_override_via_alias() -> None: manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + overrides = [ + ConfigurationOverrideModel( + target_id="policy.mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ] with pytest.raises(ValueError, match="duplicate canonical target"): realize_participant_configuration( @@ -412,16 +422,7 @@ def test_participant_configuration_rejects_duplicate_canonical_override_via_alia manifest=manifest, manifest_ref="manifests/reference-red-agent.json", manifest_digest="sha256:" + "1" * 64, - overrides=[ - ConfigurationOverrideModel( - target_id="policy.mode", - value={"kind": "literal", "value": "deterministic"}, - ), - ConfigurationOverrideModel( - target_id="mode", - value={"kind": "literal", "value": "deterministic"}, - ), - ], + overrides=overrides, ) @@ -440,6 +441,20 @@ def test_participant_configuration_rejects_missing_required_target_without_parti def test_participant_configuration_rejects_type_coercion() -> None: manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + overrides = [ + ConfigurationOverrideModel( + target_id="policy.mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="temperature", + value={"kind": "literal", "value": "0.5"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ] with pytest.raises(ValueError, match="value_type"): realize_participant_configuration( @@ -447,20 +462,7 @@ def test_participant_configuration_rejects_type_coercion() -> None: manifest=manifest, manifest_ref="manifests/reference-red-agent.json", manifest_digest="sha256:" + "1" * 64, - overrides=[ - ConfigurationOverrideModel( - target_id="policy.mode", - value={"kind": "literal", "value": "deterministic"}, - ), - ConfigurationOverrideModel( - target_id="temperature", - value={"kind": "literal", "value": "0.5"}, - ), - ConfigurationOverrideModel( - target_id="credentials.api", - value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, - ), - ], + overrides=overrides, ) @@ -486,6 +488,17 @@ def validate_and_normalize(self, configuration): def test_participant_owner_normalization_must_preserve_default_override_provenance() -> None: manifest = ParticipantImplementationManifestModel.model_validate(_participant_manifest_payload()) + overrides = [ + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "literal", "value": "deterministic"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ] + validator = _OriginChangingValidator() with pytest.raises(ValueError, match="origin"): realize_participant_configuration( @@ -493,17 +506,8 @@ def test_participant_owner_normalization_must_preserve_default_override_provenan manifest=manifest, manifest_ref="manifests/reference-red-agent.json", manifest_digest="sha256:" + "1" * 64, - overrides=[ - ConfigurationOverrideModel( - target_id="mode", - value={"kind": "literal", "value": "deterministic"}, - ), - ConfigurationOverrideModel( - target_id="credentials.api", - value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, - ), - ], - validator=_OriginChangingValidator(), + overrides=overrides, + validator=validator, ) @@ -514,6 +518,17 @@ def test_participant_owner_normalization_must_not_replace_secret_reference_with_ "secret-reference", ] manifest = ParticipantImplementationManifestModel.model_validate(payload) + overrides = [ + ConfigurationOverrideModel( + target_id="mode", + value={"kind": "secret-reference", "reference_id": "operator-secret.mode"}, + ), + ConfigurationOverrideModel( + target_id="credentials.api", + value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, + ), + ] + validator = _SecretDispositionChangingValidator() with pytest.raises(ValueError, match="literal/secret-reference disposition"): realize_participant_configuration( @@ -521,17 +536,8 @@ def test_participant_owner_normalization_must_not_replace_secret_reference_with_ manifest=manifest, manifest_ref="manifests/reference-red-agent.json", manifest_digest="sha256:" + "1" * 64, - overrides=[ - ConfigurationOverrideModel( - target_id="mode", - value={"kind": "secret-reference", "reference_id": "operator-secret.mode"}, - ), - ConfigurationOverrideModel( - target_id="credentials.api", - value={"kind": "secret-reference", "reference_id": "operator-secret.reference-red-api"}, - ), - ], - validator=_SecretDispositionChangingValidator(), + overrides=overrides, + validator=validator, ) @@ -868,6 +874,34 @@ def _processor_manifest_payload_with_registry() -> dict[str, object]: return payload +def _participant_manifest_map( + manifest: ParticipantImplementationManifestModel, +) -> dict[tuple[str, str, str, str], ParticipantImplementationManifestModel]: + return { + ( + "participants.red", + "reference-red-agent", + "1.0.0", + "participant-implementation-manifest/v1", + ): manifest + } + + +def _apparatus_manifest_map( + manifest: ProcessorManifestV2Model, + *, + component_kind: str = "processor", +) -> dict[tuple[str, str, str, str], ProcessorManifestV2Model]: + return { + ( + component_kind, + "aces-reference-processor", + "0.2.0", + "processor-manifest/v2", + ): manifest + } + + def test_canonical_target_identity_preserves_scenario_variation_point() -> None: payload = _scenario_binding_payload() second = deepcopy(payload["descriptors"][0]) # type: ignore[index] @@ -962,11 +996,12 @@ def test_scenario_binding_admission_rejects_resolver_contract_mismatch( def test_scenario_binding_admission_rejects_literal_for_secret_target() -> None: descriptors = ExperimentBindingDescriptorSetModel.model_validate(_scenario_binding_payload()) + scenario_resolver = _SecretLiteralScenarioResolver() with pytest.raises(ValueError, match="secret scenario targets"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_SecretLiteralScenarioResolver(), + scenario_resolver=scenario_resolver, participant_manifests={}, apparatus_manifests={}, ) @@ -977,19 +1012,14 @@ def test_binding_admission_rejects_miskeyed_participant_manifest_identity() -> N manifest_payload["identity"]["name"] = "different-red-agent" # type: ignore[index] manifest = ParticipantImplementationManifestModel.model_validate(manifest_payload) descriptors = ExperimentBindingDescriptorSetModel.model_validate(_participant_binding_payload()) + scenario_resolver = _ScenarioResolver() + participant_manifests = _participant_manifest_map(manifest) with pytest.raises(ValueError, match="identity must match"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), - participant_manifests={ - ( - "participants.red", - "reference-red-agent", - "1.0.0", - "participant-implementation-manifest/v1", - ): manifest - }, + scenario_resolver=scenario_resolver, + participant_manifests=participant_manifests, apparatus_manifests={}, ) @@ -999,19 +1029,14 @@ def test_binding_admission_rejects_participant_manifest_without_registry() -> No del manifest_payload["configuration_registry"] manifest = ParticipantImplementationManifestModel.model_validate(manifest_payload) descriptors = ExperimentBindingDescriptorSetModel.model_validate(_participant_binding_payload()) + scenario_resolver = _ScenarioResolver() + participant_manifests = _participant_manifest_map(manifest) with pytest.raises(ValueError, match="no configuration target registry"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), - participant_manifests={ - ( - "participants.red", - "reference-red-agent", - "1.0.0", - "participant-implementation-manifest/v1", - ): manifest - }, + scenario_resolver=scenario_resolver, + participant_manifests=participant_manifests, apparatus_manifests={}, ) @@ -1021,19 +1046,14 @@ def test_binding_admission_rejects_participant_target_owner_mismatch() -> None: payload = _participant_binding_payload() payload["descriptors"][0]["owner"]["validator_version"] = "2" # type: ignore[index] descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + scenario_resolver = _ScenarioResolver() + participant_manifests = _participant_manifest_map(manifest) with pytest.raises(ValueError, match="owner"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), - participant_manifests={ - ( - "participants.red", - "reference-red-agent", - "1.0.0", - "participant-implementation-manifest/v1", - ): manifest - }, + scenario_resolver=scenario_resolver, + participant_manifests=participant_manifests, apparatus_manifests={}, ) @@ -1044,19 +1064,14 @@ def test_binding_admission_rejects_participant_target_value_type_mismatch() -> N payload["descriptors"][0]["value_type"] = "integer" # type: ignore[index] payload["descriptors"][0]["value"]["value"] = 1 # type: ignore[index] descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + scenario_resolver = _ScenarioResolver() + participant_manifests = _participant_manifest_map(manifest) with pytest.raises(ValueError, match="value_type"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), - participant_manifests={ - ( - "participants.red", - "reference-red-agent", - "1.0.0", - "participant-implementation-manifest/v1", - ): manifest - }, + scenario_resolver=scenario_resolver, + participant_manifests=participant_manifests, apparatus_manifests={}, ) @@ -1066,20 +1081,15 @@ def test_binding_admission_rejects_miskeyed_apparatus_manifest_identity() -> Non manifest_payload["identity"]["name"] = "different-processor" # type: ignore[index] manifest = ProcessorManifestV2Model.model_validate(manifest_payload) descriptors = ExperimentBindingDescriptorSetModel.model_validate(_apparatus_binding_payload()) + scenario_resolver = _ScenarioResolver() + apparatus_manifests = _apparatus_manifest_map(manifest) with pytest.raises(ValueError, match="identity and kind must match"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), + scenario_resolver=scenario_resolver, participant_manifests={}, - apparatus_manifests={ - ( - "processor", - "aces-reference-processor", - "0.2.0", - "processor-manifest/v2", - ): manifest - }, + apparatus_manifests=apparatus_manifests, ) @@ -1095,20 +1105,15 @@ def test_binding_admission_rejects_apparatus_manifest_without_registry() -> None ) manifest = ProcessorManifestV2Model.model_validate_json(fixture_path.read_text(encoding="utf-8")) descriptors = ExperimentBindingDescriptorSetModel.model_validate(_apparatus_binding_payload()) + scenario_resolver = _ScenarioResolver() + apparatus_manifests = _apparatus_manifest_map(manifest) with pytest.raises(ValueError, match="no configuration target registry"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), + scenario_resolver=scenario_resolver, participant_manifests={}, - apparatus_manifests={ - ( - "processor", - "aces-reference-processor", - "0.2.0", - "processor-manifest/v2", - ): manifest - }, + apparatus_manifests=apparatus_manifests, ) @@ -1117,20 +1122,15 @@ def test_binding_admission_rejects_apparatus_kind_mismatch() -> None: descriptors = ExperimentBindingDescriptorSetModel.model_validate( _apparatus_binding_payload(component_kind="backend") ) + scenario_resolver = _ScenarioResolver() + apparatus_manifests = _apparatus_manifest_map(manifest, component_kind="backend") with pytest.raises(ValueError, match="identity and kind must match"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), + scenario_resolver=scenario_resolver, participant_manifests={}, - apparatus_manifests={ - ( - "backend", - "aces-reference-processor", - "0.2.0", - "processor-manifest/v2", - ): manifest - }, + apparatus_manifests=apparatus_manifests, ) @@ -1139,19 +1139,14 @@ def test_binding_admission_rejects_unknown_participant_target_without_cross_plan descriptors = ExperimentBindingDescriptorSetModel.model_validate( _participant_binding_payload("variables.worker_count") ) + scenario_resolver = _ScenarioResolver() + participant_manifests = _participant_manifest_map(manifest) with pytest.raises(ValueError, match="unknown configuration target"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), - participant_manifests={ - ( - "participants.red", - "reference-red-agent", - "1.0.0", - "participant-implementation-manifest/v1", - ): manifest - }, + scenario_resolver=scenario_resolver, + participant_manifests=participant_manifests, apparatus_manifests={}, ) @@ -1164,19 +1159,14 @@ def test_binding_admission_rejects_alias_and_canonical_duplicate_after_resolutio duplicate["target"]["target_id"] = "policy.mode" payload["descriptors"].append(duplicate) # type: ignore[union-attr] descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + scenario_resolver = _ScenarioResolver() + participant_manifests = _participant_manifest_map(manifest) with pytest.raises(ValidationError, match="duplicate canonical target"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), - participant_manifests={ - ( - "participants.red", - "reference-red-agent", - "1.0.0", - "participant-implementation-manifest/v1", - ): manifest - }, + scenario_resolver=scenario_resolver, + participant_manifests=participant_manifests, apparatus_manifests={}, ) @@ -1189,11 +1179,12 @@ def test_scenario_binding_admission_enforces_owner_declared_value_disposition() "reference_id": "operator-secret.worker-count", } descriptors = ExperimentBindingDescriptorSetModel.model_validate(payload) + scenario_resolver = _ScenarioResolver() with pytest.raises(ValueError, match="value kind"): validate_experiment_binding_targets( descriptors, - scenario_resolver=_ScenarioResolver(), + scenario_resolver=scenario_resolver, participant_manifests={}, apparatus_manifests={}, ) From 46cd7693e82ef1f0e36216ef2e69e29fe30660e6 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 07:05:17 +0200 Subject: [PATCH 05/55] Configure pre-push review limits --- .ground-control.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.ground-control.yaml b/.ground-control.yaml index eb852fb8f..56179c5bf 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -6,12 +6,16 @@ workflow: completion_command: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify lint_command: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s lint format_command: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s hygiene + codex_review: + pre_push_cap: 1 + test_quality_review: + pre_push_cap: 1 review_disposition: - enabled: true - mode: authoritative - max_auto_overrides: 1 + enabled: false + mode: shadow + max_auto_overrides: 0 judge: - enabled: true + enabled: false docs: adr_dir: docs/decisions/adrs/ example_paths: From 82c5245f6240fdcda829ddc62bb6fa9790b0bd5e Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 07:14:46 +0200 Subject: [PATCH 06/55] Fix SonarCloud findings (cycle 2) --- .../python/packages/raes_contracts/experiment_bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/python/packages/raes_contracts/experiment_bindings.py b/implementations/python/packages/raes_contracts/experiment_bindings.py index ad3e3fb15..3cdc2b901 100644 --- a/implementations/python/packages/raes_contracts/experiment_bindings.py +++ b/implementations/python/packages/raes_contracts/experiment_bindings.py @@ -67,7 +67,7 @@ def validate_experiment_binding_targets( elif isinstance(target, ApparatusBindingTargetModel): canonical_target = _resolve_apparatus_target(descriptor, target, apparatus_manifests) # The discriminated union is closed before dispatch. - else: # pragma: no cover + else: raise ValueError("unknown binding plane") admitted.append(descriptor.model_copy(update={"target": canonical_target})) return ExperimentBindingDescriptorSetModel( From 37fe653e1bc213edeebcfba0bd114422e1f7962d Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 07:33:08 +0200 Subject: [PATCH 07/55] Fix SonarCloud findings (cycle 1) --- .../raes_contracts/contracts/__init__.py | 60 ++-- .../contracts/participant_crossing.py | 89 ++++-- .../participant_crossing_validation.py | 285 +++++++++++------- ..._api_423_participant_crossing_contracts.py | 120 +++----- 4 files changed, 300 insertions(+), 254 deletions(-) diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index 141478576..30d0e3285 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -226,7 +226,13 @@ ParticipantImplementationProvenanceModel, ParticipantImplementationSelectionModel, ) -from .participant_occurrences import * # noqa: F403 +from .participant_occurrences import ( + ParticipantControlDeclarationModel, + ParticipantControlOccurrenceModel, + ParticipantCrossingOccurrenceModel, + validate_participant_control_occurrence_context, + validate_participant_crossing_occurrence_context, +) from .participant_runtime import ( ParticipantActionEffectResultModel, ParticipantActionPreconditionResultModel, @@ -330,39 +336,17 @@ SemanticProfileModel, SemanticProfilePhaseModel, ) -from .trial_cleanup import ( - CleanStateClaimModel as CleanStateClaimModel, -) -from .trial_cleanup import ( - CleanStateRequirementModel as CleanStateRequirementModel, -) -from .trial_cleanup import ( - CleanupObligationModel as CleanupObligationModel, -) -from .trial_cleanup import ( - CleanupObligationResultModel as CleanupObligationResultModel, -) -from .trial_cleanup import ( - CleanupResourceBoundaryModel as CleanupResourceBoundaryModel, -) -from .trial_cleanup import ( - ExecutionRetryPolicyModel as ExecutionRetryPolicyModel, -) -from .trial_cleanup import ( - IsolationDimensionEvidenceModel as IsolationDimensionEvidenceModel, -) -from .trial_cleanup import ( - SchedulerIsolationProofModel as SchedulerIsolationProofModel, -) -from .trial_cleanup import ( - TrialCleanupPlanModel as TrialCleanupPlanModel, -) -from .trial_cleanup import ( - TrialCleanupReceiptModel as TrialCleanupReceiptModel, -) -from .trial_cleanup import ( - validate_trial_cleanup_receipt as validate_trial_cleanup_receipt, -) +from .trial_cleanup import CleanStateClaimModel as CleanStateClaimModel +from .trial_cleanup import CleanStateRequirementModel as CleanStateRequirementModel +from .trial_cleanup import CleanupObligationModel as CleanupObligationModel +from .trial_cleanup import CleanupObligationResultModel as CleanupObligationResultModel +from .trial_cleanup import CleanupResourceBoundaryModel as CleanupResourceBoundaryModel +from .trial_cleanup import ExecutionRetryPolicyModel as ExecutionRetryPolicyModel +from .trial_cleanup import IsolationDimensionEvidenceModel as IsolationDimensionEvidenceModel +from .trial_cleanup import SchedulerIsolationProofModel as SchedulerIsolationProofModel +from .trial_cleanup import TrialCleanupPlanModel as TrialCleanupPlanModel +from .trial_cleanup import TrialCleanupReceiptModel as TrialCleanupReceiptModel +from .trial_cleanup import validate_trial_cleanup_receipt as validate_trial_cleanup_receipt from .validation_disclosure import ValidationBasisDisclosureDocumentModel from .validators import _collapse_nullable_optional_schema as _collapse_nullable_optional_schema from .validators import _resolve_instance_path_schema as _resolve_instance_path_schema @@ -436,9 +420,11 @@ "ParticipantAttributionCandidateModel", "ParticipantAttributionEdgeModel", "ParticipantAttributionEvidenceBasisModel", "ParticipantAttributionOrderingBasisModel", "ParticipantAutonomousExecutionStateModel", "ParticipantBehaviorHistoryEventModel", - "ParticipantContextViewModel", "ParticipantControlDeclarationModel", "ParticipantControlOccurrenceModel", # noqa: F405 - "ParticipantCrossingOccurrenceModel", "validate_participant_control_occurrence_context", # noqa: F405 - "validate_participant_crossing_occurrence_context", "ParticipantDecisionSurfaceActionEntryModel", # noqa: F405 + "ParticipantContextViewModel", "ParticipantControlDeclarationModel", + "ParticipantControlOccurrenceModel", "ParticipantCrossingOccurrenceModel", + "validate_participant_control_occurrence_context", + "validate_participant_crossing_occurrence_context", + "ParticipantDecisionSurfaceActionEntryModel", "ParticipantDecisionSurfaceCandidateSetFormModel", "ParticipantDecisionSurfaceConstrainedFormModel", "ParticipantDecisionSurfaceExposureBindingModel", "ParticipantDecisionSurfaceExposureRealizationModel", "ParticipantDecisionSurfaceModel", diff --git a/implementations/python/packages/raes_contracts/contracts/participant_crossing.py b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py index 84a7e7d20..6b32c7c11 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_crossing.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py @@ -29,6 +29,62 @@ from .participant_runtime import ParticipantRuntimeOrderingBasis from .schema_invariants import _add_aces_invariant +_TRANSFORMATION_OPERATIONS = frozenset( + { + ParticipantCrossingOperation.PROJECTION, + ParticipantCrossingOperation.MASKING, + ParticipantCrossingOperation.REDACTION, + ParticipantCrossingOperation.TRANSFORMATION, + ParticipantCrossingOperation.DECLASSIFICATION, + } +) + + +def _classify_gate_dispositions( + gates: ParticipantCrossingDecisionGatesModel, +) -> tuple[bool, bool]: + dispositions = set(gates.dispositions()) + failed = ParticipantCrossingGateDisposition.DENY in dispositions + unresolved = bool( + dispositions + & { + ParticipantCrossingGateDisposition.UNKNOWN, + ParticipantCrossingGateDisposition.UNSUPPORTED, + } + ) + return failed, unresolved + + +def _validate_decision_disposition( + disposition: ParticipantCrossingDecisionDisposition, + *, + failed: bool, + unresolved: bool, +) -> None: + permitted = { + ParticipantCrossingDecisionDisposition.PERMIT, + ParticipantCrossingDecisionDisposition.TRANSFORM, + } + if disposition in permitted and (failed or unresolved): + raise ValueError("permitted participant crossing decisions require every applicable gate to permit") + if disposition == ParticipantCrossingDecisionDisposition.DENY and not failed: + raise ValueError("denied participant crossing decisions require a denied gate") + if disposition == ParticipantCrossingDecisionDisposition.UNSUPPORTED and not unresolved: + raise ValueError("unsupported participant crossing decisions require an unresolved or unsupported gate") + + +def _validate_required_operation( + disposition: ParticipantCrossingDecisionDisposition, + required_operation: ParticipantCrossingOperation | None, +) -> None: + if ( + disposition == ParticipantCrossingDecisionDisposition.TRANSFORM + and required_operation not in _TRANSFORMATION_OPERATIONS + ): + raise ValueError("transform decisions require an explicit transformation operation") + if disposition != ParticipantCrossingDecisionDisposition.TRANSFORM and required_operation is not None: + raise ValueError("required_operation is reserved for transform decisions") + class ParticipantCrossingSubjectReferenceModel(ContractModel): """Typed identity for an existing carrier without copying its payload.""" @@ -184,34 +240,13 @@ class ParticipantCrossingDecisionModel(ParticipantCrossingOccurrenceBaseModel): @model_validator(mode="after") def _validate_deny_first_disposition(self) -> ParticipantCrossingDecisionModel: - dispositions = set(self.gates.dispositions()) - failed = ParticipantCrossingGateDisposition.DENY in dispositions - unresolved = bool( - dispositions - & { - ParticipantCrossingGateDisposition.UNKNOWN, - ParticipantCrossingGateDisposition.UNSUPPORTED, - } + failed, unresolved = _classify_gate_dispositions(self.gates) + _validate_decision_disposition( + self.disposition, + failed=failed, + unresolved=unresolved, ) - if self.disposition in { - ParticipantCrossingDecisionDisposition.PERMIT, - ParticipantCrossingDecisionDisposition.TRANSFORM, - } and (failed or unresolved): - raise ValueError("permitted participant crossing decisions require every applicable gate to permit") - if self.disposition == ParticipantCrossingDecisionDisposition.DENY and not failed: - raise ValueError("denied participant crossing decisions require a denied gate") - if self.disposition == ParticipantCrossingDecisionDisposition.UNSUPPORTED and not unresolved: - raise ValueError("unsupported participant crossing decisions require an unresolved or unsupported gate") - if self.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM and self.required_operation not in { - ParticipantCrossingOperation.PROJECTION, - ParticipantCrossingOperation.MASKING, - ParticipantCrossingOperation.REDACTION, - ParticipantCrossingOperation.TRANSFORMATION, - ParticipantCrossingOperation.DECLASSIFICATION, - }: - raise ValueError("transform decisions require an explicit transformation operation") - if self.disposition != ParticipantCrossingDecisionDisposition.TRANSFORM and self.required_operation is not None: - raise ValueError("required_operation is reserved for transform decisions") + _validate_required_operation(self.disposition, self.required_operation) return self 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 07acd1597..9587226ec 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py @@ -23,6 +23,8 @@ SubjectKey = tuple[object, str] PolicyKey = tuple[str, str] +_CROSSING_DECISION = "crossing decision" +_DELIVERY_ATTEMPT = "delivery attempt" def validate_participant_crossing_occurrence_context( @@ -109,7 +111,7 @@ def _index_records(records: Sequence[ParticipantCrossingOccurrenceModel]) -> _Re elif isinstance(occurrence, ParticipantCrossingDisclosureModel): _register_record(indexes.disclosures_by_id, occurrence.disclosure_id, record, "disclosure") elif isinstance(occurrence, ParticipantCrossingDeliveryAttemptModel): - _register_record(indexes.attempts_by_id, occurrence.attempt_id, record, "delivery attempt") + _register_record(indexes.attempts_by_id, occurrence.attempt_id, record, _DELIVERY_ATTEMPT) elif isinstance(occurrence, ParticipantCrossingDeliveryModel): _register_record(indexes.deliveries_by_id, occurrence.delivery_id, record, "delivery") elif isinstance(occurrence, ParticipantCrossingObservationModel): @@ -141,11 +143,33 @@ def _validate_common_context( if isinstance(occurrence, ParticipantCrossingTransformationModel): _resolve_subject(occurrence.source_subject, subjects_by_key) _resolve_subject(occurrence.result_subject, subjects_by_key) - policy = policies_by_key.get((occurrence.policy.policy_id, occurrence.policy.policy_revision)) + _validate_policy_context(occurrence.policy, policies_by_key) + _validate_reference_context( + record, + known_evidence_refs=known_evidence_refs, + known_authority_basis_refs=known_authority_basis_refs, + ) + _validate_declassification_authority(occurrence, known_authority_basis_refs) + + +def _validate_policy_context( + policy_ref: ParticipantCrossingPolicyReferenceModel, + policies_by_key: dict[PolicyKey, ParticipantCrossingPolicyReferenceModel], +) -> None: + policy = policies_by_key.get((policy_ref.policy_id, policy_ref.policy_revision)) if policy is None: raise ValueError("participant crossing policy revision must resolve") - if policy != occurrence.policy: + if policy != policy_ref: raise ValueError("participant crossing policy revision coordinates must match") + + +def _validate_reference_context( + record: ParticipantCrossingOccurrenceModel, + *, + known_evidence_refs: Collection[str], + known_authority_basis_refs: Collection[str], +) -> None: + occurrence = record.occurrence if not set(record.evidence_refs).issubset(known_evidence_refs): raise ValueError("participant crossing evidence reference must resolve") if not set(occurrence.authority_basis_refs).issubset(known_authority_basis_refs): @@ -153,6 +177,12 @@ def _validate_common_context( stage_evidence_refs = _stage_evidence_refs(occurrence) if not set(stage_evidence_refs).issubset(known_evidence_refs): raise ValueError("participant crossing stage-local evidence reference must resolve") + + +def _validate_declassification_authority( + occurrence: object, + known_authority_basis_refs: Collection[str], +) -> None: if isinstance( occurrence, ParticipantCrossingTransformationModel | ParticipantCrossingDisclosureModel, @@ -191,119 +221,158 @@ def _validate_stage_context( indexes: _RecordIndexes, ) -> None: occurrence = record.occurrence - if isinstance(occurrence, ParticipantCrossingRequestModel): - return if isinstance(occurrence, ParticipantCrossingDecisionModel): - prior = _resolve_record(indexes.requests_by_id, occurrence.request_ref, "crossing request") - _validate_successor(record, prior, require_same_subject=True) - return - if isinstance(occurrence, ParticipantCrossingTransformationModel): - prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") - decision = prior.occurrence - assert isinstance(decision, ParticipantCrossingDecisionModel) - if decision.disposition != ParticipantCrossingDecisionDisposition.TRANSFORM: - raise ValueError("participant crossing transformation requires a transform decision") - _validate_successor(record, prior, require_same_subject=False) - if occurrence.operation != decision.required_operation: - raise ValueError("participant crossing transformation operation must match the decision requirement") - _require_declassification_gate(occurrence.operation, decision) - if _subject_key(decision.subject) != _subject_key(occurrence.source_subject): - raise ValueError("transformation source must match the decided subject") - return - if isinstance(occurrence, ParticipantCrossingDisclosureModel): - prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") - _require_permitted_decision(prior) - decision = prior.occurrence - assert isinstance(decision, ParticipantCrossingDecisionModel) - _require_declassification_gate(occurrence.operation, decision) - if occurrence.transformation_ref is not None: - transformed = _resolve_record( - indexes.transformations_by_id, - occurrence.transformation_ref, - "crossing transformation", - ) - transformed_occurrence = transformed.occurrence - assert isinstance(transformed_occurrence, ParticipantCrossingTransformationModel) - _require_same_decision( - occurrence.decision_ref, - transformed_occurrence.decision_ref, - "disclosure transformation", - ) - _validate_successor(record, transformed, require_same_subject=True) - else: - if decision.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM: - raise ValueError("transform decisions require their exact transformation before disclosure") - _validate_successor(record, prior, require_same_subject=True) - return - if isinstance(occurrence, ParticipantCrossingDeliveryAttemptModel): - prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") - _require_permitted_decision(prior) - decision = prior.occurrence - assert isinstance(decision, ParticipantCrossingDecisionModel) + _validate_decision_stage(record, occurrence, indexes) + elif isinstance(occurrence, ParticipantCrossingTransformationModel): + _validate_transformation_stage(record, occurrence, indexes) + elif isinstance(occurrence, ParticipantCrossingDisclosureModel): + _validate_disclosure_stage(record, occurrence, indexes) + elif isinstance(occurrence, ParticipantCrossingDeliveryAttemptModel): + _validate_delivery_attempt_stage(record, occurrence, indexes) + elif isinstance(occurrence, ParticipantCrossingDeliveryModel): + _validate_delivery_stage(record, occurrence, indexes) + elif isinstance(occurrence, ParticipantCrossingObservationModel): + _validate_observation_stage(record, occurrence, indexes) + elif isinstance(occurrence, ParticipantCrossingAuditModel): + _validate_audit_stage(record, occurrence, indexes) + + +def _validate_decision_stage( + record: ParticipantCrossingOccurrenceModel, + occurrence: ParticipantCrossingDecisionModel, + indexes: _RecordIndexes, +) -> None: + prior = _resolve_record(indexes.requests_by_id, occurrence.request_ref, "crossing request") + _validate_successor(record, prior, require_same_subject=True) + + +def _validate_transformation_stage( + record: ParticipantCrossingOccurrenceModel, + occurrence: ParticipantCrossingTransformationModel, + indexes: _RecordIndexes, +) -> None: + prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, _CROSSING_DECISION) + decision = prior.occurrence + assert isinstance(decision, ParticipantCrossingDecisionModel) + if decision.disposition != ParticipantCrossingDecisionDisposition.TRANSFORM: + raise ValueError("participant crossing transformation requires a transform decision") + _validate_successor(record, prior, require_same_subject=False) + if occurrence.operation != decision.required_operation: + raise ValueError("participant crossing transformation operation must match the decision requirement") + _require_declassification_gate(occurrence.operation, decision) + if _subject_key(decision.subject) != _subject_key(occurrence.source_subject): + raise ValueError("transformation source must match the decided subject") + + +def _validate_disclosure_stage( + record: ParticipantCrossingOccurrenceModel, + occurrence: ParticipantCrossingDisclosureModel, + 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) + _require_declassification_gate(occurrence.operation, decision) + if occurrence.transformation_ref is None: if decision.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM: - if occurrence.transformation_ref is None: - raise ValueError("transform decisions require a transformation before delivery attempt") - transformed = _resolve_record( - indexes.transformations_by_id, - occurrence.transformation_ref, - "crossing transformation", - ) - transformed_occurrence = transformed.occurrence - assert isinstance(transformed_occurrence, ParticipantCrossingTransformationModel) - _require_same_decision( - occurrence.decision_ref, - transformed_occurrence.decision_ref, - "delivery-attempt transformation", - ) - _validate_successor(record, transformed, require_same_subject=True) - else: - if occurrence.transformation_ref is not None: - raise ValueError("delivery attempt transformation_ref requires a transform decision") - _validate_successor(record, prior, require_same_subject=True) - _require_subject_owner(record, occurrence.owning_occurrence_ref, "delivery attempt") - return - if isinstance(occurrence, ParticipantCrossingDeliveryModel): - decision = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") - _require_permitted_decision(decision) - attempt = _resolve_record(indexes.attempts_by_id, occurrence.attempt_ref, "delivery attempt") - attempt_occurrence = attempt.occurrence - assert isinstance(attempt_occurrence, ParticipantCrossingDeliveryAttemptModel) + raise ValueError("transform decisions require their exact transformation before disclosure") + _validate_successor(record, prior, require_same_subject=True) + else: + transformed = _resolve_record( + indexes.transformations_by_id, + occurrence.transformation_ref, + "crossing transformation", + ) + transformed_occurrence = transformed.occurrence + assert isinstance(transformed_occurrence, ParticipantCrossingTransformationModel) _require_same_decision( occurrence.decision_ref, - attempt_occurrence.decision_ref, - "delivery", + transformed_occurrence.decision_ref, + "disclosure transformation", + ) + _validate_successor(record, transformed, require_same_subject=True) + + +def _validate_delivery_attempt_stage( + record: ParticipantCrossingOccurrenceModel, + occurrence: ParticipantCrossingDeliveryAttemptModel, + 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 decision.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM: + if occurrence.transformation_ref is None: + raise ValueError("transform decisions require a transformation before delivery attempt") + transformed = _resolve_record( + indexes.transformations_by_id, + occurrence.transformation_ref, + "crossing transformation", ) - if attempt_occurrence.disposition != "attempted": - raise ValueError("participant crossing delivery requires a successful attempt disposition") - _validate_successor(record, attempt, require_same_subject=True) - if occurrence.delivery_order > occurrence.effective_order: - raise ValueError("delivery order cannot be later than its crossing fact") - _require_subject_owner(record, occurrence.owning_occurrence_ref, "delivery") - if occurrence.owning_occurrence_ref != attempt_occurrence.owning_occurrence_ref: - raise ValueError("participant crossing delivery owner must match its attempt") - return - if isinstance(occurrence, ParticipantCrossingObservationModel): - decision = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, "crossing decision") - _require_permitted_decision(decision) - delivery = _resolve_record(indexes.deliveries_by_id, occurrence.delivery_ref, "crossing delivery") - delivery_occurrence = delivery.occurrence - assert isinstance(delivery_occurrence, ParticipantCrossingDeliveryModel) + transformed_occurrence = transformed.occurrence + assert isinstance(transformed_occurrence, ParticipantCrossingTransformationModel) _require_same_decision( occurrence.decision_ref, - delivery_occurrence.decision_ref, - "observation", + transformed_occurrence.decision_ref, + "delivery-attempt transformation", ) - if delivery_occurrence.disposition != "delivered": - raise ValueError("participant crossing observation requires a delivered disposition") - _validate_successor(record, delivery, require_same_subject=False) - if occurrence.observation_order > occurrence.effective_order: - raise ValueError("observation order cannot be later than its crossing fact") - _require_subject_owner(record, occurrence.owning_observation_ref, "observation") - return - if isinstance(occurrence, ParticipantCrossingAuditModel): - audited = _resolve_record(indexes.records_by_event_id, occurrence.audited_event_ref, "audited event") - _validate_successor(record, audited, require_same_subject=True) - return + _validate_successor(record, transformed, require_same_subject=True) + else: + if occurrence.transformation_ref is not None: + raise ValueError("delivery attempt transformation_ref requires a transform decision") + _validate_successor(record, prior, require_same_subject=True) + _require_subject_owner(record, occurrence.owning_occurrence_ref, _DELIVERY_ATTEMPT) + + +def _validate_delivery_stage( + record: ParticipantCrossingOccurrenceModel, + occurrence: ParticipantCrossingDeliveryModel, + indexes: _RecordIndexes, +) -> None: + decision = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, _CROSSING_DECISION) + _require_permitted_decision(decision) + attempt = _resolve_record(indexes.attempts_by_id, occurrence.attempt_ref, _DELIVERY_ATTEMPT) + attempt_occurrence = attempt.occurrence + assert isinstance(attempt_occurrence, ParticipantCrossingDeliveryAttemptModel) + _require_same_decision(occurrence.decision_ref, attempt_occurrence.decision_ref, "delivery") + if attempt_occurrence.disposition != "attempted": + raise ValueError("participant crossing delivery requires a successful attempt disposition") + _validate_successor(record, attempt, require_same_subject=True) + if occurrence.delivery_order > occurrence.effective_order: + raise ValueError("delivery order cannot be later than its crossing fact") + _require_subject_owner(record, occurrence.owning_occurrence_ref, "delivery") + if occurrence.owning_occurrence_ref != attempt_occurrence.owning_occurrence_ref: + raise ValueError("participant crossing delivery owner must match its attempt") + + +def _validate_observation_stage( + record: ParticipantCrossingOccurrenceModel, + occurrence: ParticipantCrossingObservationModel, + indexes: _RecordIndexes, +) -> None: + decision = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, _CROSSING_DECISION) + _require_permitted_decision(decision) + delivery = _resolve_record(indexes.deliveries_by_id, occurrence.delivery_ref, "crossing delivery") + delivery_occurrence = delivery.occurrence + assert isinstance(delivery_occurrence, ParticipantCrossingDeliveryModel) + _require_same_decision(occurrence.decision_ref, delivery_occurrence.decision_ref, "observation") + if delivery_occurrence.disposition != "delivered": + raise ValueError("participant crossing observation requires a delivered disposition") + _validate_successor(record, delivery, require_same_subject=False) + if occurrence.observation_order > occurrence.effective_order: + raise ValueError("observation order cannot be later than its crossing fact") + _require_subject_owner(record, occurrence.owning_observation_ref, "observation") + + +def _validate_audit_stage( + record: ParticipantCrossingOccurrenceModel, + occurrence: ParticipantCrossingAuditModel, + indexes: _RecordIndexes, +) -> None: + audited = _resolve_record(indexes.records_by_event_id, occurrence.audited_event_ref, "audited event") + _validate_successor(record, audited, require_same_subject=True) def _resolve_record( diff --git a/implementations/python/tests/test_api_423_participant_crossing_contracts.py b/implementations/python/tests/test_api_423_participant_crossing_contracts.py index 398434774..f17960e2f 100644 --- a/implementations/python/tests/test_api_423_participant_crossing_contracts.py +++ b/implementations/python/tests/test_api_423_participant_crossing_contracts.py @@ -204,6 +204,25 @@ def _delivery( return ParticipantCrossingOccurrenceModel.model_validate(value) +def _validate_context( + records: list[ParticipantCrossingOccurrenceModel], + *, + subject_values: list[dict[str, object]] | None = None, + policy_values: list[dict[str, object]] | None = None, + known_evidence_refs: set[str] | None = None, +) -> None: + subjects = [_subject()] if subject_values is None else subject_values + policies = [_policy()] if policy_values is None else policy_values + evidence_refs = KNOWN_EVIDENCE_REFS if known_evidence_refs is None else known_evidence_refs + validate_participant_crossing_occurrence_context( + records, + known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(subject) for subject in subjects], + policies=[ParticipantCrossingPolicyReferenceModel.model_validate(policy) for policy in policies], + known_evidence_refs=evidence_refs, + known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + ) + + def test_crossing_request_is_a_closed_participant_runtime_fact() -> None: record = _request() @@ -388,45 +407,21 @@ def test_context_validator_accepts_ordered_requested_decided_and_realized_facts( def test_context_validator_rejects_unknown_subject_stale_policy_and_missing_evidence() -> None: request = _request() - policy = ParticipantCrossingPolicyReferenceModel.model_validate(_policy()) + empty_evidence_refs: set[str] = set() with pytest.raises(ValueError, match="typed subject reference must resolve"): - validate_participant_crossing_occurrence_context( - [request], - known_subjects=[], - policies=[policy], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, - ) + _validate_context([request], subject_values=[]) with pytest.raises(ValueError, match="policy revision must resolve"): - validate_participant_crossing_occurrence_context( - [request], - known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], - policies=[], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, - ) + _validate_context([request], policy_values=[]) with pytest.raises(ValueError, match="evidence reference must resolve"): - validate_participant_crossing_occurrence_context( - [request], - known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], - policies=[policy], - known_evidence_refs=set(), - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, - ) + _validate_context([request], known_evidence_refs=empty_evidence_refs) def test_context_validator_rejects_unresolved_stage_local_evidence() -> None: request = _request() with pytest.raises(ValueError, match="stage-local evidence reference must resolve"): - validate_participant_crossing_occurrence_context( - [request], - known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs={"evidence:crossing-1"}, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, - ) + _validate_context([request], known_evidence_refs={"evidence:crossing-1"}) def test_delivery_must_use_the_attempts_exact_decision_and_successful_disposition() -> None: @@ -440,24 +435,12 @@ def test_delivery_must_use_the_attempts_exact_decision_and_successful_dispositio wrong_decision_delivery = _delivery(attempt, decision_ref="crossing-decision.2") with pytest.raises(ValueError, match="delivery decision must match its predecessor"): - validate_participant_crossing_occurrence_context( - [request, decision, second, attempt, wrong_decision_delivery], - known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, - ) + _validate_context([request, decision, second, attempt, wrong_decision_delivery]) failed_attempt = _attempt(decision, disposition="failed") delivery_after_failure = _delivery(failed_attempt) with pytest.raises(ValueError, match="delivery requires a successful attempt disposition"): - validate_participant_crossing_occurrence_context( - [request, decision, failed_attempt, delivery_after_failure], - known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, - ) + _validate_context([request, decision, failed_attempt, delivery_after_failure]) def test_delivery_ownership_must_resolve_the_exact_typed_subject() -> None: @@ -469,17 +452,12 @@ def test_delivery_ownership_must_resolve_the_exact_typed_subject() -> None: ref="observation:foreign", ) attempt = _attempt(decision, owning_occurrence_ref="observation:foreign") + known_subjects = [_subject(), foreign_subject] with pytest.raises(ValueError, match="owner must match its typed subject"): - validate_participant_crossing_occurrence_context( + _validate_context( [request, decision, attempt], - known_subjects=[ - ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), - ParticipantCrossingSubjectReferenceModel.model_validate(foreign_subject), - ], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + subject_values=known_subjects, ) @@ -503,13 +481,7 @@ def test_context_validator_rejects_delivery_without_attempt() -> None: delivery = ParticipantCrossingOccurrenceModel.model_validate(delivery_value) with pytest.raises(ValueError, match="delivery attempt reference must resolve"): - validate_participant_crossing_occurrence_context( - [request, decision, delivery], - known_subjects=[ParticipantCrossingSubjectReferenceModel.model_validate(_subject())], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, - ) + _validate_context([request, decision, delivery]) def test_transform_decision_delivers_the_new_subject_after_the_transformation() -> None: @@ -603,17 +575,12 @@ def test_transform_must_apply_the_exact_operation_prescribed_by_its_decision() - value["occurrence"]["effective_order"] = 12 value["occurrence"]["subject"] = result_subject transformation = ParticipantCrossingOccurrenceModel.model_validate(value) + known_subjects = [_subject(), result_subject] with pytest.raises(ValueError, match="operation must match the decision requirement"): - validate_participant_crossing_occurrence_context( + _validate_context( [request, decision, transformation], - known_subjects=[ - ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), - ParticipantCrossingSubjectReferenceModel.model_validate(result_subject), - ], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + subject_values=known_subjects, ) @@ -703,17 +670,12 @@ def test_declassification_basis_must_resolve_as_declared_authority() -> None: value["occurrence"]["effective_order"] = 12 value["occurrence"]["subject"] = result_subject transformation = ParticipantCrossingOccurrenceModel.model_validate(value) + known_subjects = [_subject(), result_subject] with pytest.raises(ValueError, match="declassification authority basis must resolve"): - validate_participant_crossing_occurrence_context( + _validate_context( [request, decision, transformation], - known_subjects=[ - ParticipantCrossingSubjectReferenceModel.model_validate(_subject()), - ParticipantCrossingSubjectReferenceModel.model_validate(result_subject), - ], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + subject_values=known_subjects, ) @@ -894,15 +856,9 @@ def test_context_validator_rejects_a_transformation_cycle() -> None: transform_b = ParticipantCrossingOccurrenceModel.model_validate(transform_b_value) with pytest.raises(ValueError, match="transformation cycle"): - validate_participant_crossing_occurrence_context( + _validate_context( [request_a, decision_a, transform_a, request_b, decision_b, transform_b], - known_subjects=[ - ParticipantCrossingSubjectReferenceModel.model_validate(subject_a), - ParticipantCrossingSubjectReferenceModel.model_validate(subject_b), - ], - policies=[ParticipantCrossingPolicyReferenceModel.model_validate(_policy())], - known_evidence_refs=KNOWN_EVIDENCE_REFS, - known_authority_basis_refs=KNOWN_AUTHORITY_BASIS_REFS, + subject_values=[subject_a, subject_b], ) From d639eb6dac49317e5bf5ddedd34905bf8187cfeb Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 07:34:34 +0200 Subject: [PATCH 08/55] Fix SonarCloud findings (cycle 1) --- .../raes_runtime/control_plane_store.py | 15 +- .../participant_control_intents.py | 4 +- .../participant_control_mediation.py | 314 ++++++++++-------- .../participant_control_occurrences.py | 69 ++-- .../participant_control_rejections.py | 87 +++++ .../participant_control_targets.py | 40 ++- ...api_409_participant_control_occurrences.py | 12 +- .../test_run_310_supervisory_lifecycle.py | 18 +- 8 files changed, 362 insertions(+), 197 deletions(-) create mode 100644 implementations/python/packages/raes_runtime/participant_control_rejections.py diff --git a/implementations/python/packages/raes_runtime/control_plane_store.py b/implementations/python/packages/raes_runtime/control_plane_store.py index be2a6e65c..599cf56ce 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store.py +++ b/implementations/python/packages/raes_runtime/control_plane_store.py @@ -4,7 +4,7 @@ import os from dataclasses import dataclass, field -from typing import Any, Protocol +from typing import TYPE_CHECKING, Any, Protocol from raes_contracts.contracts import RealizationEnvelopeIdentityModel from raes_contracts.contracts.time_model import TimeRuntimeStateModel @@ -23,6 +23,9 @@ SnapshotEntry, ) +if TYPE_CHECKING: + from .control_plane_store_local import LocalControlPlaneStore + @dataclass(frozen=True) class AuditEvent: @@ -387,7 +390,15 @@ def commit_control_transition( self._audit = [*self._audit, audit_event] -from .control_plane_store_local import LocalControlPlaneStore # noqa: E402 +def __getattr__(name: str) -> object: + """Lazily expose the local store without creating an import cycle.""" + + if name == "LocalControlPlaneStore": + from .control_plane_store_local import LocalControlPlaneStore + + return LocalControlPlaneStore + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + __all__ = ( "AuditEvent", diff --git a/implementations/python/packages/raes_runtime/participant_control_intents.py b/implementations/python/packages/raes_runtime/participant_control_intents.py index c1660433b..3f38fef20 100644 --- a/implementations/python/packages/raes_runtime/participant_control_intents.py +++ b/implementations/python/packages/raes_runtime/participant_control_intents.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Annotated, Literal, TypeAlias +from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field, model_validator from raes_contracts.contracts.participant_control import ParticipantControlTargetKind @@ -111,7 +111,7 @@ class ParticipantCancellationControlIntent(ParticipantControlIntentBase): target_revision: int = Field(gt=0) -ParticipantControlIntent: TypeAlias = Annotated[ +ParticipantControlIntent = Annotated[ ParticipantProposalControlIntent | ParticipantApprovalControlIntent | ParticipantDenialControlIntent diff --git a/implementations/python/packages/raes_runtime/participant_control_mediation.py b/implementations/python/packages/raes_runtime/participant_control_mediation.py index ef5642c59..660f69f79 100644 --- a/implementations/python/packages/raes_runtime/participant_control_mediation.py +++ b/implementations/python/packages/raes_runtime/participant_control_mediation.py @@ -4,7 +4,7 @@ import hashlib import json -from dataclasses import asdict +from dataclasses import asdict, dataclass from uuid import uuid4 from raes_contracts.contracts import ( @@ -28,13 +28,24 @@ from .control_plane_security import ControlPlaneIdentity from .control_plane_store import AuditEvent, ControlPlaneOperationRecord from .participant_control_intents import ParticipantControlIntent -from .participant_control_occurrences import build_participant_control_occurrence +from .participant_control_occurrences import ( + ParticipantControlOccurrenceContext, + build_participant_control_occurrence, +) +from .participant_control_rejections import participant_control_rejection_reason from .participant_control_targets import ( participant_control_target_contexts, resolve_participant_control_target, ) -_ORDER_STRATEGY = "total-effective-order" + +@dataclass(frozen=True) +class _BoundControlRequest: + specification: ParticipantBehaviorSpecificationRuntime + transition: MixedControlTransitionRuntime + state: MixedControlControllerStateRuntime + semantic_fingerprint: str + scoped_key: str def record_participant_control( @@ -52,18 +63,7 @@ def record_participant_control( if identity.target_name is not None and identity.target_name != control_plane.target_name: raise PermissionError("participant control identity is not authorized for this target") _require_participant_binding(identity, participant_address) - specification = _specification_for_participant(control_plane, participant_address, identity) - transition = _transition_for_intent(specification, intent, identity) - state = _state_by_address(specification, transition.from_state_address) - semantic_fingerprint = _semantic_fingerprint( - control_plane, - participant_address, - intent, - identity, - specification, - transition, - ) - scoped_key = _scoped_idempotency_key( + bound = _bind_control_request( control_plane, participant_address, intent, @@ -72,109 +72,178 @@ def record_participant_control( ) with control_plane._participant_control_lock: - existing = control_plane._store.find_by_idempotency(scoped_key) if scoped_key else None + existing = control_plane._store.find_by_idempotency(bound.scoped_key) if bound.scoped_key else None if existing is not None: - if existing.request_fingerprint != semantic_fingerprint: + if existing.request_fingerprint != bound.semantic_fingerprint: raise ValueError("Idempotency-Key was reused with different semantics.") control_plane._operations[existing.receipt.operation_id] = existing return existing.receipt - - history = list(control_plane._snapshot.participant_control_history.get(participant_address, ())) - current_state, current_revision = _fold_controller_state( - specification, - history, - episode_id=intent.episode_id, - ) - resolved_target, target_rejection_reason = resolve_participant_control_target( - control_plane._snapshot, - intent, - participant_address=participant_address, - ) - rejection_reason = _rejection_reason( - specification, - transition, - state, + return _record_new_participant_control( + control_plane, + participant_address, intent, - current_state=current_state, - current_revision=current_revision, - target_rejection_reason=target_rejection_reason, + identity, + bound, ) - accepted = rejection_reason is None - occurrence = build_participant_control_occurrence( + + +def _bind_control_request( + control_plane: object, + participant_address: str, + intent: ParticipantControlIntent, + identity: ControlPlaneIdentity, + idempotency_key: str, +) -> _BoundControlRequest: + specification = _specification_for_participant(control_plane, participant_address, identity) + transition = _transition_for_intent(specification, intent, identity) + state = _state_by_address(specification, transition.from_state_address) + return _BoundControlRequest( + specification=specification, + transition=transition, + state=state, + semantic_fingerprint=_semantic_fingerprint( control_plane, participant_address, intent, + identity, specification, transition, - state, - history, - resolved_target=resolved_target, - accepted=accepted, - rejection_reason=rejection_reason, - ) - candidate_history = [*history, occurrence.model_dump(mode="json")] - _validate_candidate_history( - specification, - candidate_history, - known_targets=participant_control_target_contexts(control_plane._snapshot), - ) - next_snapshot = control_plane._snapshot.with_entries( - dict(control_plane._snapshot.entries), - participant_control_history={ - **control_plane._snapshot.participant_control_history, - participant_address: candidate_history, - }, - ) - operation_id = str(uuid4()) - submitted_at = occurrence.recorded_at - diagnostics = [] if accepted else [_rejection_diagnostic(participant_address, rejection_reason)] - receipt = OperationReceipt( - operation_id=operation_id, - domain=RuntimeDomain.PARTICIPANT, - submitted_at=submitted_at, - accepted=accepted, - diagnostics=diagnostics, - ) - status = OperationStatus( - operation_id=operation_id, - domain=RuntimeDomain.PARTICIPANT, - state=OperationState.SUCCEEDED if accepted else OperationState.FAILED, - submitted_at=submitted_at, - updated_at=submitted_at, - diagnostics=diagnostics, - changed_addresses=[participant_address], - ) - record = ControlPlaneOperationRecord( - receipt=receipt, - status=status, - request_fingerprint=semantic_fingerprint, - idempotency_key=scoped_key, - ) - audit_event = AuditEvent( - timestamp=submitted_at, - action="record_participant_control", - identity=identity.identity, - allowed=accepted, - target=participant_address, - operation_id=operation_id, - reason=rejection_reason or "accepted", - details={ - "episode_id": intent.episode_id, - "kind": intent.kind, - "event_id": occurrence.event_id, - }, - ) - expected_head = _history_head(history) - control_plane._store.commit_control_transition( + ), + scoped_key=_scoped_idempotency_key( + control_plane, + participant_address, + intent, + identity, + idempotency_key, + ), + ) + + +def _record_new_participant_control( + control_plane: object, + participant_address: str, + intent: ParticipantControlIntent, + identity: ControlPlaneIdentity, + bound: _BoundControlRequest, +) -> OperationReceipt: + history = list(control_plane._snapshot.participant_control_history.get(participant_address, ())) + current_state, current_revision = _fold_controller_state( + bound.specification, + history, + episode_id=intent.episode_id, + ) + resolved_target, target_rejection_reason = resolve_participant_control_target( + control_plane._snapshot, + intent, + participant_address=participant_address, + ) + rejection_reason = participant_control_rejection_reason( + bound.specification, + bound.transition, + bound.state, + intent, + current_state=current_state, + current_revision=current_revision, + target_rejection_reason=target_rejection_reason, + ) + accepted = rejection_reason is None + occurrence = build_participant_control_occurrence( + ParticipantControlOccurrenceContext( + control_plane=control_plane, participant_address=participant_address, - expected_head=expected_head, - snapshot=next_snapshot, - record=record, - audit_event=audit_event, - ) - control_plane._snapshot = next_snapshot - control_plane._operations[operation_id] = record - return receipt + specification=bound.specification, + transition=bound.transition, + state=bound.state, + history=history, + ), + intent, + resolved_target=resolved_target, + accepted=accepted, + rejection_reason=rejection_reason, + ) + candidate_history = [*history, occurrence.model_dump(mode="json")] + _validate_candidate_history( + bound.specification, + candidate_history, + known_targets=participant_control_target_contexts(control_plane._snapshot), + ) + next_snapshot = control_plane._snapshot.with_entries( + dict(control_plane._snapshot.entries), + participant_control_history={ + **control_plane._snapshot.participant_control_history, + participant_address: candidate_history, + }, + ) + record, audit_event = _operation_artifacts( + participant_address, + intent, + identity, + bound, + occurrence, + accepted, + rejection_reason, + ) + control_plane._store.commit_control_transition( + participant_address=participant_address, + expected_head=_history_head(history), + snapshot=next_snapshot, + record=record, + audit_event=audit_event, + ) + control_plane._snapshot = next_snapshot + control_plane._operations[record.receipt.operation_id] = record + return record.receipt + + +def _operation_artifacts( + participant_address: str, + intent: ParticipantControlIntent, + identity: ControlPlaneIdentity, + bound: _BoundControlRequest, + occurrence: ParticipantControlOccurrenceModel, + accepted: bool, + rejection_reason: str | None, +) -> tuple[ControlPlaneOperationRecord, AuditEvent]: + operation_id = str(uuid4()) + submitted_at = occurrence.recorded_at + diagnostics = [] if accepted else [_rejection_diagnostic(participant_address, rejection_reason)] + receipt = OperationReceipt( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + submitted_at=submitted_at, + accepted=accepted, + diagnostics=diagnostics, + ) + status = OperationStatus( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + state=OperationState.SUCCEEDED if accepted else OperationState.FAILED, + submitted_at=submitted_at, + updated_at=submitted_at, + diagnostics=diagnostics, + changed_addresses=[participant_address], + ) + record = ControlPlaneOperationRecord( + receipt=receipt, + status=status, + request_fingerprint=bound.semantic_fingerprint, + idempotency_key=bound.scoped_key, + ) + audit_event = AuditEvent( + timestamp=submitted_at, + action="record_participant_control", + identity=identity.identity, + allowed=accepted, + target=participant_address, + operation_id=operation_id, + reason=rejection_reason or "accepted", + details={ + "episode_id": intent.episode_id, + "kind": intent.kind, + "event_id": occurrence.event_id, + }, + ) + return record, audit_event def _specification_for_participant( @@ -271,39 +340,6 @@ def _fold_controller_state( return state_address, revision -def _rejection_reason( - specification: ParticipantBehaviorSpecificationRuntime, - transition: MixedControlTransitionRuntime, - state: MixedControlControllerStateRuntime, - intent: ParticipantControlIntent, - *, - current_state: str, - current_revision: int, - target_rejection_reason: str | None, -) -> str | None: - if specification.mixed_control_order_strategy != _ORDER_STRATEGY: - return "unsupported-order-strategy" - if intent.policy_revision != specification.mixed_control_policy_revision: - return "stale-policy" - if ( - intent.expected_state_revision != current_revision - or transition.expected_state_revision != current_revision - or transition.from_state_address != current_state - ): - return "stale-state" - if transition.policy_revision != specification.mixed_control_policy_revision: - return "stale-policy" - if target_rejection_reason is not None: - return target_rejection_reason - if state.authority_status != "active": - return "revoked-authority" - if not state.valid_from_order <= transition.effective_order <= state.valid_until_order: - return "late-authority" - if not transition.valid_from_order <= transition.effective_order <= transition.valid_until_order: - return "late-authority" - return None - - def _history_head(history: list[dict[str, object]]) -> str | None: if not history: return None diff --git a/implementations/python/packages/raes_runtime/participant_control_occurrences.py b/implementations/python/packages/raes_runtime/participant_control_occurrences.py index 500b4e5b9..41424e744 100644 --- a/implementations/python/packages/raes_runtime/participant_control_occurrences.py +++ b/implementations/python/packages/raes_runtime/participant_control_occurrences.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import UTC, datetime from uuid import uuid4 @@ -30,14 +31,21 @@ from .participant_control_targets import ResolvedParticipantControlTarget +@dataclass(frozen=True) +class ParticipantControlOccurrenceContext: + """Trusted runtime-owned inputs used to construct one occurrence.""" + + control_plane: object + participant_address: str + specification: ParticipantBehaviorSpecificationRuntime + transition: MixedControlTransitionRuntime + state: MixedControlControllerStateRuntime + history: list[dict[str, object]] + + def build_participant_control_occurrence( - control_plane: object, - participant_address: str, + context: ParticipantControlOccurrenceContext, intent: ParticipantControlIntent, - specification: ParticipantBehaviorSpecificationRuntime, - transition: MixedControlTransitionRuntime, - state: MixedControlControllerStateRuntime, - history: list[dict[str, object]], *, resolved_target: ResolvedParticipantControlTarget | None, accepted: bool, @@ -45,6 +53,12 @@ def build_participant_control_occurrence( ) -> ParticipantControlOccurrenceModel: """Build one immutable runtime-owned occurrence from a caller intent.""" + control_plane = context.control_plane + participant_address = context.participant_address + specification = context.specification + transition = context.transition + state = context.state + history = context.history now = datetime.now(UTC).isoformat().replace("+00:00", "Z") detail: dict[str, object] = { "kind": intent.kind, @@ -98,7 +112,7 @@ def _kind_detail( transition: MixedControlTransitionRuntime, ) -> dict[str, object]: if isinstance(intent, ParticipantProposalControlIntent): - return { + detail = { "proposal_id": intent.proposal_id, "proposal_revision": intent.proposal_revision, "admission_status": "not-admitted", @@ -111,47 +125,49 @@ def _kind_detail( "source_proposal_revision": intent.source_proposal_revision, "transformation_ref": intent.transformation_ref, } - if isinstance(intent, (ParticipantApprovalControlIntent, ParticipantDenialControlIntent)): - return { + elif isinstance(intent, (ParticipantApprovalControlIntent, ParticipantDenialControlIntent)): + detail = { "proposal_ref": intent.proposal_ref, "proposal_revision": intent.proposal_revision, "decision_ref": intent.decision_ref, "decision_revision": intent.decision_revision, } - if isinstance(intent, ParticipantExternalDirectionControlIntent): - return { + elif isinstance(intent, ParticipantExternalDirectionControlIntent): + detail = { "target_kind": intent.target_kind.value, "target_ref": intent.target_ref, "target_revision": intent.target_revision, } - if isinstance(intent, ParticipantInterventionControlIntent): - return { + elif isinstance(intent, ParticipantInterventionControlIntent): + detail = { "affected_target_kind": intent.affected_target_kind.value, "affected_occurrence_ref": intent.affected_occurrence_ref, "affected_revision": intent.affected_revision, "intervention_ref": intent.intervention_ref, } - if isinstance(intent, ParticipantHandoffControlIntent): - return { + elif isinstance(intent, ParticipantHandoffControlIntent): + detail = { "prior_controller_state_ref": transition.from_state_address, "resulting_controller_state_ref": transition.to_state_address, "resulting_state_revision": transition.resulting_state_revision, "completion_evidence_ref": intent.completion_evidence_ref, } - if isinstance(intent, ParticipantOverrideControlIntent): - return { + elif isinstance(intent, ParticipantOverrideControlIntent): + detail = { "superseded_target_kind": intent.superseded_target_kind.value, "superseded_occurrence_ref": intent.superseded_occurrence_ref, "superseded_revision": intent.superseded_revision, "replacement_ref": intent.replacement_ref, } - assert isinstance(intent, ParticipantCancellationControlIntent) - return { - "target_kind": intent.target_kind.value, - "target_ref": intent.target_ref, - "target_revision": intent.target_revision, - "cancellation_effect": _cancellation_effect(intent.target_kind).value, - } + else: + assert isinstance(intent, ParticipantCancellationControlIntent) + detail = { + "target_kind": intent.target_kind.value, + "target_ref": intent.target_ref, + "target_revision": intent.target_revision, + "cancellation_effect": _cancellation_effect(intent.target_kind).value, + } + return detail def _cancellation_effect(target_kind: ParticipantControlTargetKind) -> ParticipantCancellationEffect: @@ -162,4 +178,7 @@ def _cancellation_effect(target_kind: ParticipantControlTargetKind) -> Participa return ParticipantCancellationEffect.TOO_LATE -__all__ = ("build_participant_control_occurrence",) +__all__ = ( + "ParticipantControlOccurrenceContext", + "build_participant_control_occurrence", +) diff --git a/implementations/python/packages/raes_runtime/participant_control_rejections.py b/implementations/python/packages/raes_runtime/participant_control_rejections.py new file mode 100644 index 000000000..1a6dfb5b6 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_control_rejections.py @@ -0,0 +1,87 @@ +"""Ordered rejection rules for RUN-310 supervisory mediation.""" + +from __future__ import annotations + +from raes_processor.models import ( + MixedControlControllerStateRuntime, + MixedControlTransitionRuntime, + ParticipantBehaviorSpecificationRuntime, +) + +from .participant_control_intents import ParticipantControlIntent + +_ORDER_STRATEGY = "total-effective-order" + + +def participant_control_rejection_reason( + specification: ParticipantBehaviorSpecificationRuntime, + transition: MixedControlTransitionRuntime, + state: MixedControlControllerStateRuntime, + intent: ParticipantControlIntent, + *, + current_state: str, + current_revision: int, + target_rejection_reason: str | None, +) -> str | None: + """Return the first rejection in the lifecycle's binding order.""" + + checks = ( + _order_strategy_rejection(specification), + _intent_policy_rejection(specification, intent), + _state_revision_rejection(transition, intent, current_state, current_revision), + _transition_policy_rejection(specification, transition), + target_rejection_reason, + _authority_status_rejection(state), + _authority_window_rejection(state, transition), + ) + return next((reason for reason in checks if reason is not None), None) + + +def _order_strategy_rejection( + specification: ParticipantBehaviorSpecificationRuntime, +) -> str | None: + return None if specification.mixed_control_order_strategy == _ORDER_STRATEGY else "unsupported-order-strategy" + + +def _intent_policy_rejection( + specification: ParticipantBehaviorSpecificationRuntime, + intent: ParticipantControlIntent, +) -> str | None: + return None if intent.policy_revision == specification.mixed_control_policy_revision else "stale-policy" + + +def _state_revision_rejection( + transition: MixedControlTransitionRuntime, + intent: ParticipantControlIntent, + current_state: str, + current_revision: int, +) -> str | None: + matches_current_state = ( + intent.expected_state_revision == current_revision + and transition.expected_state_revision == current_revision + and transition.from_state_address == current_state + ) + return None if matches_current_state else "stale-state" + + +def _transition_policy_rejection( + specification: ParticipantBehaviorSpecificationRuntime, + transition: MixedControlTransitionRuntime, +) -> str | None: + return None if transition.policy_revision == specification.mixed_control_policy_revision else "stale-policy" + + +def _authority_status_rejection(state: MixedControlControllerStateRuntime) -> str | None: + return None if state.authority_status == "active" else "revoked-authority" + + +def _authority_window_rejection( + state: MixedControlControllerStateRuntime, + transition: MixedControlTransitionRuntime, +) -> str | None: + state_is_current = state.valid_from_order <= transition.effective_order <= state.valid_until_order + transition_is_current = transition.valid_from_order <= transition.effective_order <= transition.valid_until_order + return None if state_is_current and transition_is_current else "late-authority" + + +__all__ = ("participant_control_rejection_reason",) diff --git a/implementations/python/packages/raes_runtime/participant_control_targets.py b/implementations/python/packages/raes_runtime/participant_control_targets.py index 57884a0e1..12b5d7126 100644 --- a/implementations/python/packages/raes_runtime/participant_control_targets.py +++ b/implementations/python/packages/raes_runtime/participant_control_targets.py @@ -210,26 +210,30 @@ def _register( def _intent_target( intent: ParticipantControlIntent, ) -> tuple[ParticipantControlTargetKind, str, int] | None: + target: tuple[ParticipantControlTargetKind, str, int] | None if isinstance(intent, ParticipantProposalControlIntent): if intent.source_proposal_ref is None: - return None - assert intent.source_proposal_revision is not None - return ( - ParticipantControlTargetKind.PROPOSAL, - intent.source_proposal_ref, - intent.source_proposal_revision, - ) - if isinstance(intent, (ParticipantApprovalControlIntent, ParticipantDenialControlIntent)): - return ParticipantControlTargetKind.PROPOSAL, intent.proposal_ref, intent.proposal_revision - if isinstance(intent, ParticipantExternalDirectionControlIntent): - return intent.target_kind, intent.target_ref, intent.target_revision - if isinstance(intent, ParticipantInterventionControlIntent): - return intent.affected_target_kind, intent.affected_occurrence_ref, intent.affected_revision - if isinstance(intent, ParticipantOverrideControlIntent): - return intent.superseded_target_kind, intent.superseded_occurrence_ref, intent.superseded_revision - if isinstance(intent, ParticipantCancellationControlIntent): - return intent.target_kind, intent.target_ref, intent.target_revision - return None + target = None + else: + assert intent.source_proposal_revision is not None + target = ( + ParticipantControlTargetKind.PROPOSAL, + intent.source_proposal_ref, + intent.source_proposal_revision, + ) + elif isinstance(intent, (ParticipantApprovalControlIntent, ParticipantDenialControlIntent)): + target = (ParticipantControlTargetKind.PROPOSAL, intent.proposal_ref, intent.proposal_revision) + elif isinstance(intent, ParticipantExternalDirectionControlIntent): + target = (intent.target_kind, intent.target_ref, intent.target_revision) + elif isinstance(intent, ParticipantInterventionControlIntent): + target = (intent.affected_target_kind, intent.affected_occurrence_ref, intent.affected_revision) + elif isinstance(intent, ParticipantOverrideControlIntent): + target = (intent.superseded_target_kind, intent.superseded_occurrence_ref, intent.superseded_revision) + elif isinstance(intent, ParticipantCancellationControlIntent): + target = (intent.target_kind, intent.target_ref, intent.target_revision) + else: + target = None + return target __all__ = ( diff --git a/implementations/python/tests/test_api_409_participant_control_occurrences.py b/implementations/python/tests/test_api_409_participant_control_occurrences.py index c5cac7e06..214b9ab22 100644 --- a/implementations/python/tests/test_api_409_participant_control_occurrences.py +++ b/implementations/python/tests/test_api_409_participant_control_occurrences.py @@ -418,13 +418,15 @@ def test_rejected_typed_target_preserves_the_invalid_attempt_without_becoming_a_ ) ) + records = [direction, intervention] + declarations = [ + _declaration("external-direction"), + _declaration("intervention"), + ] with pytest.raises(ValueError, match="typed target reference and kind must resolve"): validate_participant_control_occurrence_context( - [direction, intervention], - declarations=[ - _declaration("external-direction"), - _declaration("intervention"), - ], + records, + declarations=declarations, ) diff --git a/implementations/python/tests/test_run_310_supervisory_lifecycle.py b/implementations/python/tests/test_run_310_supervisory_lifecycle.py index 200d4fa4a..d05226f12 100644 --- a/implementations/python/tests/test_run_310_supervisory_lifecycle.py +++ b/implementations/python/tests/test_run_310_supervisory_lifecycle.py @@ -352,13 +352,15 @@ def test_atomic_control_transition_commit_checks_head_and_persists_all_outputs( ] } ) + conflicting_record = replace(record, idempotency_key="scope-key-2") + conflicting_audit = replace(audit, operation_id="operation-2") with pytest.raises(ValueError, match="expected control history head"): restarted.commit_control_transition( participant_address="participant.behavior.red-agent", expected_head=None, snapshot=conflicting, - record=replace(record, idempotency_key="scope-key-2"), - audit_event=replace(audit, operation_id="operation-2"), + record=conflicting_record, + audit_event=conflicting_audit, ) assert restarted.load_snapshot().participant_control_history == snapshot.participant_control_history @@ -475,19 +477,21 @@ def test_supervisory_control_is_subject_bound_idempotent_and_state_revision_boun payload_ref="payload:proposal-1", ) + unbound_identity = _identity(bound=False) with pytest.raises(PermissionError, match="subject"): control_plane.record_participant_control( _PARTICIPANT, intent, - identity=_identity(bound=False), + identity=unbound_identity, idempotency_key="key-1", ) assert not control_plane.snapshot.participant_control_history + other_target_identity = replace(_identity(), target_name="other-target") with pytest.raises(PermissionError, match="target"): control_plane.record_participant_control( _PARTICIPANT, intent, - identity=replace(_identity(), target_name="other-target"), + identity=other_target_identity, idempotency_key="key-target", ) assert not control_plane.snapshot.participant_control_history @@ -508,11 +512,12 @@ def test_supervisory_control_is_subject_bound_idempotent_and_state_revision_boun assert len(control_plane.snapshot.participant_control_history[_PARTICIPANT]) == 1 changed = intent.model_copy(update={"proposal_id": "proposal-2"}) + changed_identity = _identity() with pytest.raises(ValueError, match="different semantics"): control_plane.record_participant_control( _PARTICIPANT, changed, - identity=_identity(), + identity=changed_identity, idempotency_key="key-1", ) @@ -816,11 +821,12 @@ def fail_atomic_write(path: Path, content: str) -> None: raise OSError("commit failed") monkeypatch.setattr(store, "_atomic_write", fail_atomic_write) + identity = _identity() with pytest.raises(OSError, match="commit failed"): control_plane.record_participant_control( _PARTICIPANT, intent, - identity=_identity(), + identity=identity, idempotency_key="key-1", ) From 6a6ddacc16eeffd9fe232695038958cb71bc1d7b Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 16:18:28 +0200 Subject: [PATCH 09/55] Integrate episode readiness with decision surfaces --- .ground-control.yaml | 1 - .../valid/human-candidate.json | 60 +- .../participant-decision-surface-v1.json | 6 +- .../participant-decision-surface-v1.json | 200 ++++++ ...decision-surface-and-exposure-semantics.md | 47 ++ ...311-sem-220-order-zero-anchor-preflight.md | 351 +++++++++++ .../raes_contracts/contracts/__init__.py | 21 +- .../contracts/participant_decision_surface.py | 31 + .../participant_decision_surface_anchor.py | 63 ++ .../raes_processor/models/__init__.py | 8 + .../raes_processor/models/decision_surface.py | 46 +- .../models/decision_surface_anchor.py | 306 ++++++++++ .../raes_runtime/participant_control.py | 8 +- ...st_sem_220_participant_decision_surface.py | 571 +++++++++++++++++- specs/formal/participant-semantics/README.md | 36 +- 15 files changed, 1686 insertions(+), 69 deletions(-) create mode 100644 docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py create mode 100644 implementations/python/packages/raes_processor/models/decision_surface_anchor.py diff --git a/.ground-control.yaml b/.ground-control.yaml index 35764b894..eb852fb8f 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -25,7 +25,6 @@ requirements: routing: enabled: true default_provider: claude - default_fallback: parent stages: {} sonarcloud: project_key: Brad-Edwards_aces diff --git a/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json b/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json index 8ace14172..df9464ca9 100644 --- a/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json +++ b/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json @@ -1,18 +1,18 @@ { - "surface_id": "decision-surfaces.red.human.order-4", + "surface_id": "decision-surfaces.red.human.order-1", "participant_address": "participant.behavior.red-agent", "episode_id": "episode-1", "observation_point": "behavior-history:4", - "observation_order": 4, + "observation_order": 1, "behavior_specification_address": "participant.behavior-specification.red-surface", "observation_boundary_address": "participant.observation-boundary.red-view", - "context_view_ref": "context-views.red.episode-1.order-4", + "context_view_ref": "context-views.red.episode-1.order-1", "implementation_selection_ref": "participant-selections.red.human.v1", "decision_control_mode": "human-supervised", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", "exposure_policy_ref": "exposure-policy.red.v1", - "visibility_projection_ref": "visibility-projection.red.order-4", + "visibility_projection_ref": "visibility-projection.red.order-1", "visible_context_refs": [ "context.public" ], @@ -54,10 +54,11 @@ "open_extension_binding_ref": null }, "evidence_refs": [ - "evidence.surface.red.order-4" + "evidence.surface.red.order-1" ], "provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1", + "participant-behavior-event:sha256:4444444444444444444444444444444444444444444444444444444444444444" ], "marking_definition_refs": [ "markings.participant-visible.v1" @@ -66,6 +67,23 @@ "semantic_limitations": [ "Candidate membership does not imply eligibility or admission" ], + "projection_anchor": { + "participant_address": "participant.behavior.red-agent", + "episode_id": "episode-1", + "decision_surface_order": 1, + "event_ref": "participant-behavior-event:sha256:4444444444444444444444444444444444444444444444444444444444444444", + "anchor_order": 4, + "evidence_refs": [ + "evidence.surface.red.order-1" + ], + "provenance_refs": [ + "participant-behavior-event:sha256:4444444444444444444444444444444444444444444444444444444444444444" + ], + "anchor_kind": "behavior_event", + "event_type": "observation_emitted", + "action_instance_id": "scan-1", + "history_prefix_length": 5 + }, "audience_scope_ref": "audience.participant.behavior.red-agent", "exposure_bindings": [ { @@ -76,7 +94,7 @@ "episode_id": "episode-1", "audience_scope_ref": "audience.participant.behavior.red-agent", "observation_point": "behavior-history:4", - "observation_order": 4, + "observation_order": 1, "visibility_basis_ref": "visibility-bases.context.public", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", @@ -95,19 +113,19 @@ "markings.participant-visible.v1" ], "source_provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "result_provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "declassification_basis_ref": null, "redaction_policy_ref": null, "transformation_rule_ref": null, "evidence_refs": [ - "evidence.surface.red.order-4" + "evidence.surface.red.order-1" ], "provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "loss_and_limitations": [ "No known projection loss" @@ -123,7 +141,7 @@ "episode_id": "episode-1", "audience_scope_ref": "audience.participant.behavior.red-agent", "observation_point": "behavior-history:4", - "observation_order": 4, + "observation_order": 1, "visibility_basis_ref": "visibility-bases.participant.action-contract.scan", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", @@ -142,19 +160,19 @@ "markings.participant-visible.v1" ], "source_provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "result_provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "declassification_basis_ref": null, "redaction_policy_ref": null, "transformation_rule_ref": null, "evidence_refs": [ - "evidence.surface.red.order-4" + "evidence.surface.red.order-1" ], "provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "loss_and_limitations": [ "No known projection loss" @@ -170,7 +188,7 @@ "episode_id": "episode-1", "audience_scope_ref": "audience.participant.behavior.red-agent", "observation_point": "behavior-history:4", - "observation_order": 4, + "observation_order": 1, "visibility_basis_ref": "visibility-bases.participant.behavior-specification.red-surface.tool-affordance.scanner", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", @@ -189,19 +207,19 @@ "markings.participant-visible.v1" ], "source_provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "result_provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "declassification_basis_ref": null, "redaction_policy_ref": null, "transformation_rule_ref": null, "evidence_refs": [ - "evidence.surface.red.order-4" + "evidence.surface.red.order-1" ], "provenance_refs": [ - "provenance.surface.red.human.order-4" + "provenance.surface.red.human.order-1" ], "loss_and_limitations": [ "No known projection loss" diff --git a/contracts/schema-publication/entries/participant-decision-surface-v1.json b/contracts/schema-publication/entries/participant-decision-surface-v1.json index 0e1554909..e00be1db3 100644 --- a/contracts/schema-publication/entries/participant-decision-surface-v1.json +++ b/contracts/schema-publication/entries/participant-decision-surface-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-decision-surface-v1", "schema_path": "contracts/schemas/control-plane/participant-decision-surface-v1.json", "stability": "draft", - "content_hash": "77920c1edfa6fe1785d50d6d33a194acd59fdc41845c016eedbf88af342bd2e2", + "content_hash": "e65ab17f47266f0b21964e5d97517b3d5e9ff998454c48605d2fc40057630c13", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "77920c1edfa6fe1785d50d6d33a194acd59fdc41845c016eedbf88af342bd2e2" + "summary": "Added an optional tagged projection anchor that distinguishes derived decision-surface order from episode-readiness and terminal-observation history order, evidence, and provenance without changing existing decision-surface fields.", + "content_hash": "e65ab17f47266f0b21964e5d97517b3d5e9ff998454c48605d2fc40057630c13" } } diff --git a/contracts/schemas/control-plane/participant-decision-surface-v1.json b/contracts/schemas/control-plane/participant-decision-surface-v1.json index 2b9bfb315..52f448346 100644 --- a/contracts/schemas/control-plane/participant-decision-surface-v1.json +++ b/contracts/schemas/control-plane/participant-decision-surface-v1.json @@ -153,6 +153,90 @@ "title": "ParticipantDecisionSurfaceActionEntryModel", "type": "object" }, + "ParticipantDecisionSurfaceBehaviorAnchorModel": { + "additionalProperties": false, + "description": "One terminal observation and exact prefix anchoring a later surface.", + "properties": { + "action_instance_id": { + "minLength": 1, + "title": "Action Instance Id", + "type": "string" + }, + "anchor_kind": { + "const": "behavior_event", + "title": "Anchor Kind", + "type": "string" + }, + "anchor_order": { + "minimum": 0, + "title": "Anchor Order", + "type": "integer" + }, + "decision_surface_order": { + "minimum": 0, + "title": "Decision Surface Order", + "type": "integer" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "event_ref": { + "minLength": 1, + "title": "Event Ref", + "type": "string" + }, + "event_type": { + "const": "observation_emitted", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "history_prefix_length": { + "minimum": 1, + "title": "History Prefix Length", + "type": "integer" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + } + }, + "required": [ + "participant_address", + "episode_id", + "decision_surface_order", + "event_ref", + "anchor_order", + "evidence_refs", + "provenance_refs", + "anchor_kind", + "event_type", + "action_instance_id", + "history_prefix_length" + ], + "title": "ParticipantDecisionSurfaceBehaviorAnchorModel", + "type": "object" + }, "ParticipantDecisionSurfaceCandidateSetFormModel": { "additionalProperties": false, "properties": { @@ -286,6 +370,84 @@ "title": "ParticipantDecisionSurfaceConstrainedFormModel", "type": "object" }, + "ParticipantDecisionSurfaceEpisodeReadinessAnchorModel": { + "additionalProperties": false, + "description": "RUN-311 ``episode_running`` anchor for one episode's initial surface.", + "properties": { + "anchor_kind": { + "const": "episode_readiness", + "title": "Anchor Kind", + "type": "string" + }, + "anchor_order": { + "minimum": 0, + "title": "Anchor Order", + "type": "integer" + }, + "decision_surface_order": { + "minimum": 0, + "title": "Decision Surface Order", + "type": "integer" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "episode_sequence_number": { + "minimum": 0, + "title": "Episode Sequence Number", + "type": "integer" + }, + "event_ref": { + "minLength": 1, + "title": "Event Ref", + "type": "string" + }, + "event_type": { + "const": "episode_running", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + } + }, + "required": [ + "participant_address", + "episode_id", + "decision_surface_order", + "event_ref", + "anchor_order", + "evidence_refs", + "provenance_refs", + "anchor_kind", + "event_type", + "episode_sequence_number" + ], + "title": "ParticipantDecisionSurfaceEpisodeReadinessAnchorModel", + "type": "object" + }, "ParticipantDecisionSurfaceExposureBindingModel": { "additionalProperties": false, "description": "Resolved SEM-226 basis for one item admitted to a surface.", @@ -788,6 +950,32 @@ "title": "Participant Address", "type": "string" }, + "projection_anchor": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "behavior_event": "#/$defs/ParticipantDecisionSurfaceBehaviorAnchorModel", + "episode_readiness": "#/$defs/ParticipantDecisionSurfaceEpisodeReadinessAnchorModel" + }, + "propertyName": "anchor_kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/ParticipantDecisionSurfaceEpisodeReadinessAnchorModel" + }, + { + "$ref": "#/$defs/ParticipantDecisionSurfaceBehaviorAnchorModel" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Projection Anchor" + }, "projection_policy_ref": { "minLength": 1, "title": "Projection Policy Ref", @@ -911,6 +1099,18 @@ ], "level": "error", "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_surface_relations" + }, + { + "description": "A typed projection anchor, when present, must agree with the surface participant, episode, anchor-local decision-surface order, evidence, and provenance without merging lifecycle and behavior histories.", + "id": "decision-surface-projection-anchor-agreement", + "inputs": [ + { + "contract_id": "participant-decision-surface-v1", + "instance_path": "#/projection_anchor" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_projection_anchor" } ], "x-aces-semantic-profile": { diff --git a/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md b/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md index a00acecb6..efd50210b 100644 --- a/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md +++ b/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md @@ -232,6 +232,53 @@ implement their rows through existing package ownership and must update the matrix when a carrier or enforcement point changes. They may strengthen a row but may not redefine the joint relations independently. +### 8. Ground the first surface in RUN-311 episode readiness + +A participant episode's first decision surface is grounded by the existing +RUN-311 lifecycle rather than by a fabricated behavior event. +`episode_initialized`, `episode_reset`, and `episode_restarted` establish an +episode generation; the following `episode_running` event is the authoritative +readiness anchor. Only then may the runtime derive the initial context from +compiled `V_p,0` and project `D(p,e,0)`. + +The public projection anchor is tagged as either episode readiness or a +behavior event. It references one event in one owning history and carries the +participant, episode, per-episode decision-surface order, anchor-local order, +stable event reference, evidence, and provenance. Episode lifecycle and +participant behavior remain separate histories. The tagged anchor makes the +surface `observation_order` the per-episode decision-surface coordinate, while +`anchor_order` remains the referenced lifecycle- or behavior-history +coordinate. RUN-311 `sequence_number` remains only the episode-generation +coordinate. Readiness derives decision-surface order zero. Each terminal +`observation_emitted` event advances the derived decision-surface order by one; +callers cannot choose that value independently. + +The portable order is: + +```text +episode_initialized | episode_reset | episode_restarted + -> episode_running + -> V_p,0 context + -> D(p,e,0) + -> proposal and selection + -> admitted action_attempted + -> state_transition_recorded + -> terminal observation_emitted + -> D(p,e,1) +``` + +Proposal and selection do not create participant behavior. Admission creates +the first behavior event. A reset or restart creates a new episode id and a new +order-zero surface with no behavior prefix from the new episode. + +Anchor shape is not authority. Projection and admission resolve the anchor +against the current trusted runtime snapshot and complete participant-local +history. A standalone event, isolated fragment, final snapshot, prior-episode +surface, surface superseded by later behavior, or unanchored surface presented +to the runtime admission path fails closed. Initial projection continues to use +the compiled initial view relation; later projection continues to use the +existing behavior-anchor indexes and effective view-relation selector. + ## Alternatives Considered ### Add a flat participant `tools` list diff --git a/docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md b/docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md new file mode 100644 index 000000000..2e2328551 --- /dev/null +++ b/docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md @@ -0,0 +1,351 @@ +# Issue 909 RUN-311 / SEM-220 Order-Zero Anchor Preflight + +Date: 2026-07-26 + +Issue: #909. + +Requirements: none. The GitHub issue is the authoritative contract. + +This note records architecture guardrails for integrating RUN-311 episode +readiness with the existing SEM-210/SEM-220 projection. It is non-normative +preflight guidance. It does not implement a projector, contract, schema, +control-plane operation, persistence path, or test. + +## Existing Authorities + +- ADR-013 and `raes_contracts.participant_episode` own participant-episode + identity, state, control actions, lifecycle history, reset/restart, and + `sequence_number`. +- ADR-022 and `specs/formal/participant-semantics/README.md` own action-linked + participant behavior history, `V_p,t`, observation boundaries, visibility + transitions, and the separation of participant-visible state from world truth + and archival evidence. +- ADR-054 owns the separation between episode lifecycle and observable behavior + lifecycle. A silent episode with empty behavior history is valid; no bootstrap + action may be invented to make a history non-empty. +- ADR-083, `ParticipantDecisionSurfaceModel`, + `ParticipantDecisionSurfaceProjectionInput`, and + `project_participant_decision_surface()` own `D(p,e,o)`, including its + participant/episode scope, compiled semantic inputs, context/exposure + projection, order/event/evidence basis, and selection meaning. +- The issue-119 preflight remains the cross-cutting authority for SEM-219, + SEM-220, and SEM-226 reuse, security, persistence, errors, and package + boundaries. This note narrows the unresolved initial-anchor and ordering + relationship; it does not fork that design. + +## Architecture Decision And Guardrails + +### Use one typed projection anchor, not a third history + +The projection boundary needs a small, closed, public projection-anchor value +or equivalent tagged contract. It references exactly one event from exactly one +of the two existing histories: + +- an **episode-readiness anchor** references a RUN-311 + `ParticipantEpisodeHistoryEvent`; or +- a **behavior-event anchor** references a + `ParticipantBehaviorHistoryEvent`. + +The two histories remain separate typed inputs. Do not create a heterogeneous +event list, add episode lifecycle values to +`ParticipantBehaviorHistoryEventType`, add action fields to episode history, or +depend on Python duck typing. + +The anchor must carry or resolve, without caller-authored semantic strings: + +- participant address and episode id; +- per-episode decision-surface order; +- anchor kind and stable event reference; +- the anchor's order in its owning history domain; +- evidence/provenance references supporting the projection; and +- for a behavior anchor, the exact behavior-history prefix used to derive + `V_p,o`. + +Lifecycle order, decision-surface order, and behavior-history order are +different coordinates. A field may not silently change meaning by anchor kind. +If the published decision-surface v1 carrier cannot represent those coordinates +without overloading `observation_order` or encoding structure into +`observation_point`, the contract must be compatibility-classified and +versioned under ADR-061. Do not silently reinterpret an existing v1 field. + +A typed event proves shape, not authority. The anchor must be resolved against +the current trusted `RuntimeSnapshot` lifecycle result/history and, for later +surfaces, the exact participant-local behavior-history prefix. A caller-created +event object, event-ref string, final snapshot, or isolated history fragment is +not sufficient. + +### `episode_running` is the readiness anchor + +`episode_initialized` records creation of the first episode identity and its +initialize control action. It precedes readiness and is not sufficient to expose +participant context or accept a decision. + +`episode_running` is the authoritative readiness anchor for every new episode: + +- for the first episode it follows `episode_initialized`; +- for reset it follows `episode_reset`; and +- for restart it follows `episode_restarted`. + +The readiness resolver must require exact participant and episode agreement, a +RUNNING live result, a valid lifecycle history whose matching +`episode_running` event is in the current episode scope, and agreement between +the live result and history head under +`iter_participant_episode_snapshot_violations()`. A terminal, superseded, +cross-participant, or previous-episode running event fails closed. + +RUN-311 `sequence_number` identifies the participant's episode generation. It is +not lifecycle-event order, decision-surface order, behavior-step order, an +action count, or a visibility-transition order. + +### Portable ordering relationship + +For a newly initialized episode, the required causal order is: + +| Domain | Order | Meaning | +| --- | ---: | --- | +| episode lifecycle | first event in the new scope | `episode_initialized` for the first episode, or `episode_reset` / `episode_restarted` for a successor episode | +| episode lifecycle | next event in the same scope | `episode_running`; authoritative readiness anchor | +| projection | derived from the readiness anchor | initial participant context from SEM-210 `V_p,0`, with the existing boundary, audience, exposure, marking, redaction, evidence, provenance, and apparatus gates | +| decision surface | 0 | `D(p,e,0)`, sharing the readiness/event/evidence basis with its context view; behavior history is still empty | +| decision lifecycle | after surface 0 | proposal and selection reference surface 0; neither is an action attempt | +| behavior history | 0 | the first admitted `action_attempted` event | +| behavior history | subsequent ordered events | the admitted action's state transition and terminal observation under the existing action-instance discipline | +| decision surface | 1 | the next surface, anchored to the applicable terminal observation and its behavior-history prefix | + +The current canonical admission path appends +`action_attempted -> state_transition_recorded -> observation_emitted` +consecutively. The projection abstraction must nevertheless key later surfaces +to the exact behavior event reference and behavior-history order, not to +arithmetic such as `3*n+2`; that preserves the seam for long-running, +orphaned-action, partial-order, or future lifecycle variants. + +Reset and restart create a new `episode_id`, increment RUN-311 +`sequence_number`, and restart decision-surface order at zero with empty +behavior history for the new episode. They never reuse the prior episode's +initial anchor, surface, selection, proposal, behavior prefix, or exposure +authorization. + +### Preserve the existing visibility and selection paths + +The initial anchor changes only how the initial relation is grounded. `V_p,0` +continues to come from compiled `view_rules`. Later surfaces continue to use +`_participant_behavior_history_anchor_indexes()` and +`participant_observation_effective_relation()` so a visibility transition is +effective only when its declared action-linked anchor exists at or before the +selected behavior-history order. + +Initial context and surface projection must still pass the same: + +- compiled behavior-specification, action-contract, argument-shape, + observation-boundary, and affordance resolution; +- audience, source-layer, transformation, marking, redaction, withholding, + exposure-policy revision, authorization, evidence, provenance, and apparatus + checks; +- context-view / decision-surface relational agreement; and +- SEM-220 selection-shape, surface membership, eligibility, apparatus, and + admission checks. + +`bind_participant_decision_surface_selection()` and +`RuntimeControlPlane.admit_participant_decision_surface_selection()` remain the +selection path. The live runtime must additionally reject a surface whose +participant, episode, readiness/behavior anchor, or decision order is no longer +current. A surface from a prior reset/restart must not be admitted into the +current episode merely because its action and participant addresses still +resolve. + +Invalid proposal, selection, argument-shape, apparatus, exposure, or admission +inputs create no participant behavior event. Only the existing admitted-action +path may append `action_attempted`, its transition, and its observation. + +## Canonical Incumbents To Reuse + +- **Lifecycle contracts and validation:** + `ParticipantEpisodeExecutionState`, `ParticipantEpisodeHistoryEvent`, + `ParticipantEpisodeHistoryEventModel`, + `iter_participant_episode_snapshot_violations()`, and + `BaseParticipantRuntime.initialize()`, `reset()`, and `restart()`. +- **Runtime state and persistence:** `RuntimeSnapshot`, + `RuntimeControlPlane`, `execute_participant_action()`, `ControlPlaneStore`, + `InMemoryControlPlaneStore`, and `LocalControlPlaneStore`. Projection state + must not move into a new current-surface store or snapshot `metadata`. +- **Compiled semantic scope:** `RuntimeModel`, + `ParticipantBehaviorSpecificationRuntime`, + `ParticipantActionContractRuntime`, + `ParticipantObservationBoundaryRuntime`, + `ParticipantToolAffordanceRuntime`, and the compiler-produced + `participant.*` addresses. +- **Visibility:** `_participant_behavior_initial_view_relation()`, + `_participant_behavior_history_anchor_indexes()`, + `participant_observation_effective_relation()`, and the compiled + `view_relation_timeline`. There must be one effective-relation algorithm. +- **Decision and exposure:** `ParticipantDecisionSurfaceProjectionInput`, + `project_participant_decision_surface()`, + `ParticipantExposureResolvers`, projection-policy revision selection, + exposure authorization/occurrence validation, + `ParticipantDecisionSurfaceModel`, `ParticipantContextViewModel`, and + `validate_participant_decision_surface_context()`. +- **Selection and admission:** `ParticipantDecisionSurfaceSelectionModel`, + `ParticipantValidatedActionSelection`, + `ParticipantDecisionSurfaceBindingResolvers`, + `bind_participant_decision_surface_selection()`, + `ParticipantActionAdmissionRequest`, + `participant_action_admission_request_violations()`, and + `ParticipantControlMixin.admit_participant_action()`. +- **Backend transition gate:** `_call_backend_apply()`, + participant runtime state/history transition diagnostics, + `ParticipantBehaviorHistoryEventModel`, and + `iter_participant_behavior_history_violations()`. +- **Contracts and schema governance:** `ContractModel`, + `schema_bundle()`, `contracts/schemas/`, `contracts/fixtures/`, + `contracts/schema-publication-manifest.json`, and + `contracts/schema-publication/entries/`. +- **Diagnostics and observability:** `Diagnostic`, `Severity`, + `OperationReceipt`, `OperationStatus`, `AuditEvent`, and existing + control-plane audit recording. Audit and raw logs are operational records, + not substitutes for the semantic lifecycle/behavior anchor. + +## Cross-Cutting Layers The Design Must Pass + +### Shape and semantic validation + +1. Public payloads remain closed `ContractModel` shapes with + `extra="forbid"` behavior and JSON Schema parity. +2. Lifecycle payloads normalize through the existing RUN-311 types and complete + snapshot invariants; accepting a standalone structurally valid event is + insufficient. +3. Participant, episode, behavior, boundary, action, affordance, argument-shape, + and anchor refs resolve against the compiled runtime model and trusted + snapshot. +4. Initial projection selects only compiled `V_p,0`; later projection uses the + existing behavior-anchor indexes and effective-relation selector. +5. Exposure resolvers validate immutable policy version/digest, effective + revision, exact participant/episode/order/apparatus coordinates, + authorization, markings, evidence, provenance, and any realized occurrence. +6. The context-view relational validator agrees with the surface on scope, + observation point, payload ref, projection, evidence, provenance, markings, + redaction, and limitations. +7. Selection binding validates surface identity/order, membership, eligibility, + support, argument shape, proposal coordinates, apparatus selection, exposure + policy, and admission-request agreement before normal action admission. +8. Backend apply validation preserves the predecessor snapshot, append-only + lifecycle/behavior histories, action-instance uniqueness, exact event order, + live episode scope, and terminal observation contract. + +### Authentication, authorization, secrets, and errors + +- No new HTTP endpoint is required by this integration. If an HTTP surface is + later added or an existing route is extended, it must enter through + `create_control_plane_app()`, + `ControlPlaneSecurityConfig.strict_defaults()`, bearer or verified-proxy + identity, target binding, `ControlPlaneRole` read/mutation authorization, + request-size guards, request fingerprints, idempotency, and `AuditEvent`. +- Control-plane caller authorization, scenario participant authority, + participant visibility, exposure authorization, and action admission are five + separate gates. Success at one does not imply another. +- Bearer tokens, credentials, hidden prompts, answer material, raw evidence, + raw exposure/configuration bodies, backend object representations, and full + tracebacks must not enter the anchor, surface, context view, snapshot, + diagnostic, audit details, or public error detail. Use stable refs, digests, + markings, redaction policies, and governed evidence/provenance. +- Expected contract and semantic failures use existing `ValueError`/`TypeError` + normalization at library boundaries, structured `Diagnostic` values and + operation envelopes at the control plane, or bounded existing 4xx details at + HTTP boundaries. Unexpected HTTP failures keep the redacted + `{"detail": "internal server error"}` envelope. Do not add an anchor-specific + exception hierarchy. +- This design needs no new environment binding, secret provider, config file, + subprocess, socket, filesystem path, or command-line option. A later backend + adapter must not place tokens, credentials, proposal payloads, hidden context, + or policy bodies in process argv, environment dumps, shell strings, + stdout/stderr, or logs. Existing typed adapter calls and injected resolvers are + the boundary; no `shell=True` execution is justified. + +### Persistence, replay, and observability + +- The current `RuntimeSnapshot` and `ControlPlaneStore` remain authoritative for + live episode and behavior history. Local persistence keeps its atomic snapshot + writes and append-only audit path. +- An initial anchor is valid only while it resolves to the current running + episode. Reset, restart, termination, participant mismatch, history + truncation, or live-result/history disagreement invalidates it. +- A later anchor must identify exactly one event in the supplied complete + participant/episode behavior prefix. Empty behavior history is permitted only + with a valid readiness anchor for decision order zero. +- Operation idempotency/request fingerprints and action-instance uniqueness + remain the mutation replay guards. Semantic surface replay also requires live + participant/episode/anchor agreement; idempotency alone does not make an old + surface current. +- Projection failures and authorization denials use existing diagnostics and + audit events. Do not add a decision-surface log schema, replay database, + side-channel cache, or treat logs/audit events as participant-visible + evidence. + +## Extensibility Seam + +The seam is the tagged, participant/episode-scoped projection anchor plus its +trusted resolver. Adding another truthful pre-action readiness source, partial +ordering basis, or externally realized participant must add a new governed +anchor variant/resolver rule without: + +- editing the RUN-311 or behavior-history event enums; +- changing `V_p,0`; +- changing action, argument-shape, exposure, or admission meaning; +- adding another history or visibility algorithm; or +- overloading lifecycle `sequence_number`, decision-surface order, or behavior + order. + +Anchor identity/order/evidence is the parameter. Backend kind, participant kind, +UI form, prompt format, and APTL scenario are not. + +## Gotchas And Anti-Patterns + +Avoid: + +- using `episode_initialized` as readiness; +- treating the simultaneous timestamps currently emitted for + `episode_initialized` and `episode_running` as ordering evidence; +- using lifecycle list position or `sequence_number` as behavior order; +- inserting a fake setup action, observation, choice, or state transition to + make behavior history non-empty; +- passing a lifecycle object through a + `Sequence[ParticipantBehaviorHistoryEvent]` and relying on shared attributes; +- merging lifecycle and behavior events into one union history; +- allowing an arbitrary empty history without a current readiness anchor; +- accepting a final snapshot, standalone event DTO, caller-owned event ref, or + backend-private bootstrap record as projection authority; +- encoding anchor structure in free-form `details`, snapshot `metadata`, + `observation_point`, audit details, or a naming convention; +- copying the initial view relation or visibility-transition walk into a second + projector; +- using future behavior, policy, authorization, or disclosure to justify the + initial surface; +- allowing a stale surface from a terminated, reset, or restarted episode to + bind to the current episode; +- treating presentation as proposal, selection, admission, action, result, or + participant choice; +- weakening argument-shape, apparatus, exposure, SEM-211, or normal admission + checks for the first selection; +- treating `AuditEvent`, `OperationReceipt`, backend logs, or timestamps alone + as semantic evidence; +- adding a duplicate DTO family, schema registry, validator stack, exception + hierarchy, persistence store, audit/log path, or backend-specific bootstrap + protocol; or +- changing a published schema without generator parity, fixtures, publication + ledger updates, and ADR-061 compatibility classification. + +## Non-Goals And Implementation Boundaries + +- No second participant, episode, visibility, context, decision, exposure, or + history model. +- No arbitrary snapshot projection and no weakening of later time-indexed + behavior-history semantics. +- No participant UI, prompt, agent framework, tool runner, backend setup action, + credential broker, policy engine, or OS sandbox. +- No APTL-, TechVault-, coding-agent-, LLM-, RL-, human-, or backend-specific + initialization semantics. +- No exposure of evaluator-only state, hidden truth, private answer material, + raw evidence, credentials, or backend-private identifiers. +- No redesign of RUN-311 lifecycle, SEM-210 transitions, SEM-211 applicability, + SEM-214/216 context/audience views, SEM-226 exposure authorization, control + plane authentication, persistence, audit, diagnostics, or experiment + provenance. diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index f3ddb889d..a8b86c9c2 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -199,6 +199,11 @@ ParticipantDecisionSurfaceSelectionModel, validate_participant_decision_surface_context, ) +from .participant_decision_surface_anchor import ( + ParticipantDecisionSurfaceBehaviorAnchorModel, + ParticipantDecisionSurfaceEpisodeReadinessAnchorModel, + ParticipantDecisionSurfaceProjectionAnchorModel, +) from .participant_decision_surface_exposure import ( ParticipantDecisionSurfaceExposureBindingModel, ParticipantDecisionSurfaceExposureRealizationModel, @@ -439,17 +444,11 @@ "ParticipantAutonomousExecutionStateModel", "ParticipantBehaviorHistoryEventModel", "ParticipantContextViewModel", "ParticipantControlDeclarationModel", "ParticipantControlOccurrenceModel", "validate_participant_control_occurrence_context", - "ParticipantDecisionSurfaceActionEntryModel", "ParticipantDecisionSurfaceCandidateSetFormModel", - "ParticipantDecisionSurfaceConstrainedFormModel", "ParticipantDecisionSurfaceExposureBindingModel", - "ParticipantDecisionSurfaceExposureRealizationModel", "ParticipantDecisionSurfaceModel", - "ParticipantDecisionSurfaceOpenEndedFormModel", "ParticipantDecisionSurfaceSelectionModel", - "validate_participant_decision_surface_context", "ParticipantEpisodeHistoryEventModel", - "ParticipantEpisodeStateModel", "ParticipantExposurePolicyModel", "ParticipantFeatureSupportLevel", - "ParticipantFeatureSupportModel", "ParticipantHistoryViewBehaviorEventModel", - "ParticipantHistoryViewEpisodeEventModel", "ParticipantHistoryViewModel", - "ParticipantImplementationCapabilitiesModel", "ParticipantImplementationCompatibilityModel", - "ParticipantImplementationManifestModel", "ParticipantImplementationProvenanceModel", - "ParticipantImplementationSelectionModel", "ParticipantJointActionAccessSetModel", + "ParticipantDecisionSurfaceActionEntryModel", "ParticipantDecisionSurfaceBehaviorAnchorModel", "ParticipantDecisionSurfaceCandidateSetFormModel", "ParticipantDecisionSurfaceConstrainedFormModel", "ParticipantDecisionSurfaceEpisodeReadinessAnchorModel", "ParticipantDecisionSurfaceExposureBindingModel", "ParticipantDecisionSurfaceExposureRealizationModel", "ParticipantDecisionSurfaceModel", "ParticipantDecisionSurfaceOpenEndedFormModel", "ParticipantDecisionSurfaceProjectionAnchorModel", "ParticipantDecisionSurfaceSelectionModel", "validate_participant_decision_surface_context", + "ParticipantEpisodeHistoryEventModel", "ParticipantEpisodeStateModel", "ParticipantExposurePolicyModel", + "ParticipantFeatureSupportLevel", "ParticipantFeatureSupportModel", "ParticipantHistoryViewBehaviorEventModel", + "ParticipantHistoryViewEpisodeEventModel", "ParticipantHistoryViewModel", "ParticipantImplementationCapabilitiesModel", + "ParticipantImplementationCompatibilityModel", "ParticipantImplementationManifestModel", "ParticipantImplementationProvenanceModel", "ParticipantImplementationSelectionModel", "ParticipantJointActionAccessSetModel", "ParticipantJointActionRecordModel", "ParticipantLifecycleEventModel", "ParticipantObservationEnvelopeModel", "ParticipantObservationLossDescriptorModel", "ParticipantObservationStochasticContextModel", "ParticipantOutcomeInterpretationRecordModel", "ParticipantOutcomeReportModel", diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py index 3d7b3818a..805a80720 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py @@ -11,6 +11,7 @@ from .base import ContractModel, NonEmptyString from .participant_context import ParticipantContextViewModel +from .participant_decision_surface_anchor import ParticipantDecisionSurfaceProjectionAnchorModel from .participant_decision_surface_exposure import ( ParticipantDecisionSurfaceExposureBindingModel, ) @@ -353,6 +354,7 @@ class ParticipantDecisionSurfaceModel(ContractModel): marking_definition_refs: list[NonEmptyString] = Field(default_factory=list) redaction_policy_ref: NonEmptyString | None = None semantic_limitations: list[NonEmptyString] = Field(min_length=1) + projection_anchor: ParticipantDecisionSurfaceProjectionAnchorModel | None = None @model_validator(mode="after") def _validate_surface_relations(self) -> ParticipantDecisionSurfaceModel: @@ -369,8 +371,29 @@ def _validate_surface_relations(self) -> ParticipantDecisionSurfaceModel: _validate_surface_form_relations(self.form, entries_by_id, entries_by_address) _validate_surface_affordances(self.affordance_refs, self.action_entries) _validate_surface_exposure_bindings(self) + self._validate_projection_anchor() return self + def _validate_projection_anchor(self) -> None: + anchor = self.projection_anchor + if anchor is None: + return + mismatched = [ + name + for name, anchor_value, surface_value in ( + ("participant_address", anchor.participant_address, self.participant_address), + ("episode_id", anchor.episode_id, self.episode_id), + ("decision_surface_order", anchor.decision_surface_order, self.observation_order), + ) + if anchor_value != surface_value + ] + if mismatched: + raise ValueError("projection anchor disagrees with the decision surface on: " + ", ".join(mismatched)) + if not set(anchor.evidence_refs).issubset(self.evidence_refs): + raise ValueError("projection anchor evidence_refs must be carried by the decision surface") + if not set(anchor.provenance_refs).issubset(self.provenance_refs): + raise ValueError("projection anchor provenance_refs must be carried by the decision surface") + @classmethod def __get_pydantic_json_schema__( cls, @@ -404,6 +427,14 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_surface_relations", inputs=[{"contract_id": "participant-decision-surface-v1", "instance_path": "#/exposure_bindings"}], ) + _add_aces_invariant( + json_schema, + "decision-surface-projection-anchor-agreement", + "A typed projection anchor, when present, must agree with the surface participant, episode, anchor-local " + "decision-surface order, evidence, and provenance without merging lifecycle and behavior histories.", + validator="raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_projection_anchor", + inputs=[{"contract_id": "participant-decision-surface-v1", "instance_path": "#/projection_anchor"}], + ) return json_schema diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py new file mode 100644 index 000000000..dd9a6d57b --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py @@ -0,0 +1,63 @@ +"""Typed event anchors for SEM-220 participant decision surfaces.""" + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import Field, StrictInt, model_validator + +from .base import ContractModel, NonEmptyString + + +def _require_unique(values: list[str], field_name: str) -> None: + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must not contain duplicates") + + +class _ParticipantDecisionSurfaceProjectionAnchorBase(ContractModel): + """Coordinates shared by every trusted decision-surface event anchor.""" + + participant_address: NonEmptyString + episode_id: NonEmptyString + decision_surface_order: StrictInt = Field(ge=0) + event_ref: NonEmptyString + anchor_order: StrictInt = Field(ge=0) + evidence_refs: list[NonEmptyString] = Field(min_length=1) + provenance_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_anchor_refs(self) -> _ParticipantDecisionSurfaceProjectionAnchorBase: + _require_unique(self.evidence_refs, "evidence_refs") + _require_unique(self.provenance_refs, "provenance_refs") + if self.event_ref not in self.provenance_refs: + raise ValueError("projection anchor event_ref must be carried by provenance_refs") + return self + + +class ParticipantDecisionSurfaceEpisodeReadinessAnchorModel(_ParticipantDecisionSurfaceProjectionAnchorBase): + """RUN-311 ``episode_running`` anchor for one episode's initial surface.""" + + anchor_kind: Literal["episode_readiness"] + event_type: Literal["episode_running"] + episode_sequence_number: StrictInt = Field(ge=0) + + +class ParticipantDecisionSurfaceBehaviorAnchorModel(_ParticipantDecisionSurfaceProjectionAnchorBase): + """One terminal observation and exact prefix anchoring a later surface.""" + + anchor_kind: Literal["behavior_event"] + event_type: Literal["observation_emitted"] + action_instance_id: NonEmptyString + history_prefix_length: StrictInt = Field(ge=1) + + @model_validator(mode="after") + def _validate_history_prefix(self) -> ParticipantDecisionSurfaceBehaviorAnchorModel: + if self.history_prefix_length != self.anchor_order + 1: + raise ValueError("behavior projection anchor history_prefix_length must equal anchor_order + 1") + return self + + +ParticipantDecisionSurfaceProjectionAnchorModel = Annotated[ + ParticipantDecisionSurfaceEpisodeReadinessAnchorModel | ParticipantDecisionSurfaceBehaviorAnchorModel, + Field(discriminator="anchor_kind"), +] diff --git a/implementations/python/packages/raes_processor/models/__init__.py b/implementations/python/packages/raes_processor/models/__init__.py index 9c3bd91a8..4af082846 100644 --- a/implementations/python/packages/raes_processor/models/__init__.py +++ b/implementations/python/packages/raes_processor/models/__init__.py @@ -132,6 +132,11 @@ ParticipantDecisionSurfaceProjectionInput, project_participant_decision_surface, ) +from .decision_surface_anchor import ( + resolve_participant_behavior_projection_anchor, + resolve_participant_episode_readiness_anchor, + validate_participant_decision_surface_projection_anchor, +) from .history_event import ( ParticipantBehaviorHistoryEvent, ) @@ -288,6 +293,8 @@ "ParticipantPhaseRealization", "ParticipantRuntimeLifecyclePhase", "project_participant_decision_surface", + "resolve_participant_behavior_projection_anchor", + "resolve_participant_episode_readiness_anchor", "ParticipantTemporalRuntimeContext", "ParticipantTemporalState", "ParticipantTemporalStateTransition", @@ -303,6 +310,7 @@ "RuntimeModel", "RuntimeSnapshot", "RuntimeSnapshotEnvelope", + "validate_participant_decision_surface_projection_anchor", "RuntimeTemplate", "ScriptRuntime", "Severity", diff --git a/implementations/python/packages/raes_processor/models/decision_surface.py b/implementations/python/packages/raes_processor/models/decision_surface.py index 3b434ecfc..dc5d95f2c 100644 --- a/implementations/python/packages/raes_processor/models/decision_surface.py +++ b/implementations/python/packages/raes_processor/models/decision_surface.py @@ -5,7 +5,11 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass -from raes_contracts.contracts import ParticipantDecisionSurfaceModel +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceModel, + ParticipantDecisionSurfaceProjectionAnchorModel, +) +from raes_contracts.runtime_state import RuntimeSnapshot from .behavior_anchor_checks import participant_observation_effective_relation from .behavior_anchor_index import _participant_behavior_history_anchor_indexes @@ -14,6 +18,7 @@ ParticipantBehaviorSpecificationRuntime, ParticipantObservationBoundaryRuntime, ) +from .decision_surface_anchor import _validate_resolved_projection_anchor from .history_event import ParticipantBehaviorHistoryEvent from .participant_exposure import project_participant_exposure_bindings from .participant_exposure_authority import ( @@ -67,6 +72,7 @@ class ParticipantDecisionSurfaceProjectionInput: marking_definition_refs: tuple[str, ...] redaction_policy_ref: str | None semantic_limitations: tuple[str, ...] + projection_anchor: ParticipantDecisionSurfaceProjectionAnchorModel | None = None def _surface_action_refs(form: Mapping[str, object]) -> tuple[str, tuple[str, ...]]: @@ -187,9 +193,20 @@ def _validate_context_visibility( def _validate_projection_history( + runtime_model: RuntimeModel, history_events: Sequence[ParticipantBehaviorHistoryEvent], projection: ParticipantDecisionSurfaceProjectionInput, -) -> None: + runtime_snapshot: RuntimeSnapshot | None, +) -> int: + if projection.projection_anchor is not None: + if runtime_snapshot is None: + raise ValueError("anchored participant decision surfaces require the current trusted RuntimeSnapshot") + return _validate_resolved_projection_anchor( + runtime_model, + runtime_snapshot, + history_events, + projection, + ) if not history_events: raise ValueError("participant decision surfaces require time-indexed history; a final snapshot is insufficient") if projection.observation_order < 0 or projection.observation_order >= len(history_events): @@ -199,6 +216,7 @@ def _validate_projection_history( for event in history_events ): raise ValueError("participant decision surface history must contain one participant and episode") + return projection.observation_order def _resolve_projection_scope( @@ -220,12 +238,14 @@ def _resolve_projection_scope( def _projection_visibility_relation( history_events: Sequence[ParticipantBehaviorHistoryEvent], + *, + history_order: int, projection: ParticipantDecisionSurfaceProjectionInput, boundary: ParticipantObservationBoundaryRuntime, ) -> Mapping[str, str]: action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(history_events) relation, _ = participant_observation_effective_relation( - observation_index=projection.observation_order, + observation_index=history_order, boundary_address=projection.observation_boundary_address, boundary=boundary, action_attempts=action_attempts, @@ -345,21 +365,37 @@ def _surface_payload( "marking_definition_refs": list(projection.marking_definition_refs), "redaction_policy_ref": projection.redaction_policy_ref, "semantic_limitations": list(projection.semantic_limitations), + **( + {"projection_anchor": projection.projection_anchor.model_dump(mode="json")} + if projection.projection_anchor is not None + else {} + ), } def project_participant_decision_surface( runtime_model: RuntimeModel, *, + runtime_snapshot: RuntimeSnapshot | None = None, history_events: Sequence[ParticipantBehaviorHistoryEvent], projection: ParticipantDecisionSurfaceProjectionInput, exposure_resolvers: ParticipantExposureResolvers, ) -> ParticipantDecisionSurfaceModel: """Derive one surface from compiled meaning and one scoped history prefix.""" - _validate_projection_history(history_events, projection) + history_order = _validate_projection_history( + runtime_model, + history_events, + projection, + runtime_snapshot, + ) behavior, boundary = _resolve_projection_scope(runtime_model, projection) - relation = _projection_visibility_relation(history_events, projection, boundary) + relation = _projection_visibility_relation( + history_events, + history_order=history_order, + projection=projection, + boundary=boundary, + ) _validate_context_visibility( relation, refs=projection.visible_context_refs, diff --git a/implementations/python/packages/raes_processor/models/decision_surface_anchor.py b/implementations/python/packages/raes_processor/models/decision_surface_anchor.py new file mode 100644 index 000000000..988754f74 --- /dev/null +++ b/implementations/python/packages/raes_processor/models/decision_surface_anchor.py @@ -0,0 +1,306 @@ +"""Trusted lifecycle and behavior anchors for decision-surface projection.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING + +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceBehaviorAnchorModel, + ParticipantDecisionSurfaceEpisodeReadinessAnchorModel, + ParticipantDecisionSurfaceModel, + ParticipantDecisionSurfaceProjectionAnchorModel, +) +from raes_contracts.participant_behavior import ParticipantBehaviorHistoryEventType +from raes_contracts.participant_episode import ( + ParticipantEpisodeExecutionState, + ParticipantEpisodeHistoryEvent, + ParticipantEpisodeHistoryEventType, + ParticipantEpisodeStatus, + iter_participant_episode_snapshot_violations, +) +from raes_contracts.runtime_state import RuntimeSnapshot + +from .behavior_history_violations import iter_participant_behavior_history_violations +from .history_event import ParticipantBehaviorHistoryEvent +from .runtime_model import RuntimeModel + +if TYPE_CHECKING: + from .decision_surface import ParticipantDecisionSurfaceProjectionInput + + +def _stable_projection_event_ref(event_domain: str, payload: Mapping[str, object]) -> str: + try: + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + except (TypeError, ValueError) as exc: + raise ValueError("projection anchor events must have canonically serializable payloads") from exc + return f"participant-{event_domain}-event:sha256:{hashlib.sha256(canonical).hexdigest()}" + + +def _participant_episode_snapshot_context( + runtime_snapshot: RuntimeSnapshot, + participant_address: str, +) -> tuple[ParticipantEpisodeExecutionState, tuple[ParticipantEpisodeHistoryEvent, ...]]: + if not isinstance(runtime_snapshot, RuntimeSnapshot): + raise TypeError("runtime_snapshot must be a current trusted RuntimeSnapshot") + state_payload = runtime_snapshot.participant_episode_results.get(participant_address) + history_payloads = runtime_snapshot.participant_episode_history.get(participant_address) + if state_payload is None or history_payloads is None: + raise ValueError("projection anchor participant does not have current RUN-311 state and history") + violations = tuple( + iter_participant_episode_snapshot_violations( + {participant_address: state_payload}, + {participant_address: history_payloads}, + ) + ) + if violations: + raise ValueError(f"projection anchor RUN-311 snapshot is invalid: {violations[0][1]}") + state = ParticipantEpisodeExecutionState.from_payload(state_payload) + history = tuple(ParticipantEpisodeHistoryEvent.from_payload(payload) for payload in history_payloads) + if not history or history[0].event_type != ParticipantEpisodeHistoryEventType.EPISODE_INITIALIZED: + raise ValueError( + "projection anchors require the complete participant lifecycle history from episode_initialized" + ) + return state, history + + +def _current_episode_behavior_events( + runtime_snapshot: RuntimeSnapshot, + *, + participant_address: str, + episode_id: str, +) -> tuple[ParticipantBehaviorHistoryEvent, ...]: + payloads = runtime_snapshot.participant_behavior_history.get(participant_address, []) + events = tuple(ParticipantBehaviorHistoryEvent.from_payload(payload) for payload in payloads) + return tuple(event for event in events if event.episode_id == episode_id) + + +def resolve_participant_episode_readiness_anchor( + runtime_snapshot: RuntimeSnapshot, + *, + participant_address: str, + decision_surface_order: int, + evidence_refs: Sequence[str], + provenance_refs: Sequence[str], +) -> ParticipantDecisionSurfaceEpisodeReadinessAnchorModel: + """Resolve the current RUN-311 ``episode_running`` event as a trusted anchor.""" + + if decision_surface_order != 0: + raise ValueError("episode-readiness projection is always decision_surface_order zero") + state, history = _participant_episode_snapshot_context(runtime_snapshot, participant_address) + head = history[-1] + if ( + state.status != ParticipantEpisodeStatus.RUNNING + or head.event_type != ParticipantEpisodeHistoryEventType.EPISODE_RUNNING + ): + raise ValueError("episode-readiness projection requires the current lifecycle head to be episode_running") + if head.participant_address != state.participant_address or head.episode_id != state.episode_id: + raise ValueError("episode-readiness projection state and lifecycle head must identify the same episode") + if _current_episode_behavior_events( + runtime_snapshot, + participant_address=participant_address, + episode_id=state.episode_id, + ): + raise ValueError("episode-readiness projection requires empty current-episode behavior history") + event_ref = _stable_projection_event_ref("episode", head.to_payload()) + return ParticipantDecisionSurfaceEpisodeReadinessAnchorModel( + anchor_kind="episode_readiness", + participant_address=state.participant_address, + episode_id=state.episode_id, + decision_surface_order=decision_surface_order, + event_ref=event_ref, + anchor_order=len(history) - 1, + event_type=head.event_type.value, + episode_sequence_number=state.sequence_number, + evidence_refs=list(evidence_refs), + provenance_refs=list(dict.fromkeys((*provenance_refs, event_ref))), + ) + + +def _resolved_behavior_decision_surface_order( + events: Sequence[ParticipantBehaviorHistoryEvent], + behavior_history_order: int, +) -> int: + event = events[behavior_history_order] + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + raise ValueError("behavior decision surfaces must be anchored by a terminal observation_emitted event") + return sum( + candidate.event_type == ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED + for candidate in events[: behavior_history_order + 1] + ) + + +def resolve_participant_behavior_projection_anchor( + runtime_snapshot: RuntimeSnapshot, + *, + runtime_model: RuntimeModel, + participant_address: str, + episode_id: str, + decision_surface_order: int, + behavior_history_order: int, + evidence_refs: Sequence[str], + provenance_refs: Sequence[str], +) -> ParticipantDecisionSurfaceBehaviorAnchorModel: + """Resolve one exact event in the current participant/episode behavior prefix.""" + + state, episode_history = _participant_episode_snapshot_context(runtime_snapshot, participant_address) + if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != episode_id: + raise ValueError("behavior projection anchor must identify the current running participant episode") + events = _current_episode_behavior_events( + runtime_snapshot, + participant_address=participant_address, + episode_id=episode_id, + ) + if behavior_history_order < 0 or behavior_history_order >= len(events): + raise ValueError("behavior_history_order must identify an event in the current episode behavior history") + if behavior_history_order != len(events) - 1: + raise ValueError("behavior projection anchor must identify the exact current behavior-history prefix head") + payloads = [event.to_payload() for event in events] + violations = tuple( + iter_participant_behavior_history_violations( + payloads, + action_contracts=runtime_model.action_contracts, + observation_boundaries=runtime_model.observation_boundaries, + participant_episode_history=[event.to_payload() for event in episode_history], + expected_participant_address=participant_address, + ) + ) + if violations: + raise ValueError(f"projection anchor behavior history is invalid: {violations[0][1]}") + event = events[behavior_history_order] + resolved_decision_surface_order = _resolved_behavior_decision_surface_order(events, behavior_history_order) + if decision_surface_order != resolved_decision_surface_order: + raise ValueError( + "decision_surface_order must equal the number of completed observation_emitted events in the episode" + ) + event_ref = _stable_projection_event_ref("behavior", event.to_payload()) + return ParticipantDecisionSurfaceBehaviorAnchorModel( + anchor_kind="behavior_event", + participant_address=participant_address, + episode_id=episode_id, + decision_surface_order=decision_surface_order, + event_ref=event_ref, + anchor_order=behavior_history_order, + event_type=event.event_type.value, + action_instance_id=event.action_instance_id, + history_prefix_length=len(events), + evidence_refs=list(evidence_refs), + provenance_refs=list(dict.fromkeys((*provenance_refs, event_ref))), + ) + + +def _validate_projection_anchor_refs( + anchor: ParticipantDecisionSurfaceProjectionAnchorModel, + projection: ParticipantDecisionSurfaceProjectionInput, +) -> None: + mismatched = [ + name + for name, anchor_value, projection_value in ( + ("participant_address", anchor.participant_address, projection.participant_address), + ("episode_id", anchor.episode_id, projection.episode_id), + ("decision_surface_order", anchor.decision_surface_order, projection.observation_order), + ) + if anchor_value != projection_value + ] + if mismatched: + raise ValueError("projection anchor disagrees with projection input on: " + ", ".join(mismatched)) + if not set(anchor.evidence_refs).issubset(projection.evidence_refs): + raise ValueError("projection anchor evidence_refs must be carried by projection evidence_refs") + if not set(anchor.provenance_refs).issubset(projection.provenance_refs): + raise ValueError("projection anchor provenance_refs must be carried by projection provenance_refs") + + +def _validate_resolved_projection_anchor( + runtime_model: RuntimeModel, + runtime_snapshot: RuntimeSnapshot, + history_events: Sequence[ParticipantBehaviorHistoryEvent], + projection: ParticipantDecisionSurfaceProjectionInput, +) -> int: + anchor = projection.projection_anchor + if anchor is None: + raise ValueError("projection_anchor is required for trusted anchor validation") + _validate_projection_anchor_refs(anchor, projection) + if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorModel): + resolved = resolve_participant_episode_readiness_anchor( + runtime_snapshot, + participant_address=anchor.participant_address, + decision_surface_order=anchor.decision_surface_order, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + if resolved != anchor: + raise ValueError("episode-readiness projection anchor does not match the current trusted RuntimeSnapshot") + if history_events: + raise ValueError("episode-readiness projection requires empty current-episode behavior history") + return 0 + resolved = resolve_participant_behavior_projection_anchor( + runtime_snapshot, + runtime_model=runtime_model, + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + decision_surface_order=anchor.decision_surface_order, + behavior_history_order=anchor.anchor_order, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + if resolved != anchor: + raise ValueError("behavior projection anchor does not match the current trusted RuntimeSnapshot") + current_prefix = _current_episode_behavior_events( + runtime_snapshot, + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + ) + if tuple(history_events) != current_prefix: + raise ValueError("behavior projection requires the exact current behavior-history prefix") + return anchor.anchor_order + + +def validate_participant_decision_surface_projection_anchor( + runtime_snapshot: RuntimeSnapshot, + surface: ParticipantDecisionSurfaceModel, +) -> None: + """Reject an anchored surface that is no longer current at admission.""" + + anchor = surface.projection_anchor + if anchor is None: + return + if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorModel): + resolved = resolve_participant_episode_readiness_anchor( + runtime_snapshot, + participant_address=anchor.participant_address, + decision_surface_order=anchor.decision_surface_order, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + else: + state, _ = _participant_episode_snapshot_context(runtime_snapshot, anchor.participant_address) + if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != anchor.episode_id: + raise ValueError("behavior projection anchor is outside the current running episode") + events = _current_episode_behavior_events( + runtime_snapshot, + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + ) + if anchor.anchor_order != len(events) - 1: + raise ValueError("behavior projection anchor is not the current behavior-history prefix head") + event = events[anchor.anchor_order] + resolved_decision_surface_order = _resolved_behavior_decision_surface_order(events, anchor.anchor_order) + if anchor.decision_surface_order != resolved_decision_surface_order: + raise ValueError("behavior projection anchor has a stale or forged decision_surface_order") + resolved = ParticipantDecisionSurfaceBehaviorAnchorModel( + anchor_kind="behavior_event", + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + decision_surface_order=anchor.decision_surface_order, + event_ref=_stable_projection_event_ref("behavior", event.to_payload()), + anchor_order=anchor.anchor_order, + event_type=event.event_type.value, + action_instance_id=event.action_instance_id, + history_prefix_length=len(events), + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + if resolved != anchor: + raise ValueError("participant decision surface projection anchor is stale or does not resolve") diff --git a/implementations/python/packages/raes_runtime/participant_control.py b/implementations/python/packages/raes_runtime/participant_control.py index 9c8de66eb..7344e259a 100644 --- a/implementations/python/packages/raes_runtime/participant_control.py +++ b/implementations/python/packages/raes_runtime/participant_control.py @@ -18,7 +18,10 @@ ) from raes_contracts.planning import RuntimeDomain from raes_contracts.runtime_state import OperationReceipt -from raes_processor.models import ParticipantBehaviorRuntime +from raes_processor.models import ( + ParticipantBehaviorRuntime, + validate_participant_decision_surface_projection_anchor, +) from .control_plane_execution import execute_participant_action @@ -382,6 +385,9 @@ def admit_participant_decision_surface_selection( request_fingerprint=request_fingerprint, ) try: + if surface.projection_anchor is None: + raise ValueError("participant decision surface admission requires a current projection_anchor") + validate_participant_decision_surface_projection_anchor(self._snapshot, surface) request = bind_participant_decision_surface_selection( surface=surface, selection=selection, diff --git a/implementations/python/tests/test_sem_220_participant_decision_surface.py b/implementations/python/tests/test_sem_220_participant_decision_surface.py index da9b29d6e..264f523a7 100644 --- a/implementations/python/tests/test_sem_220_participant_decision_surface.py +++ b/implementations/python/tests/test_sem_220_participant_decision_surface.py @@ -10,8 +10,11 @@ import pytest from jsonschema import Draft202012Validator from pydantic import ValidationError +from raes_backend_stubs.stubs import create_stub_target from raes_contracts.contracts import ( ParticipantContextViewModel, + ParticipantDecisionSurfaceBehaviorAnchorModel, + ParticipantDecisionSurfaceEpisodeReadinessAnchorModel, ParticipantDecisionSurfaceModel, ParticipantDecisionSurfaceSelectionModel, ParticipantImplementationManifestModel, @@ -25,10 +28,12 @@ ParticipantValidatedActionSelection, bind_participant_decision_surface_selection, ) +from raes_contracts.runtime_state import RuntimeSnapshot from raes_processor.models import ( ParticipantActionContractRuntime, ParticipantBehaviorHistoryEvent, ParticipantBehaviorHistoryEventType, + ParticipantBehaviorRuntime, ParticipantBehaviorSpecificationRuntime, ParticipantDecisionSurfaceActionAssessment, ParticipantDecisionSurfaceProjectionInput, @@ -41,7 +46,10 @@ ParticipantToolAffordanceRuntime, RuntimeModel, project_participant_decision_surface, + resolve_participant_behavior_projection_anchor, + resolve_participant_episode_readiness_anchor, ) +from raes_runtime.control_plane import RuntimeControlPlane from raes_runtime.participant_control import ParticipantControlMixin REPO_ROOT = Path(__file__).resolve().parents[3] @@ -615,13 +623,68 @@ def test_decision_surface_schema_is_closed_discriminated_and_published() -> None schema = schema_bundle()["participant-decision-surface-v1"] assert schema["additionalProperties"] is False assert schema["properties"]["form"]["discriminator"]["propertyName"] == "surface_form" + anchor_schema = schema["properties"]["projection_anchor"]["anyOf"][0] + assert anchor_schema["discriminator"]["propertyName"] == "anchor_kind" + assert set(anchor_schema["discriminator"]["mapping"]) == { + "behavior_event", + "episode_readiness", + } assert {entry["id"] for entry in schema["x-aces-invariants"]} >= { "decision-surface-entry-reference-agreement", "decision-surface-presentation-not-lifecycle-evidence", + "decision-surface-projection-anchor-agreement", "decision-surface-sem226-item-exposure-agreement", } +def _surface_payload_with_readiness_anchor() -> dict[str, object]: + payload = _surface_payload() + event_ref = "participant-episode-event:sha256:" + "4" * 64 + payload["provenance_refs"].append(event_ref) # type: ignore[union-attr] + payload["projection_anchor"] = { + "anchor_kind": "episode_readiness", + "participant_address": PARTICIPANT, + "episode_id": EPISODE, + "decision_surface_order": 0, + "event_ref": event_ref, + "anchor_order": 1, + "event_type": "episode_running", + "episode_sequence_number": 0, + "evidence_refs": ["evidence.surface.red.order-0"], + "provenance_refs": [event_ref], + } + return payload + + +@pytest.mark.parametrize( + ("anchor_updates", "message"), + ( + ({"participant_address": "participant.behavior.blue-agent"}, "disagrees with the decision surface"), + ({"episode_id": "episode-other"}, "disagrees with the decision surface"), + ({"decision_surface_order": 1}, "disagrees with the decision surface"), + ({"evidence_refs": ["evidence.anchor.other"]}, "evidence_refs must be carried"), + ( + { + "provenance_refs": [ + "participant-episode-event:sha256:" + "4" * 64, + "provenance.anchor.other", + ] + }, + "provenance_refs must be carried", + ), + ), +) +def test_projection_anchor_must_agree_with_surface_scope_and_refs( + anchor_updates: dict[str, object], + message: str, +) -> None: + payload = _surface_payload_with_readiness_anchor() + payload["projection_anchor"].update(anchor_updates) # type: ignore[union-attr] + + with pytest.raises(ValidationError, match=message): + ParticipantDecisionSurfaceModel.model_validate(payload) + + def test_decision_surface_valid_and_invalid_fixtures_match_model_and_schema() -> None: validator = Draft202012Validator(schema_bundle()["participant-decision-surface-v1"]) valid_paths = sorted((FIXTURE_ROOT / "valid").glob("*.json")) @@ -632,6 +695,8 @@ def test_decision_surface_valid_and_invalid_fixtures_match_model_and_schema() -> payload = json.loads(path.read_text(encoding="utf-8")) validator.validate(payload) ParticipantDecisionSurfaceModel.model_validate(payload) + if path.stem == "human-candidate": + assert payload["projection_anchor"]["anchor_kind"] == "behavior_event" for path in invalid_paths: payload = json.loads(path.read_text(encoding="utf-8")) assert list(validator.iter_errors(payload)), path @@ -942,6 +1007,8 @@ def test_realization_kind_preserves_surface_semantic_refs( assert surface.action_entries[0].action_contract_address == SCAN assert surface.action_entries[0].selection_shape_ref == SCAN_SHAPE assert surface.form.selection_meaning_ref == "selection-meaning.candidate.v1" + assert surface.decision_control_mode == decision_control_mode + assert surface.implementation_selection_ref == implementation_selection_ref @pytest.mark.parametrize( @@ -1011,8 +1078,9 @@ def test_context_envelope_and_payload_must_agree(field_name: str, mismatched_val class _RecordingControl(ParticipantControlMixin): - def __init__(self) -> None: + def __init__(self, snapshot: RuntimeSnapshot | None = None) -> None: self._target = SimpleNamespace(participant_runtime=object()) + self._snapshot = snapshot or RuntimeSnapshot() self.admitted: ParticipantActionAdmissionRequest | None = None def _reject_diagnostics(self, **kwargs: object) -> str: @@ -1029,19 +1097,10 @@ def admit_participant_action( def test_open_ended_proposal_validates_before_existing_admission_path() -> None: - payload = _surface_payload(surface_form="open_ended_generation") - payload["action_entries"][0]["eligibility"] = "eligible" # type: ignore[index] - payload["action_entries"][0]["eligibility_reason_refs"] = [] # type: ignore[index] - surface = ParticipantDecisionSurfaceModel.model_validate(payload) - selection = ParticipantDecisionSurfaceSelectionModel( - surface_id=surface.surface_id, - observation_order=surface.observation_order, - action_contract_address=SCAN, - argument_shape_ref="selection-shapes.scan.v1", - proposal_ref="proposals.scan.1", - ) + snapshot, surface = _anchored_surface(surface_form="open_ended_generation") + selection = _surface_selection(surface) request = _admission_request() - control = _RecordingControl() + control = _RecordingControl(snapshot) resolver_calls: list[str] = [] apparatus_calls: list[tuple[str, str]] = [] @@ -1064,7 +1123,7 @@ def reject_shape(**kwargs: str) -> bool: ), ) assert rejected == "rejected" - assert resolver_calls == ["proposals.scan.1"] + assert resolver_calls == ["proposals.selection.1"] assert apparatus_calls == [(surface.implementation_selection_ref, surface.exposure_policy_ref)] assert control.admitted is None @@ -1154,22 +1213,13 @@ def resolve_different_coordinates(**kwargs: object) -> ParticipantValidatedActio def test_surface_apparatus_must_resolve_to_the_admission_selection() -> None: - payload = _surface_payload(surface_form="open_ended_generation") - payload["action_entries"][0]["eligibility"] = "eligible" # type: ignore[index] - payload["action_entries"][0]["eligibility_reason_refs"] = [] # type: ignore[index] - surface = ParticipantDecisionSurfaceModel.model_validate(payload) - selection = ParticipantDecisionSurfaceSelectionModel( - surface_id=surface.surface_id, - observation_order=surface.observation_order, - action_contract_address=SCAN, - argument_shape_ref="selection-shapes.scan.v1", - proposal_ref="proposals.scan.1", - ) + snapshot, surface = _anchored_surface(surface_form="open_ended_generation") + selection = _surface_selection(surface) request = _admission_request() mismatched_selection = request.implementation_selection.model_copy( update={"configuration_ref": "participant-configurations.other.v1"} ) - control = _RecordingControl() + control = _RecordingControl(snapshot) shape_calls: list[str] = [] rejected = control.admit_participant_decision_surface_selection( @@ -1210,3 +1260,472 @@ def test_presentation_cannot_be_encoded_as_selection_result_or_outcome() -> None payload["outcome"] = "succeeded" with pytest.raises(ValidationError, match="Extra inputs are not permitted"): ParticipantDecisionSurfaceModel.model_validate(payload) + + +def _compiled_participant_behavior() -> ParticipantBehaviorRuntime: + return ParticipantBehaviorRuntime( + address=PARTICIPANT, + name="red-agent", + spec={}, + participant_name="red-agent", + action_contract_addresses=(SCAN,), + observation_boundary_addresses=(BOUNDARY,), + ) + + +def _projection_with_anchor( + anchor: ParticipantDecisionSurfaceEpisodeReadinessAnchorModel | ParticipantDecisionSurfaceBehaviorAnchorModel, + *, + surface_form: str = "candidate_action_set", +) -> ParticipantDecisionSurfaceProjectionInput: + projection = _projection_input( + observation_order=anchor.decision_surface_order, + surface_form=surface_form, + ) + return replace( + projection, + observation_point=( + f"participant-episode-history:{anchor.anchor_order}" + if anchor.anchor_kind == "episode_readiness" + else f"behavior-history:{anchor.anchor_order}" + ), + projection_anchor=anchor, + evidence_refs=tuple(dict.fromkeys((*projection.evidence_refs, *anchor.evidence_refs))), + provenance_refs=tuple(dict.fromkeys((*projection.provenance_refs, *anchor.provenance_refs))), + ) + + +def _anchored_surface( + *, + surface_form: str, +) -> tuple[RuntimeSnapshot, ParticipantDecisionSurfaceModel]: + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + snapshot = control_plane.get_snapshot().snapshot + anchor = resolve_participant_episode_readiness_anchor( + snapshot, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=("evidence.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection_with_anchor(anchor, surface_form=surface_form) + surface = project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=snapshot, + history_events=(), + projection=projection, + exposure_resolvers=_projection_exposure_resolvers(projection), + ) + return snapshot, surface + + +def _context_for_surface(surface: ParticipantDecisionSurfaceModel) -> ParticipantContextViewModel: + return ParticipantContextViewModel.model_validate( + { + "view_id": surface.context_view_ref, + "participant_address": surface.participant_address, + "episode_id": surface.episode_id, + "generated_at": "2026-07-26T08:00:00Z", + "source_snapshot_ref": "snapshots.run-1.initial", + "view_ref": "views.decision-surface.v1", + "meaning_ref": "semantics.decision-surface.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": surface.observation_point, + "derived_from_refs": [ + "snapshots.run-1.initial", + surface.projection_anchor.event_ref, + ], + "source_layers": [ + { + "source_id": "episode-readiness", + "source_layer": "participant_episode_state", + "ref": "snapshots.run-1.initial", + "temporal_relation": "same_observation_point", + "observation_point": surface.observation_point, + "evidence_refs": surface.evidence_refs, + "provenance_refs": surface.provenance_refs, + } + ], + "transformation": { + "transformation_rule_ref": surface.projection_policy_ref, + "description": "Project the initial participant-local decision surface from episode readiness", + "input_source_ids": ["episode-readiness"], + "output_semantics_ref": "semantics.decision-surface.v1", + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.decision-surface.v1", + "backend_disclosure_refs": [], + "limitations": surface.semantic_limitations, + }, + "evidence_refs": surface.evidence_refs, + "provenance_refs": surface.provenance_refs, + "semantic_limitations": surface.semantic_limitations, + "derivation_basis_ref": surface.projection_policy_ref, + "payload_ref": surface.surface_id, + "visibility_projection_ref": surface.visibility_projection_ref, + "marking_definition_refs": surface.marking_definition_refs, + "redaction_policy_ref": surface.redaction_policy_ref, + } + ) + + +def test_public_initialize_projects_initial_context_and_surface_before_first_admission() -> None: + control_plane = RuntimeControlPlane(create_stub_target()) + receipt = control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + snapshot = control_plane.get_snapshot().snapshot + + assert receipt.accepted is True + assert snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] + anchor = resolve_participant_episode_readiness_anchor( + snapshot, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=("evidence.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection_with_anchor(anchor) + surface = project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=snapshot, + history_events=(), + projection=projection, + exposure_resolvers=_projection_exposure_resolvers(projection), + ) + context = _context_for_surface(surface) + + assert anchor.anchor_kind == "episode_readiness" + assert anchor.event_type == "episode_running" + assert anchor.decision_surface_order == 0 + assert anchor.anchor_order == 1 + assert surface.observation_order == 0 + assert surface.projection_anchor == anchor + assert surface.action_entries[0].action_contract_address == SCAN + validate_participant_decision_surface_context(surface, context) + + selection = _surface_selection(surface) + admission_request = _admission_request() + admitted = control_plane.admit_participant_decision_surface_selection( + _compiled_participant_behavior(), + surface=surface, + selection=selection, + admission_request=admission_request, + resolvers=ParticipantDecisionSurfaceBindingResolvers( + argument_shape=_resolved_selection, + apparatus=lambda **_: admission_request.implementation_selection, + ), + ) + admitted_snapshot = control_plane.get_snapshot().snapshot + + assert admitted.accepted is True + assert [event["event_type"] for event in admitted_snapshot.participant_behavior_history[PARTICIPANT]] == [ + "action_attempted", + "state_transition_recorded", + "observation_emitted", + ] + replayed = control_plane.admit_participant_decision_surface_selection( + _compiled_participant_behavior(), + surface=surface, + selection=selection, + admission_request=admission_request, + resolvers=ParticipantDecisionSurfaceBindingResolvers( + argument_shape=_resolved_selection, + apparatus=lambda **_: admission_request.implementation_selection, + ), + ) + replayed_snapshot = control_plane.get_snapshot().snapshot + assert replayed.accepted is False + assert len(replayed_snapshot.participant_behavior_history[PARTICIPANT]) == 3 + + +def test_control_plane_rejects_an_unanchored_surface_without_recording_behavior() -> None: + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + surface = _eligible_surface() + request = _admission_request() + + receipt = control_plane.admit_participant_decision_surface_selection( + _compiled_participant_behavior(), + surface=surface, + selection=_surface_selection(surface), + admission_request=request, + resolvers=ParticipantDecisionSurfaceBindingResolvers( + argument_shape=_resolved_selection, + apparatus=lambda **_: request.implementation_selection, + ), + ) + snapshot = control_plane.get_snapshot().snapshot + + assert receipt.accepted is False + assert snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] + + +def test_readiness_anchor_requires_current_complete_running_snapshot() -> None: + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + snapshot = control_plane.get_snapshot().snapshot + anchor = resolve_participant_episode_readiness_anchor( + snapshot, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=("evidence.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection_with_anchor(anchor) + + with pytest.raises(ValueError, match="current trusted RuntimeSnapshot"): + project_participant_decision_surface( + _runtime_model(), + history_events=(), + projection=projection, + exposure_resolvers=_projection_exposure_resolvers(projection), + ) + with pytest.raises(ValueError, match="empty current-episode behavior history"): + project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=snapshot, + history_events=_history(), + projection=projection, + exposure_resolvers=_projection_exposure_resolvers(projection), + ) + with pytest.raises(ValueError, match="evidence_refs"): + project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=snapshot, + history_events=(), + projection=replace(projection, evidence_refs=("evidence.surface.other",)), + exposure_resolvers=_projection_exposure_resolvers(projection), + ) + with pytest.raises(ValueError, match="decision_surface_order zero"): + resolve_participant_episode_readiness_anchor( + snapshot, + participant_address=PARTICIPANT, + decision_surface_order=1, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + + +def test_reset_invalidates_old_surface_and_creates_new_episode_order_zero_anchor() -> None: + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + first_snapshot = control_plane.get_snapshot().snapshot + first_anchor = resolve_participant_episode_readiness_anchor( + first_snapshot, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=("evidence.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + first_projection = _projection_with_anchor(first_anchor) + first_surface = project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=first_snapshot, + history_events=(), + projection=first_projection, + exposure_resolvers=_projection_exposure_resolvers(first_projection), + ) + + control_plane.reset_participant_episode(PARTICIPANT, episode_id="episode-2") + reset_snapshot = control_plane.get_snapshot().snapshot + stale_request = _admission_request() + stale = control_plane.admit_participant_decision_surface_selection( + _compiled_participant_behavior(), + surface=first_surface, + selection=_surface_selection(first_surface), + admission_request=stale_request, + resolvers=ParticipantDecisionSurfaceBindingResolvers( + argument_shape=_resolved_selection, + apparatus=lambda **_: stale_request.implementation_selection, + ), + ) + reset_anchor = resolve_participant_episode_readiness_anchor( + reset_snapshot, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=("evidence.episode-running.reset",), + provenance_refs=("provenance.runtime-control-plane",), + ) + + assert stale.accepted is False + assert reset_anchor.episode_id == "episode-2" + assert reset_anchor.episode_sequence_number == 1 + assert reset_anchor.decision_surface_order == 0 + assert reset_snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] + + +def test_restart_creates_a_new_episode_order_zero_anchor() -> None: + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + control_plane.terminate_participant_episode(PARTICIPANT) + control_plane.restart_participant_episode(PARTICIPANT, episode_id="episode-restarted") + snapshot = control_plane.get_snapshot().snapshot + + anchor = resolve_participant_episode_readiness_anchor( + snapshot, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=("evidence.episode-running.restart",), + provenance_refs=("provenance.runtime-control-plane",), + ) + + assert anchor.episode_id == "episode-restarted" + assert anchor.episode_sequence_number == 1 + assert anchor.decision_surface_order == 0 + assert snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] + + +def test_readiness_anchor_rejects_incomplete_forged_and_cross_scope_inputs() -> None: + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + snapshot = control_plane.get_snapshot().snapshot + anchor = resolve_participant_episode_readiness_anchor( + snapshot, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=("evidence.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + + incomplete = replace( + snapshot, + participant_episode_history={ + PARTICIPANT: snapshot.participant_episode_history[PARTICIPANT][-1:], + }, + ) + with pytest.raises(ValueError, match="complete participant lifecycle history"): + resolve_participant_episode_readiness_anchor( + incomplete, + participant_address=PARTICIPANT, + decision_surface_order=0, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + + forged_ref = "participant-episode-event:sha256:" + "0" * 64 + forged = anchor.model_copy( + update={ + "event_ref": forged_ref, + "provenance_refs": [*anchor.provenance_refs, forged_ref], + } + ) + forged_projection = _projection_with_anchor(forged) + with pytest.raises(ValueError, match="current trusted RuntimeSnapshot"): + project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=snapshot, + history_events=(), + projection=forged_projection, + exposure_resolvers=_projection_exposure_resolvers(forged_projection), + ) + + cross_episode = anchor.model_copy(update={"episode_id": "episode-other"}) + with pytest.raises(ValueError, match="episode_id"): + project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=snapshot, + history_events=(), + projection=_projection_with_anchor(cross_episode), + exposure_resolvers=_projection_exposure_resolvers(_projection_with_anchor(cross_episode)), + ) + + cross_participant = anchor.model_copy(update={"participant_address": "participant.behavior.blue-agent"}) + with pytest.raises(ValueError, match="participant_address"): + project_participant_decision_surface( + _runtime_model(), + runtime_snapshot=snapshot, + history_events=(), + projection=_projection_with_anchor(cross_participant), + exposure_resolvers=_projection_exposure_resolvers(_projection_with_anchor(cross_participant)), + ) + + +def test_behavior_anchor_resolves_exact_current_episode_history_prefix() -> None: + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + control_plane.admit_participant_action(_compiled_participant_behavior(), _admission_request()) + snapshot = control_plane.get_snapshot().snapshot + runtime_model = _runtime_model() + boundary = runtime_model.observation_boundaries[BOUNDARY] + runtime_model = replace( + runtime_model, + observation_boundaries={ + BOUNDARY: replace( + boundary, + view_transitions=(), + view_relation_timeline=(boundary.view_relation_timeline[0],), + ) + }, + ) + history = tuple( + ParticipantBehaviorHistoryEvent.from_payload(payload) + for payload in snapshot.participant_behavior_history[PARTICIPANT] + ) + anchor = resolve_participant_behavior_projection_anchor( + snapshot, + runtime_model=runtime_model, + participant_address=PARTICIPANT, + episode_id=EPISODE, + decision_surface_order=1, + behavior_history_order=2, + evidence_refs=("evidence.scan-result",), + provenance_refs=("provenance.runtime-control-plane",), + ) + + assert anchor.anchor_kind == "behavior_event" + assert anchor.decision_surface_order == 1 + assert anchor.anchor_order == 2 + assert anchor.history_prefix_length == 3 + assert anchor.event_type == "observation_emitted" + + projection = _projection_with_anchor(anchor) + surface = project_participant_decision_surface( + runtime_model, + runtime_snapshot=snapshot, + history_events=history, + projection=projection, + exposure_resolvers=_projection_exposure_resolvers(projection), + ) + assert surface.observation_order == 1 + assert surface.projection_anchor.anchor_order == 2 + with pytest.raises(ValueError, match="number of completed observation_emitted"): + resolve_participant_behavior_projection_anchor( + snapshot, + runtime_model=runtime_model, + participant_address=PARTICIPANT, + episode_id=EPISODE, + decision_surface_order=2, + behavior_history_order=2, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + forged_anchor = anchor.model_copy(update={"decision_surface_order": 2}) + forged_surface = surface.model_copy( + update={ + "observation_order": 2, + "projection_anchor": forged_anchor, + } + ) + request = _admission_request() + rejected = control_plane.admit_participant_decision_surface_selection( + _compiled_participant_behavior(), + surface=forged_surface, + selection=_surface_selection(surface).model_copy(update={"observation_order": 2}), + admission_request=request, + resolvers=ParticipantDecisionSurfaceBindingResolvers( + argument_shape=_resolved_selection, + apparatus=lambda **_: request.implementation_selection, + ), + ) + assert rejected.accepted is False + assert len(control_plane.get_snapshot().snapshot.participant_behavior_history[PARTICIPANT]) == 3 + with pytest.raises(ValueError, match="exact current behavior-history prefix"): + project_participant_decision_surface( + runtime_model, + runtime_snapshot=snapshot, + history_events=history[:-1], + projection=projection, + exposure_resolvers=_projection_exposure_resolvers(projection), + ) diff --git a/specs/formal/participant-semantics/README.md b/specs/formal/participant-semantics/README.md index 727675961..c1cce208b 100644 --- a/specs/formal/participant-semantics/README.md +++ b/specs/formal/participant-semantics/README.md @@ -1439,6 +1439,40 @@ Every surface has: - evidence/provenance, markings, redaction, limitations, and weakening; and - the event/order/evidence anchor from which the surface was derived. +The event/order/evidence anchor is a closed tagged value, not a mixed history. +An `episode_readiness` anchor resolves the current RUN-311 `episode_running` +event from the trusted runtime snapshot and complete participant lifecycle +history. It grounds compiled `V_p,0`, the initial context, and `D(p,e,0)` while +the new episode's behavior history remains empty. A `behavior_event` anchor +resolves one exact action-linked event and the complete participant/episode +behavior-history prefix used by the existing effective-view-relation +algorithm. + +The anchor keeps three order domains explicit: + +- RUN-311 `sequence_number` identifies the episode generation only; +- `decision_surface_order`, carried as the surface `observation_order`, numbers + `D(p,e,0)`, `D(p,e,1)`, and subsequent surfaces within that episode; and +- `anchor_order` identifies the referenced event in its tagged lifecycle or + behavior history without changing the meaning of `observation_order`. + +The readiness resolver derives decision-surface order zero. A later surface is +anchored by the exact current terminal `observation_emitted` event, and its +decision-surface order equals the number of completed observation events in the +episode. The value is resolved from runtime history rather than supplied as +caller-authored metadata. + +For a new episode, `episode_initialized`, `episode_reset`, or +`episode_restarted` precedes `episode_running`; readiness then precedes the +initial context and surface. Proposal and selection follow the surface but do +not create participant behavior. Admission creates the first +`action_attempted`; its state transition and terminal observation precede the +next behavior-anchored surface. Reset and restart create a new episode id and +restart `decision_surface_order` at zero. Projection and admission both +re-resolve an anchor against current runtime authority, so a standalone, +truncated, previous-episode, terminated, behavior-superseded, or absent runtime +admission anchor fails closed. + The three surface forms have distinct selection meaning: - **Open-ended generation:** the participant implementation may propose an @@ -1566,7 +1600,7 @@ must preserve or strengthen its rows. | SEM-219 E: constraints fail closed | affordance action refs plus unchanged SEM-211 preconditions/failure classes | semantic validation and existing planner/admission/result gates | complete action constraints remain reachable through the compiled action address | binding copies, drops, or overrides exhausted/unknown constraints | I4, I7 / #294 | | SEM-219 F: support is apparatus metadata | authored affordance IR remains separate from manifest/selection support | absence-preserving compilation plus existing apparatus validation | support can be joined later without changing authored meaning | installed content or backend support creates an affordance grant | I11, I12 / #294 | | SEM-219 G: side effects and observations are explicit | affordance observation addresses plus action effects/evidence expectations | boundary classification, compiler IR, existing result/snapshot/conformance gates | tool output remains governed by referenced observation/effect contracts | tool output lacks a view rule or leaks hidden truth | I5, I13 / #294 | -| SEM-220 A: surface has participant/episode/order identity | `ParticipantContextViewModel` envelope plus typed `D(p,e,o)` payload/ref | retrieval and context-view validation | surface resolves to one participant, episode, and observation point | cumulative/global context substitutes for participant-local state | I1, I3, I15 / #295 | +| SEM-220 A: surface has participant/episode/order identity | `ParticipantContextViewModel` envelope, typed `D(p,e,o)` payload/ref, and tagged episode-readiness or behavior-event projection anchor | trusted runtime-snapshot/history resolution, projection, admission-time freshness validation, and context-view validation | `episode_running` grounds `V_p,0` and `D(p,e,0)` without behavior; later surfaces resolve one exact behavior prefix | cumulative/global context, a standalone lifecycle event, or a stale prior-episode surface substitutes for current participant-local state | I1, I3, I15 / #295, #909 | | SEM-220 B: candidate membership is not eligibility | action-entry contract ref plus explicit SEM-211 eligibility state/reason refs | surface derivation followed by independent admission | visible candidate is marked ineligible with a typed reason | every presented candidate is implicitly executable | I4 / #295 | | SEM-220 C: open-ended proposals bind before admission | compiled `ParticipantActionContractRuntime.argument_shape_ref`, `ParticipantValidatedActionSelection`, and SEM-211 admission helper | proposal resolution, concrete argument validation/normalization, immutable carrier binding, then runtime admission | generated proposal resolves and validates before an attempt | free-form generation bypasses applicability or invents backend-local meaning | I4, I11 / #295, #303 | | SEM-220 D: constrained forms preserve mapping meaning | `ParticipantActionArgumentDefinition`, canonical compiled shape identity, and explicit default/normalization/omission/loss disclosure | closed authoring validation, compiler mapping, `resolve_participant_action_arguments()`, and conformance comparison | form values map deterministically to validated action arguments | omitted/defaulted field changes meaning without disclosure | I12, I14, I16 / #295, #303 | From 816e03f13dfd3c09ea72af659ceb7d8bfa623ea0 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 17:08:07 +0200 Subject: [PATCH 10/55] Clarify RAES naming boundaries --- .../issue-908-aces-naming-audit-preflight.md | 206 ++++++++++++++++++ docs/migration/raes-rename.md | 9 +- .../_techvault_native_ops.py | 6 +- .../raes_backend_libvirt/drivers/libvirt.py | 16 +- .../guest_certified_driver.py | 4 +- .../techvault_lifecycle.py | 8 +- .../raes_backend_libvirt/techvault_matrix.py | 6 +- .../raes_backend_libvirt/techvault_native.py | 4 +- .../_cross_backend_corpus_backend_runs.py | 8 +- .../tests/test_libvirt_backend_driver.py | 11 +- 10 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 docs/decisions/issue-908-aces-naming-audit-preflight.md diff --git a/docs/decisions/issue-908-aces-naming-audit-preflight.md b/docs/decisions/issue-908-aces-naming-audit-preflight.md new file mode 100644 index 000000000..526dcdeb5 --- /dev/null +++ b/docs/decisions/issue-908-aces-naming-audit-preflight.md @@ -0,0 +1,206 @@ +# Issue 908 ACES Naming Audit Preflight + +Date: 2026-07-26 + +Issue: #908. Requirement: none. The GitHub issue is the authoritative audit +contract. + +This note classifies the remaining ACES-bearing surfaces before implementation. +It does not authorize a repository-wide replacement. The issue supplies an +inventory and asks for decisions on contract and runtime identities; it does +not provide acceptance criteria for an in-place breaking migration. + +## Architecture Decision + +ADR-093 remains the rename authority. Issue #908 completes its classification, +not the earlier Python import cut: + +| Surface | Disposition for #908 | Owning authority | +|---|---|---| +| Current non-contract prose, comments, and private symbol names | May use RAES when meaning and emitted values do not change | Owning docs/package/tool | +| `https://aces.dev/schemas/...` | Retain for every current schema lineage; the namespace is not retired by this issue | ADR-061, published schemas, schema-publication records | +| Contract ids, profile ids, `$defs` names, annotation keywords, wire discriminators, and wire keys | Retain in current contract versions; do not rename in place | Normative schemas/fixtures/specs and closed DTOs | +| `aces-reference-processor` and other apparatus identities | Retain until an apparatus-identity migration names producer, consumer, version, and conformance evidence | Manifest owners and manifest authority | +| `aces.lock.json`, `aces-trust.yaml`, OCI media types/annotations, evidence schema ids, auth headers, host labels, kernel parameters, and resource prefixes | Retain; each is an external config, artifact, security, guest, or host-ownership protocol | Owning registry, runtime, operations, or backend boundary | +| Participant `aces.*` values | Retain as wire `schema_name` and status-mapping vocabulary; they are not message-broker topics | Participant-runtime spec, DTOs, schemas, and fixtures | +| `ACES_REQUIREMENT_UID` and `ACES_REAL_LIBVIRT_URI` | Retain as the only actual environment-variable surfaces in the audit | Requirement governance and opt-in libvirt certification | +| `ACES_NATIVE` and `ACES_RELATIVE_TO_SOURCE` | Retain as Python enum member names for provenance wire values; they are not environment variables | Provenance DTO and lineage ledger | +| Accepted ADRs, changelog history, provenance ledger, pinned research/evidence, citations, and external URLs | Preserve as historical or immutable evidence | ADR-059, ADR-080, and the owning record | + +The current schema URI namespace is an identifier, not a request to resolve +schemas over the network. Repository validation uses local published schemas +and `schema_bundle()`; no HTTP resolver is present. A future RAES URI namespace +is eligible only after its target is owned and publishable, old identities +remain resolvable or are explicitly deprecated, and old and new lineages can +coexist. A global base-URL replacement is not a valid cutover. + +All current schema-publication entries are `draft`, but `draft` does not make +consumer-visible identity changes non-breaking. It permits governed evolution +under ADR-061; it does not waive migration evidence, fixture coordination, or +the ecosystem compatibility rules. + +No ADR amendment is required. ADR-093 already separates project identity from +contract identity, and `docs/migration/raes-rename.md` is the surface map for +the retained decisions above. + +## Canonical Incumbents + +Implementation must reuse these owners rather than introduce a rename layer: + +- Authority and publication: ADR-009, ADR-019, + `specs/authority/authority-boundary.yaml`, ADR-061, + `contracts/schema-publication-manifest.json`, + `contracts/schema-publication/entries/`, + `tools/check_schema_publication.py`, `schema_bundle()`, and + `tools/check_generated_schemas.py`. +- Compatibility and lifecycle: ADR-075, + `specs/evolution/versioning-deprecation-and-migration.md`, + `specs/evolution/deprecation-records.yaml`, + `tools/check_deprecation_lifecycle.py`, and + `docs/migration/raes-rename.md`. +- Contract shape and validation: `raes_contracts._base.ContractModel`, + `raes._base.SDLModel`, the existing Pydantic models, local Draft 2020-12 + JSON Schema validation, normative fixtures, `tools/check_json_artifacts.py`, + and `raes_contracts.corpus`. +- Semantic annotations: the existing `schema_constraints.py`, + `schema_invariants.py`, and `contracts/schemas/profiles/` + `aces-semantic-invariants-v1.json` profile. Internal helper names may change; + the published `x-aces-*` protocol may not change accidentally with them. +- Module/config security: `raes.module_registry` constants and loaders, + `yaml.safe_load`, closed `TrustPolicy`/`Lockfile` models, digest/signature + checks, capped OCI reads, safe archive extraction, and `raes_cli.sdl`. +- Apparatus and runtime artifacts: the existing processor/backend manifest + renderers and closed models; evidence builders and their shared redaction + gate; participant-runtime DTOs/spec/fixtures; and the current artifact + validators. Do not add a second identifier catalog. +- Host ownership: `provider_resource_name()`, the libvirt fixed UUID namespace, + OCI ownership labels and inspection join, libvirt ownership checks, + structured XML builders, fixed argv execution, bounded timeouts, and + ownership-confined teardown. +- Runtime security and persistence: + `ControlPlaneSecurityConfig.strict_defaults()`, `ControlPlaneRole`, + bearer/trusted-proxy authentication, `request_size_guard_response()`, + request fingerprints/idempotency, append-only audit records, redacted + unexpected-error responses, `RuntimeSnapshot`, and + `LocalControlPlaneStore`. +- Errors and diagnostics: existing Pydantic `ValidationError`, + `SDLParseError`/`SDLValidationError`, Typer `BadParameter`/`Exit`, + package-local `Diagnostic`, MCP structured JSON, and bounded HTTP JSON + envelopes. There is no rename-specific exception hierarchy. +- Workflow: ADR-014, `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, + `.github/workflows/ci.yml`, `tools/check_repo_policy.py`, + `tools/check_requirement_governance.py`, `tools/verify_all.py`, gitleaks, + private-key detection, ADR pins, authority checks, publication checks, and + the owning focused tests. + +## Cross-Cutting Security And Whole-Path Gates + +- **Published contract ingress:** edit the normative schema/fixture/spec owner + first. JSON must remain locally parseable, closed DTOs keep + `extra="forbid"`, schema annotations keep their existing semantic validator, + publication records carry the current canonical hash and `last_change`, and + `schema_bundle()` remains byte-identical. Do not fetch remote `$ref` values or + make DNS/HTTP availability part of validation. +- **Auth surface:** `x-aces-client-verified` and + `x-aces-client-identity` are trusted-proxy protocol fields, not branding. + This issue leaves them unchanged. Any future dual-name adapter belongs in + `ControlPlaneSecurityConfig`, which already parameterizes the header names; + it must remain disabled under strict defaults, accept values only behind the + trusted proxy boundary, reject conflicting old/new headers, enforce + request-size limits, and audit denials. Role checks remain mandatory. The + current bearer-token branch returns before the proxy branch's target check; + do not claim bearer target scoping or build a header migration on that gap + without first unifying the post-authentication target check. +- **Secret surface:** no rename needs a secret, token, credential, private key, + environment dump, raw backend object, or real payload. Do not read secret + files or copy real values into fixtures, docs, logs, schema examples, or + migration records. Module trust keeps signature and digest validation. + Evidence keeps the shared redaction gate. +- **Environment/config shape:** `ACES_REQUIREMENT_UID` remains an external + Ground Control/workflow input. `ACES_REAL_LIBVIRT_URI` remains an opt-in test + input and must never carry credentials. The real-libvirt tests currently read + it directly; a naming cleanup must not add another alias or call path that + widens that pre-existing validation gap. Do not add dotenv or a generic + environment binder. +- **OS/host exposure:** OCI labels and libvirt names are passed by fixed argv; + kernel parameters are visible in the guest and host process boundary; guest + paths and evidence ids are persisted. The challenge kernel parameter must + remain a non-secret correlation value. Preserve the fixed libvirt UUID + namespace even if private Python symbols are renamed: changing its value + would make existing owned objects look foreign, create duplicates, or block + safe teardown. Do not use shell interpolation or place secrets in argv. +- **Error envelopes and logging:** a naming failure must flow through the + owning parser/DTO/CLI/diagnostic/HTTP envelope. Do not include raw input, + environment values, host paths, native stdout/stderr, schema bodies, or + tracebacks. Existing audit and logs are evidence, not a second migration + ledger; do not rewrite old records or add a rename logger. +- **Persistence:** lockfiles, trust policies, contract fixtures, evidence + artifacts, runtime snapshot histories, append-only audit data, and native + ownership markers may contain retained identifiers. Do not silently rewrite + them, normalize them on read, or add a migration database/service. A future + versioned reader adapter stays at the owning boundary and must preserve the + source artifact. +- **Workflow:** the branch is requirement-free. Do not invent a requirement UID + or bypass repository policy. Reuse nox and the existing owner-specific gates; + do not add a global “zero ACES tokens” rule because retained contracts and + historical evidence make zero both impossible and incorrect. + +## Extensibility Seams + +The parameter is the **surface class and version/lineage**, not a global +old-name/new-name pair. + +- A future schema namespace or contract-id migration extends the existing + per-contract `schema_bundle()` metadata path and schema-publication entry so + old and new lineages coexist. `_schema_id_for_contract_id()` is the existing + implementation seam; it must become lineage-aware rather than substitute one + global base URL. +- A future auth-header variation uses the already parameterized + `ControlPlaneSecurityConfig` fields. +- A future module artifact variation stays behind `LOCKFILE_NAME`, + `TRUST_POLICY_NAME`, media-type constants, and their existing loaders. +- A future provider naming variation uses the existing `name_prefix` input and + `provider_resource_name()` while preserving the separate ownership UUID + invariant. +- A future deprecation adds its surface class/record to the existing ecosystem + lifecycle policy and owning checker. It does not create a universal alias + registry, runtime lookup service, endpoint, store, or exception tree. + +## Gotchas And Anti-Patterns + +Avoid: + +- blind replacement, substring matching, or treating audit counts as a design; +- changing normative schemas only in Python or hand-editing generated copies; +- equating package names, schema `$id`, contract ids, `$defs` class names, + annotation keywords, apparatus ids, artifact ids, auth headers, config keys, + filenames, OCI labels, resource names, and prose; +- treating participant `schema_name` values as broker topics or introducing an + event bus/topic registry; +- treating provenance enum members as environment variables; +- renaming `_aces_uuid` while also changing its fixed namespace value; +- changing OCI labels or provider prefixes without preserving discovery, + ownership joins, rollback, and teardown of already-created resources; +- accepting both old and new config/header values with last-one-wins behavior; +- renaming a valid fixture string merely because the fixture is under + `contracts/fixtures/`; +- rewriting accepted ADRs, `CHANGELOG.md`, the normative lineage ledger, + pinned research/evidence, external URLs, or negative legacy-import tests; +- weakening closed-model, signature, digest, redaction, auth, request-size, + path-containment, schema-publication, ADR-pin, gitleaks, or private-key gates; +- adding duplicate schemas, validators, compatibility tables, workflow logic, + exception hierarchies, logs, stores, or migration services. + +## Non-Goals And Boundaries + +- Implementing issue #908 or reducing the occurrence count in this preflight. +- Retiring `aces.dev`, selecting an unverified RAES domain, or adding network + schema resolution. +- Renaming current contract lineages, wire fields, apparatus identities, + runtime artifacts, auth/config protocols, host ownership markers, or + persisted records. +- Changing SDL, contract, runtime, security, observability, persistence, + backend, or conformance semantics. +- Adding compatibility aliases for the already removed Python, CLI, or MCP + surfaces. +- Rewriting historical records solely to erase the former project name. diff --git a/docs/migration/raes-rename.md b/docs/migration/raes-rename.md index 299e29200..ac81cae5c 100644 --- a/docs/migration/raes-rename.md +++ b/docs/migration/raes-rename.md @@ -46,8 +46,13 @@ surfaces: | Agent guidance profile id | `aces-agent-guidance` | `raes-agent-guidance` | `specs/agent-guidance/agent-guidance.yaml` | Migrated canonical profile id | `tools/check_agent_guidance.py` and MCP guidance tests | | Intended-use scope string | `aces-delivery-capability` | `raes-delivery-capability` | `raes_mcp.tools.completeness` | Migrated emitted scope; `legacy_scope` removed | MCP intended-use tests | | Runtime OpenAPI title | ACES Runtime Control Plane | RAES Runtime Control Plane | `raes_runtime.control_plane_api` | Migrated emitted title | Version-classification tests | -| HTTP headers and config keys | `x-aces-*`, `ACES_REQUIREMENT_UID` | Retained external/workflow keys | Runtime/security and workflow owners | Outside the Python namespace cut in issue #884 | Runtime security and repo-policy gates | -| Published schemas and wire ids | `io.aces.*`, `aces-*`, contract discriminators | Retained governed contract identifiers | `contracts/` and ADR-061 schema publication | Outside the Python namespace cut in issue #884 | Contract and schema-publication checks | +| HTTP security headers | `x-aces-client-verified`, `x-aces-client-identity` | Retained security-protocol keys | `ControlPlaneSecurityConfig` and the trusted-proxy boundary | Not aliases for project prose; a future migration must preserve fail-closed auth and reject conflicting old/new values | Runtime auth, role, request-size, denial-audit, and redacted-error tests | +| Environment and external workflow keys | `ACES_REQUIREMENT_UID`, `ACES_REAL_LIBVIRT_URI` | Retained external/workflow keys | Requirement governance and opt-in real-libvirt certification | Outside the Python namespace cut; uppercase provenance enum members such as `ACES_NATIVE` are not environment variables | Repo-policy and opt-in integration-test gates | +| Published schema URI namespace | `https://aces.dev/schemas/...` | Retained for every current schema lineage | `contracts/schemas/`, schema-publication records, and `schema_bundle()` | Not retired by #908; a future namespace must coexist with old identities and prove target ownership/redirect behavior | Contract, generated-schema parity, and schema-publication checks | +| Published schemas and wire ids | `io.aces.*`, `x-aces-*`, `aces-*`, contract discriminators and wire field names | Retained governed contract identifiers | `contracts/`, closed DTOs, and ADR-061 schema publication | Current lineages are not renamed in place; a replacement requires its owning versioned contract migration | Contract models, fixtures, JSON Schema validation, and publication checks | +| Module registry artifacts | `aces.lock.json`, `aces-trust.yaml`, `.aces/module-cache`, `application/vnd.aces.*`, `io.aces.module.id` | Retained module/config protocol | `raes.module_registry` | User-authored, persisted, signed, or registry-visible values; not prose cleanup | Closed SDL models, safe YAML/JSON parsing, digest/signature, registry, archive, and CLI tests | +| Runtime and evidence artifact ids | `aces.libvirt.*`, `aces.cross-backend-*`, `aces.participant_runtime.*`, `aces.lifecycle.*` and related values | Retained artifact/wire vocabularies | Owning evidence builders, participant-runtime spec, DTOs, and fixtures | These are schema/discriminator/status values, not a broker topic registry; migrate only with the owning artifact lineage | Artifact validators, redaction gates, DTO/fixture tests, and persistence round trips | +| Host/runtime ownership names | `aces.workspace`, `aces.address`, `aces.*` kernel parameters, libvirt/OCI name prefixes, fixed UUID namespace, guest paths | Retained host ownership and guest-protocol values | OCI/libvirt drivers and guest appliance | A cosmetic rename can orphan resources or defeat ownership proof; requires a separately bounded host migration | Fixed-argv, ownership-conflict, teardown, guest-probe, and real-daemon tests | | Processor/backend identities | `aces-reference-processor`, package-specific manifest ids | Retained apparatus identities | Manifest owners | Outside the Python namespace cut in issue #884 | Manifest and conformance tests | | Accepted ADRs, changelog, research snapshots | Historical ACES references | Retained history | Historical record owners | Not rewritten solely to erase the old name | ADR immutability and docs checks | diff --git a/implementations/python/packages/raes_backend_libvirt/_techvault_native_ops.py b/implementations/python/packages/raes_backend_libvirt/_techvault_native_ops.py index f0425191d..f47ad0dd0 100644 --- a/implementations/python/packages/raes_backend_libvirt/_techvault_native_ops.py +++ b/implementations/python/packages/raes_backend_libvirt/_techvault_native_ops.py @@ -15,7 +15,7 @@ from raes_contracts.diagnostics import Diagnostic, Severity -from .drivers.libvirt import _aces_uuid, _error_code, _existing_uuid +from .drivers.libvirt import _error_code, _existing_uuid, _raes_uuid from .techvault_lifecycle import ( NativeOwnershipConflict as _OwnershipConflict, ) @@ -54,13 +54,13 @@ def _ensure_name_available(connection: object, method_name: str, name: str, addr if _error_code(exc) in {42, 43}: return raise - if _existing_uuid(native) != _aces_uuid(address): + if _existing_uuid(native) != _raes_uuid(address): raise _OwnershipConflict(address) raise RuntimeError("owned native object already exists for CREATE") def _artifact_token(address: str) -> str: - return _aces_uuid(address).replace("-", "") + return _raes_uuid(address).replace("-", "") _MESSAGES = { diff --git a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py index 89642102c..849922bbd 100644 --- a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py +++ b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py @@ -37,7 +37,7 @@ # 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. -_ACES_UUID_NAMESPACE = uuid.UUID("ace50000-0000-5000-8000-000000000001") +_RAES_UUID_NAMESPACE = uuid.UUID("ace50000-0000-5000-8000-000000000001") # 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 @@ -89,14 +89,14 @@ def _is_absence_error(exc: BaseException) -> bool: return _error_code(exc) in _ABSENCE_ERROR_CODES -def _aces_uuid(address: str) -> str: - return str(uuid.uuid5(_ACES_UUID_NAMESPACE, address)) +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(_ACES_UUID_NAMESPACE, f"nwfilter:{address}")) + return str(uuid.uuid5(_RAES_UUID_NAMESPACE, f"nwfilter:{address}")) class _NativeResource(Protocol): @@ -226,7 +226,7 @@ def _realize_network(self, connection: object, spec: NetworkSpec, created: list[ pre_existing = self._converge_existing(connection, "networkLookupByName", name, spec.address) if not pre_existing: created.append(spec.address) - network_xml = _network_xml(spec, name, _aces_uuid(spec.address)) + network_xml = _network_xml(spec, name, _raes_uuid(spec.address)) native = _call_libvirt(connection, "networkDefineXML", network_xml) native.create() except _OwnershipConflict: @@ -255,7 +255,7 @@ def _realize_domain(self, connection: object, spec: DomainSpec, created: list[st created.append(spec.address) seed_path = self._build_seed(spec, name) filter_name = self._define_nwfilter(connection, spec, name) - xml = _domain_xml(spec, name, network_names, seed_path, _aces_uuid(spec.address), filter_name) + xml = _domain_xml(spec, name, network_names, seed_path, _raes_uuid(spec.address), filter_name) native = _call_libvirt(connection, "defineXML", xml) native.create() except _OwnershipConflict: @@ -413,7 +413,7 @@ def _converge_existing(connection: object, lookup_method: str, name: str, addres native = _lookup(connection, lookup_method, name) if native is None: return False - if _existing_uuid(native) != _aces_uuid(address): + if _existing_uuid(native) != _raes_uuid(address): raise _OwnershipConflict(name) _stop_native(native) cast(_NativeResource, native).undefine() @@ -445,7 +445,7 @@ def _destroy_one(self, connection: object, lookup_method: str, address: str) -> # A present object is torn down only when its UUID proves RAES ownership # (the same invariant as convergence), never a foreign name collision. if native is not None: - if _existing_uuid(native) != _aces_uuid(address): + if _existing_uuid(native) != _raes_uuid(address): raise _OwnershipConflict(address) try: _stop_native(native) diff --git a/implementations/python/packages/raes_backend_libvirt/guest_certified_driver.py b/implementations/python/packages/raes_backend_libvirt/guest_certified_driver.py index 06c310647..a9abe5e12 100644 --- a/implementations/python/packages/raes_backend_libvirt/guest_certified_driver.py +++ b/implementations/python/packages/raes_backend_libvirt/guest_certified_driver.py @@ -23,7 +23,7 @@ from raes_contracts.diagnostics import Diagnostic from .driver import DomainSpec, NetworkSpec, RealizationObservation -from .drivers.libvirt import _aces_uuid +from .drivers.libvirt import _raes_uuid from .guest_appliance import GuestObservingInitramfsBuilder from .guest_observation import GuestObservationConfig, correlation_digest, observe_guest from .guest_transport import FileSerialGuestFactTransport, GuestFactTransport @@ -140,7 +140,7 @@ def _fact_channel_path(self, address: str) -> Path: def _guest_binding(self, matrix: Mapping[str, object]) -> dict[str, object]: domains = [item for item in matrix.get("domains", ()) if isinstance(item, Mapping)] correlations = { - str(domain.get("address", "")): correlation_digest(_aces_uuid(str(domain.get("address", "")))) + str(domain.get("address", "")): correlation_digest(_raes_uuid(str(domain.get("address", "")))) for domain in domains } return { diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_lifecycle.py b/implementations/python/packages/raes_backend_libvirt/techvault_lifecycle.py index 1ef05c3c1..5d2d42b49 100644 --- a/implementations/python/packages/raes_backend_libvirt/techvault_lifecycle.py +++ b/implementations/python/packages/raes_backend_libvirt/techvault_lifecycle.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass -from .drivers.libvirt import _aces_uuid, _error_code, _existing_uuid +from .drivers.libvirt import _error_code, _existing_uuid, _raes_uuid from .techvault_matrix import runtime_name @@ -45,7 +45,7 @@ def _resolve_by_uuid( native_items = _list_native(connection, list_method) resolved: NativeResolution | None = None if native_items is not None: - owned = [item for item in native_items if _existing_uuid(item) == _aces_uuid(address)] + owned = [item for item in native_items if _existing_uuid(item) == _raes_uuid(address)] if not owned: fallback_name = runtime_name(name_prefix, address) if any(_native_name(item) == fallback_name for item in native_items): @@ -84,7 +84,7 @@ def _resolve_verified_absence( address: str, ) -> NativeResolution | None: native_items = _list_native(connection, list_method) - if native_items is None or any(_existing_uuid(item) == _aces_uuid(address) for item in native_items): + if native_items is None or any(_existing_uuid(item) == _raes_uuid(address) for item in native_items): return None return NativeResolution(native=None, name=None) @@ -97,7 +97,7 @@ def verify_native_removed( ) -> bool: native_items = _list_native(connection, list_method) return native_items is not None and not any( - _native_name(item) == name or _existing_uuid(item) == _aces_uuid(address) for item in native_items + _native_name(item) == name or _existing_uuid(item) == _raes_uuid(address) for item in native_items ) diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py b/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py index 157c69d14..b1353bfee 100644 --- a/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py +++ b/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py @@ -13,7 +13,7 @@ from raes_backend_protocols.naming import provider_resource_name from .driver import DomainSpec, NetworkSpec -from .drivers.libvirt import _aces_uuid +from .drivers.libvirt import _raes_uuid _SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.-]+") _SUBSTRATE = "libvirt-qemu-initramfs" @@ -144,7 +144,7 @@ def domain_placements(spec: DomainSpec) -> dict[str, object]: def network_xml(network: Mapping[str, object]) -> str: root = ET.Element("network") ET.SubElement(root, "name").text = str(network.get("runtime_name", "")) - ET.SubElement(root, "uuid").text = _aces_uuid(str(network.get("address", ""))) + ET.SubElement(root, "uuid").text = _raes_uuid(str(network.get("address", ""))) if not network.get("internal"): ET.SubElement(root, "forward", {"mode": "nat"}) ip_node = ET.SubElement( @@ -174,7 +174,7 @@ def domain_xml( ) -> str: root = ET.Element("domain", {"type": "qemu"}) ET.SubElement(root, "name").text = str(domain.get("runtime_name", "")) - ET.SubElement(root, "uuid").text = _aces_uuid(str(domain.get("address", ""))) + ET.SubElement(root, "uuid").text = _raes_uuid(str(domain.get("address", ""))) ET.SubElement(root, "memory", {"unit": "MiB"}).text = str(domain.get("memory_mib", 128)) ET.SubElement(root, "vcpu").text = str(domain.get("vcpus", 1)) os_node = ET.SubElement(root, "os") diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_native.py b/implementations/python/packages/raes_backend_libvirt/techvault_native.py index 4d144ba6b..68089355b 100644 --- a/implementations/python/packages/raes_backend_libvirt/techvault_native.py +++ b/implementations/python/packages/raes_backend_libvirt/techvault_native.py @@ -44,7 +44,7 @@ NetworkSpec, RealizationObservation, ) -from .drivers.libvirt import Connector, _aces_uuid, _existing_uuid +from .drivers.libvirt import Connector, _existing_uuid, _raes_uuid from .envelopes import load_libvirt_realization_envelope from .techvault_appliance import ( BusyboxInitramfsBuilder, @@ -486,7 +486,7 @@ def _destroy_one(self, connection: object, lookup_method: str, address: str) -> if resolved.native is None: cleaned = True else: - if _existing_uuid(resolved.native) != _aces_uuid(address): + if _existing_uuid(resolved.native) != _raes_uuid(address): raise _OwnershipConflict(address) removed = _deactivate_and_undefine(resolved.native) cleaned = removed and _verify_native_removed( diff --git a/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py b/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py index abaf79c46..14f952a01 100644 --- a/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py +++ b/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py @@ -213,13 +213,13 @@ def _apply_export_addresses( export_addresses = export.get("compiled_address_sets") if not isinstance(export_addresses, Mapping): return [] - aces_sets = {cls: sorted(str(a) for a in _sequence(values)) for cls, values in address_sets.items()} - export_sets = {cls: sorted(str(a) for a in _sequence(export_addresses.get(cls))) for cls in aces_sets} + raes_sets = {cls: sorted(str(a) for a in _sequence(values)) for cls, values in address_sets.items()} + export_sets = {cls: sorted(str(a) for a in _sequence(export_addresses.get(cls))) for cls in raes_sets} descriptor["compiled_address_sets"] = export_sets return [ f"APTL export compiled_address_sets[{cls}] differs from the compiled RAES address set" - for cls in aces_sets - if export_sets[cls] != aces_sets[cls] + for cls in raes_sets + if export_sets[cls] != raes_sets[cls] ] diff --git a/implementations/python/tests/test_libvirt_backend_driver.py b/implementations/python/tests/test_libvirt_backend_driver.py index 6be79ed31..6c459f4c6 100644 --- a/implementations/python/tests/test_libvirt_backend_driver.py +++ b/implementations/python/tests/test_libvirt_backend_driver.py @@ -7,7 +7,7 @@ from raes_backend_libvirt.cloudinit import CloudInitSpec, CloudInitUser from raes_backend_libvirt.driver import DomainSpec, NetworkAcl, NetworkSpec -from raes_backend_libvirt.drivers.libvirt import LibvirtDeploymentDriver, _aces_uuid +from raes_backend_libvirt.drivers.libvirt import LibvirtDeploymentDriver, _raes_uuid from raes_backend_protocols.naming import provider_resource_name # Real libvirt reports a missing object with these stable VIR_ERR_NO_* codes via @@ -391,7 +391,7 @@ def test_libvirt_convergence_fails_closed_when_stopping_owned_object_fails(): # (permission/internal) must NOT be suppressed-then-undefined — the apply fails # closed and the still-running owned domain is left intact for retry. connection = _FakeConnection() - existing = _NativeObject(uuid=_aces_uuid("provision.node.web"), fail_destroy_code=_VIR_ERR_INTERNAL_ERROR) + existing = _NativeObject(uuid=_raes_uuid("provision.node.web"), fail_destroy_code=_VIR_ERR_INTERNAL_ERROR) connection.domains[_runtime_name("provision.node.web")] = existing driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) @@ -408,7 +408,7 @@ def test_libvirt_convergence_tolerates_stopping_an_inactive_owned_object(): # The benign side of the same path: converging an owned object that is already # inactive (stop raises VIR_ERR_OPERATION_INVALID) still undefines + redefines. connection = _FakeConnection() - existing = _NativeObject(uuid=_aces_uuid("provision.node.web"), fail_destroy_code=_VIR_ERR_OPERATION_INVALID) + existing = _NativeObject(uuid=_raes_uuid("provision.node.web"), fail_destroy_code=_VIR_ERR_OPERATION_INVALID) connection.domains[_runtime_name("provision.node.web")] = existing driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) @@ -421,7 +421,7 @@ def test_libvirt_convergence_tolerates_stopping_an_inactive_owned_object(): assert driver.realized_addresses() == {"provision.node.web"} -def test_libvirt_domain_xml_carries_deterministic_aces_uuid(): +def test_libvirt_domain_xml_carries_stable_raes_ownership_uuid(): connection = _FakeConnection() driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) @@ -434,6 +434,7 @@ def test_libvirt_domain_xml_carries_deterministic_aces_uuid(): uuid_second = _uuid_from_xml(connection.domain_xml[1]) assert not first.diagnostics + assert _raes_uuid("provision.node.web") == "049f04cb-0e5b-50b5-935e-acee9fe0cce9" assert uuid_first and uuid_first == uuid_second @@ -705,7 +706,7 @@ def test_libvirt_realize_rollback_leaves_a_pre_existing_updated_object_intact(): # second (foreign-named) domain fails; the updated domain must survive so the # preserved baseline snapshot that still claims it realized stays truthful. connection = _FakeConnection() - existing = _NativeObject(uuid=_aces_uuid("provision.node.web")) + existing = _NativeObject(uuid=_raes_uuid("provision.node.web")) connection.domains[_runtime_name("provision.node.web")] = existing foreign = _NativeObject(uuid="11111111-2222-3333-4444-555555555555") connection.domains[_runtime_name("provision.node.other")] = foreign From f1086fc594bfd8f331302cf37d965fe99e6b6e84 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 17:15:50 +0200 Subject: [PATCH 11/55] Document projection anchor compatibility --- .../participant-decision-surface-v1.json | 2 +- ...decision-surface-and-exposure-semantics.md | 27 +++++++++++++++++++ .../reference/shared-semantic-integrity.md | 2 +- docs/explain/sdl/lineage.md | 22 ++++++++++----- 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/contracts/schema-publication/entries/participant-decision-surface-v1.json b/contracts/schema-publication/entries/participant-decision-surface-v1.json index e00be1db3..2df8e6bd0 100644 --- a/contracts/schema-publication/entries/participant-decision-surface-v1.json +++ b/contracts/schema-publication/entries/participant-decision-surface-v1.json @@ -4,7 +4,7 @@ "stability": "draft", "content_hash": "e65ab17f47266f0b21964e5d97517b3d5e9ff998454c48605d2fc40057630c13", "last_change": { - "summary": "Added an optional tagged projection anchor that distinguishes derived decision-surface order from episode-readiness and terminal-observation history order, evidence, and provenance without changing existing decision-surface fields.", + "summary": "Breaking semantic and runtime-admission change to the draft v1 contract: added a structurally optional tagged projection anchor, defined observation_order as the derived per-episode decision-surface order distinct from anchor-local history order, and required a current resolved anchor for runtime admission.", "content_hash": "e65ab17f47266f0b21964e5d97517b3d5e9ff998454c48605d2fc40057630c13" } } diff --git a/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md b/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md index efd50210b..7533b8e2e 100644 --- a/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md +++ b/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md @@ -279,6 +279,33 @@ to the runtime admission path fails closed. Initial projection continues to use the compiled initial view relation; later projection continues to use the existing behavior-anchor indexes and effective view-relation selector. +### 9. Classify the anchor integration as a breaking semantic change + +The `participant-decision-surface-v1` schema remains in the `draft` stability +class. ADR-061 therefore permits an in-place v1 change, and adding +`projection_anchor` is structurally additive because the serialized property is +optional. That structural fact is not an end-to-end compatibility claim. + +This amendment is a breaking semantic and runtime-admission change: + +- surface `observation_order` is the derived per-episode decision-surface + coordinate, not the referenced behavior-history index; +- an anchored projector requires the current trusted runtime snapshot; and +- runtime admission rejects an unanchored surface even though an older payload + can still pass structural schema validation. + +The Python distribution release carrying this amendment must therefore use the +repository's breaking-change release classification. The publication ledger +must describe the structural-versus-semantic distinction and must not claim +backward, forward, behavioral, or operational compatibility. + +Consumers migrate by resolving a readiness or behavior anchor from the current +trusted snapshot, carrying it on the projected surface, passing that snapshot +to anchored projection, using `observation_order` for `D(p,e,o)`, and using +`anchor_order` only for the referenced lifecycle or behavior-history event. +Cached or independently constructed unanchored surfaces cannot be migrated by +copying an event ref; they must be reprojected from current authority. + ## Alternatives Considered ### Add a flat participant `tools` list diff --git a/docs/explain/reference/shared-semantic-integrity.md b/docs/explain/reference/shared-semantic-integrity.md index 6de2023cd..8ebe1a1e7 100644 --- a/docs/explain/reference/shared-semantic-integrity.md +++ b/docs/explain/reference/shared-semantic-integrity.md @@ -251,7 +251,7 @@ so they are tracked by their own requirements, not here. | 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 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, and exposure/visibility-boundary semantics | SEM-219, SEM-220, SEM-226 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-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`, `implementations/python/packages/raes/participant_behavior_specification.py`, `implementations/python/packages/raes/semantics/participant_behavior.py`, `implementations/python/packages/raes_processor/compiler/participant_behaviors.py`, `implementations/python/packages/raes_processor/models/behavior_resources.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | +| Participant tool/affordance, decision-surface, and exposure/visibility-boundary semantics | SEM-219, SEM-220, SEM-226 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-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`, `docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md`, `contracts/schemas/control-plane/participant-decision-surface-v1.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_anchor.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_anchor.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_participant_semantics_invariant_oracle.py` | partial | | 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 | diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index e87668c2a..45c7a8f82 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -953,10 +953,14 @@ which dynamic queue/log/config details remain evidence or bounded settings. - SEM-220's executable participant decision surface adopts the existing action/observation-interface lineage above without importing a UI, prompt, command, or backend-native parameter language. RAES maps one participant, - episode, and behavior-history order point to - `ParticipantDecisionSurfaceModel`; maps the three portable selection forms - to its discriminated open-ended, constrained-form, and candidate-set - payloads; maps governed action meaning to compiled + episode, per-episode decision-surface order, and typed lifecycle- or + behavior-history anchor to `ParticipantDecisionSurfaceModel`. The current + RUN-311 `episode_running` event grounds `D(p,e,0)` while behavior history is + empty; later surfaces use the exact terminal `observation_emitted` event and + complete behavior-history prefix without treating that event's history order + as the decision-surface order. RAES maps the three portable selection forms + to discriminated open-ended, constrained-form, and candidate-set payloads; + maps governed action meaning to compiled `ParticipantActionContractRuntime` addresses; maps participant-local presentation to the shared observation-boundary effective-view selector; maps candidate applicability to explicit SEM-211 eligibility state and @@ -965,9 +969,13 @@ which dynamic queue/log/config details remain evidence or bounded settings. argument-shape resolution before the existing `ParticipantActionAdmissionRequest` path. Delivery is implemented for the published closed contract and fixtures, compiled-runtime projection, - context-envelope agreement, proposal binding, runtime admission routing, - schema publication, and adversarial ordering/bypass checks. Evidence is - `participant-decision-surface-v1`, `project_participant_decision_surface()`, + trusted snapshot/history anchor resolution, admission-time freshness + validation, context-envelope agreement, proposal binding, runtime admission + routing, schema publication, and adversarial ordering/bypass checks. Evidence + is `participant-decision-surface-v1`, + `resolve_participant_episode_readiness_anchor()`, + `resolve_participant_behavior_projection_anchor()`, + `project_participant_decision_surface()`, `bind_participant_decision_surface_selection()`, and `implementations/python/tests/test_sem_220_participant_decision_surface.py`. Human proxy, script, LLM-agent, and RL-agent fixtures retain identical stable From dd859292566365a19bc34fae916f89dafeee7055 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 18:27:13 +0200 Subject: [PATCH 12/55] Declare participant policy capabilities --- .../controlled-vocabularies-v1.json | 24 ++ .../valid/feature-support-bounded.json | 18 + .../backend-manifest-v2/valid/stub.json | 77 +++- .../valid/reference.json | 24 ++ .../backend/full-remote-control-plane.json | 2 + .../entries/backend-manifest-v2.json | 6 +- .../entries/backend-profile-v1.json | 6 +- .../backend-manifest/backend-manifest-v2.json | 125 ++++++ .../schemas/profiles/backend-profile-v1.json | 2 + ...participant-policy-capability-preflight.md | 359 ++++++++++++++++++ docs/explain/sdl/lineage.md | 33 ++ .../prior-art-and-design-criteria.md | 31 ++ .../packages/raes_backend_libvirt/manifest.py | 10 + .../raes_backend_protocols/capabilities.py | 3 + .../capability_admission.py | 105 +++++ .../raes_backend_protocols/manifest.py | 4 + .../participant_capabilities.py | 101 +++-- .../packages/raes_backend_stubs/manifest.py | 21 +- .../raes_conformance/conformance/report.py | 12 + .../raes_conformance/conformance/target.py | 69 +++- .../conformance/validators.py | 2 + .../contracts/feature_support.py | 147 +++++++ .../raes_contracts/contracts/manifests.py | 90 +---- .../contracts/participant_manifests.py | 21 + .../raes_contracts/manifest_authority.py | 82 ++++ .../packages/raes_processor/planner/core.py | 25 +- .../raes_reference_backend/manifest.py | 21 +- .../python/tests/test_backend_manifest.py | 281 +++++++++++++- .../python/tests/test_backend_profiles.py | 2 + ...st_dsl_437_benign_participant_execution.py | 67 +++- .../python/tests/test_runtime_conformance.py | 52 ++- 31 files changed, 1662 insertions(+), 160 deletions(-) create mode 100644 docs/decisions/issue-801-api-407-participant-policy-capability-preflight.md create mode 100644 implementations/python/packages/raes_contracts/contracts/feature_support.py diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index 24ca22534..d126e1f21 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -959,6 +959,30 @@ "title": "Outcome Interpretation", "description": "Backend supports participant-local outcome interpretation records." }, + "participant_declassification": { + "title": "Participant Declassification", + "description": "Backend supports governed participant-relative declassification decisions and evidence." + }, + "participant_directed_inject_delivery": { + "title": "Participant-Directed Inject Delivery", + "description": "Backend supports governed delivery of participant-directed injects without conflating environment injects." + }, + "participant_egress_projection": { + "title": "Participant Egress Projection", + "description": "Backend supports governed participant-relative projection at the egress boundary." + }, + "participant_ingress_admission": { + "title": "Participant Ingress Admission", + "description": "Backend supports governed participant action or control admission at the ingress boundary." + }, + "participant_intervention": { + "title": "Participant Intervention", + "description": "Backend supports governed participant intervention and supervisory control occurrences." + }, + "participant_transformation": { + "title": "Participant Transformation", + "description": "Backend supports governed non-mutating participant-boundary transformations with fresh result identity." + }, "preconditions": { "title": "Preconditions", "description": "Backend supports declared participant action preconditions." 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 f2ee0191f..0fbbff811 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 @@ -19,6 +19,7 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", @@ -181,6 +182,7 @@ "failure_classes", "observation_boundaries", "outcome_interpretation", + "participant_transformation", "preconditions", "state_transitions", "temporal_contracts" @@ -208,6 +210,22 @@ "constraint_refs": [], "disclosure_refs": [] }, + { + "feature": "participant_transformation", + "support_level": "bounded", + "constraint_refs": [ + "constraint:participant-transformation:finite-rule-set" + ], + "limitation_refs": [ + "limitation:participant-transformation:finite-rule-set" + ], + "disclosure_refs": [ + "disclosure:participant-transformation:bounded" + ], + "evidence_refs": [ + "conformance:participant-transformation:bounded-case-1" + ] + }, { "feature": "x-acme:custom-feature", "support_level": "disclosed_weak", 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 21cc7cbb6..4eadb7bcf 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -125,7 +125,80 @@ }, "participant_runtime": { "constraints": {}, - "feature_support": [], + "feature_support": [ + { + "constraint_refs": [], + "disclosure_refs": [ + "disclosure:participant_declassification:unsupported" + ], + "evidence_refs": [], + "feature": "participant_declassification", + "limitation_refs": [ + "limitation:participant_declassification:not-realized" + ], + "support_level": "unsupported" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "disclosure:participant_directed_inject_delivery:unsupported" + ], + "evidence_refs": [], + "feature": "participant_directed_inject_delivery", + "limitation_refs": [ + "limitation:participant_directed_inject_delivery:not-realized" + ], + "support_level": "unsupported" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "disclosure:participant_egress_projection:unsupported" + ], + "evidence_refs": [], + "feature": "participant_egress_projection", + "limitation_refs": [ + "limitation:participant_egress_projection:not-realized" + ], + "support_level": "unsupported" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "disclosure:participant_ingress_admission:unsupported" + ], + "evidence_refs": [], + "feature": "participant_ingress_admission", + "limitation_refs": [ + "limitation:participant_ingress_admission:not-realized" + ], + "support_level": "unsupported" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "disclosure:participant_intervention:unsupported" + ], + "evidence_refs": [], + "feature": "participant_intervention", + "limitation_refs": [ + "limitation:participant_intervention:not-realized" + ], + "support_level": "unsupported" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "disclosure:participant_transformation:unsupported" + ], + "evidence_refs": [], + "feature": "participant_transformation", + "limitation_refs": [ + "limitation:participant_transformation:not-realized" + ], + "support_level": "unsupported" + } + ], "max_autonomous_action_attempts": null, "max_autonomous_in_flight": null, "max_autonomous_participants": null, @@ -307,6 +380,8 @@ "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-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", 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 24ca22534..d126e1f21 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -959,6 +959,30 @@ "title": "Outcome Interpretation", "description": "Backend supports participant-local outcome interpretation records." }, + "participant_declassification": { + "title": "Participant Declassification", + "description": "Backend supports governed participant-relative declassification decisions and evidence." + }, + "participant_directed_inject_delivery": { + "title": "Participant-Directed Inject Delivery", + "description": "Backend supports governed delivery of participant-directed injects without conflating environment injects." + }, + "participant_egress_projection": { + "title": "Participant Egress Projection", + "description": "Backend supports governed participant-relative projection at the egress boundary." + }, + "participant_ingress_admission": { + "title": "Participant Ingress Admission", + "description": "Backend supports governed participant action or control admission at the ingress boundary." + }, + "participant_intervention": { + "title": "Participant Intervention", + "description": "Backend supports governed participant intervention and supervisory control occurrences." + }, + "participant_transformation": { + "title": "Participant Transformation", + "description": "Backend supports governed non-mutating participant-boundary transformations with fresh result identity." + }, "preconditions": { "title": "Preconditions", "description": "Backend supports declared participant action preconditions." diff --git a/contracts/profiles/backend/full-remote-control-plane.json b/contracts/profiles/backend/full-remote-control-plane.json index 2a39c7d3d..a28d8cb0d 100644 --- a/contracts/profiles/backend/full-remote-control-plane.json +++ b/contracts/profiles/backend/full-remote-control-plane.json @@ -16,6 +16,8 @@ "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-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1" diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index f5d62636a..171315df1 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": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48", + "content_hash": "2c1c31cbbeff25e007246caa9e4f130a9b0aae36a7d704bca6b75090bba45075", "last_change": { - "summary": "Added a typed, owner-governed apparatus configuration target registry and experiment binding contract support.", - "content_hash": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48" + "summary": "Added evidence-backed participant policy feature support, limitation, and disclosure declarations.", + "content_hash": "2c1c31cbbeff25e007246caa9e4f130a9b0aae36a7d704bca6b75090bba45075" } } diff --git a/contracts/schema-publication/entries/backend-profile-v1.json b/contracts/schema-publication/entries/backend-profile-v1.json index 2998372de..ac7261a1c 100644 --- a/contracts/schema-publication/entries/backend-profile-v1.json +++ b/contracts/schema-publication/entries/backend-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "fd118e131b273684ac540675a8ba8d4b6ef1f10990ddd0d0dc5776c1b6c4a233", + "content_hash": "63c54137176f37d1253095c1fb93b3090084f604972e41588d0f71de14855e53", "last_change": { - "summary": "Added the authoritative experiment binding descriptor contract to the governed backend contract vocabulary.", - "content_hash": "fd118e131b273684ac540675a8ba8d4b6ef1f10990ddd0d0dc5776c1b6c4a233" + "summary": "Added participant control and crossing occurrence contracts to the governed backend profile vocabulary.", + "content_hash": "63c54137176f37d1253095c1fb93b3090084f604972e41588d0f71de14855e53" } } diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index 11b6c1de7..9707fefd5 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -748,6 +748,113 @@ "disclosure_refs" ] } + }, + { + "if": { + "properties": { + "feature": { + "enum": [ + "participant_declassification", + "participant_directed_inject_delivery", + "participant_egress_projection", + "participant_ingress_admission", + "participant_intervention", + "participant_transformation" + ] + }, + "support_level": { + "enum": [ + "unsupported", + "disclosed_weak", + "bounded" + ] + } + }, + "required": [ + "feature", + "support_level" + ] + }, + "then": { + "properties": { + "limitation_refs": { + "minItems": 1 + } + }, + "required": [ + "limitation_refs" + ] + } + }, + { + "if": { + "properties": { + "feature": { + "enum": [ + "participant_declassification", + "participant_directed_inject_delivery", + "participant_egress_projection", + "participant_ingress_admission", + "participant_intervention", + "participant_transformation" + ] + }, + "support_level": { + "const": "bounded" + } + }, + "required": [ + "feature", + "support_level" + ] + }, + "then": { + "properties": { + "constraint_refs": { + "minItems": 1 + } + }, + "required": [ + "constraint_refs" + ] + } + }, + { + "if": { + "properties": { + "feature": { + "enum": [ + "participant_declassification", + "participant_directed_inject_delivery", + "participant_egress_projection", + "participant_ingress_admission", + "participant_intervention", + "participant_transformation" + ] + }, + "support_level": { + "enum": [ + "disclosed_weak", + "bounded", + "exact" + ] + } + }, + "required": [ + "feature", + "support_level" + ] + }, + "then": { + "properties": { + "evidence_refs": { + "minItems": 1 + } + }, + "required": [ + "evidence_refs" + ] + } } ], "description": "API-407 per-feature participant runtime support declaration.", @@ -768,11 +875,27 @@ "title": "Disclosure Refs", "type": "array" }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, "feature": { "minLength": 1, "title": "Feature", "type": "string" }, + "limitation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Limitation Refs", + "type": "array" + }, "support_level": { "$ref": "#/$defs/ParticipantFeatureSupportLevel" } @@ -1613,6 +1736,8 @@ "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-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index d35cde8a1..079d471ca 100644 --- a/contracts/schemas/profiles/backend-profile-v1.json +++ b/contracts/schemas/profiles/backend-profile-v1.json @@ -29,6 +29,8 @@ "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-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", diff --git a/docs/decisions/issue-801-api-407-participant-policy-capability-preflight.md b/docs/decisions/issue-801-api-407-participant-policy-capability-preflight.md new file mode 100644 index 000000000..9387e6bc7 --- /dev/null +++ b/docs/decisions/issue-801-api-407-participant-policy-capability-preflight.md @@ -0,0 +1,359 @@ +# Issue 801 API-407 Participant Policy Capability Preflight + +Date: 2026-07-26 + +Issue: #801. + +Requirement: API-407. + +This note records architecture boundaries and implementation guardrails for +extending backend participant feature-support declarations to participant +information-flow and control features. It is guidance only. It does not change +the backend manifest, publish schemas or vocabularies, implement admission, +change a backend, run conformance, or claim participant-policy realization. + +## Decisive Current-State Finding + +API-407 already has one canonical manifest surface: +`capabilities.participant_runtime.feature_support` on +`backend-manifest-v2`. `ParticipantFeatureSupportModel`, +`ParticipantFeatureSupport`, `backend_manifest_v2_model()`, and +`backend_manifest_from_v2_model()` already preserve that surface across the +published contract and internal backend protocol. + +The current surface is not sufficient for issue #801 by itself: + +- the governed behavior and interaction feature vocabularies do not name the + required participant-control features; +- feature entries carry strength, constraint refs, and disclosure refs, but + not the issue's explicit limitation and conformance-evidence refs; +- below-exact disclosure and one contradiction are validated, but positive + support is not checked against its required evidence contracts; +- `participant_runtime_capability_contract_gaps()` checks API-405 supported + lists, while the planner independently reduces required features to set + membership and ignores API-407 strength; +- `BACKEND_SUPPORTED_CONTRACT_IDS` and the full remote-control backend profile + do not yet include the API-409/API-423 participant control/crossing + contracts; and +- `BackendConformanceReport` can already carry finite cases, gaps, + limitations, and explicit nonclaims, but no participant-control + feature-support cases currently exercise it. + +Issue #801 therefore extends existing declarations, evidence mapping, +admission, and conformance. It does not need another manifest block, profile +family, report, policy engine, or ADR. + +## Binding Authorities + +- Accepted ADR-085 and + `specs/formal/participant-semantics/information-flow-control.md` define the + participant-control operations, deny-first policy composition, weakening + rule, evidence limits, and the requirement that missing support fail closed. +- ADR-060 and + `specs/formal/runtime-contracts/participant-backend-contracts.md` own the + API-405/API-407 manifest boundary and the existing + `unsupported < disclosed_weak < bounded < exact` scale. ADR-085's later + participant-control decision extends that entry; it does not merge it with + SEM-218 realization support. +- API-409 `participant-control-occurrence-v1` and API-423 + `participant-crossing-occurrence-v1` own control and crossing realization + facts. A manifest declaration references their contract/evidence surface; it + does not copy their policy, occurrence, transformation, delivery, or audit + fields. +- ADR-009, ADR-012, ADR-061, `contracts/README.md`, and the schema/concept + policy tools govern published shape, vocabulary, compatibility, and + publication records. +- `.ground-control.yaml`, `.gc/plan-rules.md`, and `noxfile.py` own workflow. + The branch name does not contain `API-407`, so implementation and completion + checks must set `ACES_REQUIREMENT_UID=API-407`. + +These authorities settle the architecture. A new ADR is required only if the +implementation discovers an actual conflict with them. + +## Architecture Decisions And Guardrails + +### Extend the existing entry, never fork the surface + +Participant-control support remains one `ParticipantFeatureSupport` entry per +governed feature under `capabilities.participant_runtime.feature_support`. +Keep the existing support-level enum and its ordering. Do not add control +booleans, a `participant_policy` manifest block, backend-specific capability +objects, another support-strength enum, or a second profile registry. + +The six required semantic features are distinct: + +- participant ingress admission; +- participant egress projection; +- governed declassification; +- non-mutating transformation; +- participant intervention; and +- participant-directed inject delivery. + +Each must receive one canonical feature id in the existing participant runtime +feature authority. Ingress and egress must remain visible in the ids; generic +`admission` or `projection` terms are too ambiguous. Participant-directed +inject delivery must retain its distinction from an environment inject. +Lexical ids are declared once in +`participant-runtime-behavior-features` and reused everywhere; local enums or +aliases are not authority. These are participant-runtime policy features, not +new interaction patterns, so a third feature taxonomy is unwarranted. + +### Keep constraint, limitation, disclosure, and evidence meanings separate + +The same feature-support entry is the extension seam. In addition to its +existing `feature`, `support_level`, `constraint_refs`, and `disclosure_refs`, +issue #801 needs out-of-line `limitation_refs` and `evidence_refs`. Their +meanings must not collapse: + +- `constraint_refs` identify the domain or bound within which support is + claimed; +- `limitation_refs` identify known exclusions, weakened guarantees, and + explicit nonclaims; +- `disclosure_refs` identify the disclosure required for an affected consumer + or audience; and +- `evidence_refs` identify reviewable conformance/evidence records for a + positive support claim. + +All are unique, non-empty references. They never contain policy bodies, +payloads, backend logs, credentials, hidden participant state, or inline +evidence. + +Preserve the existing rule that every below-exact entry requires disclosure. +A bounded claim must name its bound; a disclosed-weak claim must name its +limitations; and a positive claim (`disclosed_weak`, `bounded`, or `exact`) +must cite evidence and the term's required published contracts. Unsupported +is an explicit negative posture, not positive realization evidence. + +For each issue-801 feature, the broad API-405 presence declaration and the +API-407 strength entry must agree: positive strength requires the feature to +be present in the owning supported-feature list, while `unsupported` requires +it to be absent. Duplicate entries, unknown ids or strengths, empty refs, +missing required refs, and either direction of contradiction fail manifest +validation. + +Do not make every historical manifest globally total as an incidental schema +break. Total declaration of the six participant-control features belongs to +the backend profile/conformance/admission contexts that claim this capability. +Outside those contexts, absence continues to make no strength claim under +ADR-060. Inside them, absence is a fail-closed unsupported gap. + +### Keep evidence criteria canonical and claims honest + +`PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS` remains the single +term-to-required-contract table. Extend it for the new governed terms and keep +the existing vocabulary-parity test total. Do not add a second feature evidence +map in contracts, profiles, the planner, or conformance. + +Required contract evidence must point to the owning surfaces. At minimum, the +mapping must distinguish: + +- ingress admission from API-409 control and SEM-211 admission facts; +- egress projection from API-406 observation/projection facts; +- declassification and transformation from API-423 crossing facts; +- intervention from API-409 control occurrences plus API-423 crossings; and +- participant-directed inject delivery from its DSL-142 binding plus API-423 + delivery/observation facts. + +Only backend-facing/live contract ids belong in a backend manifest. An SDL +authoring schema or requirement reference is not backend realization evidence. +Add any newly claimable API-409/API-423 contract ids to +`BACKEND_SUPPORTED_CONTRACT_IDS` before a manifest or profile can cite them, +and update the canonical full remote-control profile rather than inventing an +issue-specific profile. + +An entry's evidence refs do not replace the required-contract table, and +declared contracts do not replace conformance. Contract publication, method +presence, a manifest claim, runtime realization, bounded conformance, model +checking, and proof remain independent statuses. + +### Put strength-aware admission in the existing owning layer + +`raes_backend_protocols.capability_admission` owns backend capability matching. +Extend that layer so one parameterized comparison evaluates: + +- required governed feature id; +- required minimum strength; +- the manifest's explicit feature entry; +- required contract/evidence availability; and +- an optional, already-authorized downgrade reference and allowed effective + strength. + +The helper must not decide policy authorization itself. Without an explicit +authorization supplied by the owning participant policy/crossing decision, +missing support, a missing entry, insufficient strength, missing evidence, or +an unresolved extension term is an error gap. An authorized downgrade records +the weaker effective strength, provenance, disclosure, and limitation refs and +removes the stronger claim; success-with-warning while retaining `exact` is +forbidden. + +The planner's `_participant_execution_diagnostics()` must consume this owning +helper instead of maintaining a second supported-feature set subtraction. +Target selection, planning, and conformance must call the same comparison; they +must not each reinterpret strength ordering or downgrade rules. + +`RuntimeTarget._validate_runtime_target_shape()` remains a protocol-shape +check. Do not infer participant-policy support from `admit_action`, another +method's presence, a callable signature, or a component being non-null. + +### Extend the existing conformance report + +Use `run_target_conformance()`, `ConformanceCaseResult`, +`BackendConformanceReport`, `unsupported_capability_gaps`, and +`BehavioralClaimBindingModel`. Feature-specific cases must record the exact +feature, declared/effective strength, finite target/profile/corpus scope, +evidence refs, limitations, and explicit nonclaims. Negative cases cover +missing entries, insufficient strength, contradictions, missing evidence, +unauthorized downgrade, and a downgrade that retains a stronger claim. + +Do not create an API-407 report type or runner. The existing bounded claim +already states that finite cases do not establish native realization, +unexecuted behavior, trace equivalence, bisimulation, or universal proof; keep +and specialize those nonclaims rather than replacing them. + +## Canonical Incumbents To Reuse + +| Concern | Canonical incumbent and required use | +| --- | --- | +| Published manifest shape | `BackendManifestV2Model`, `BackendCapabilitiesV2Model`, `ParticipantRuntimeCapabilitiesModel`, and `ParticipantFeatureSupportModel`. Keep `ContractModel(extra="forbid")` and one model-level owner for entry/cross-field invariants. | +| Internal backend declaration | `BackendManifest`, `ParticipantRuntimeCapabilities`, `ParticipantFeatureSupport`, `backend_manifest_v2_model()`, `backend_manifest_payload()`, and `backend_manifest_from_v2_model()`. Preserve exact round-trip parity; add no DTO layer. | +| Concept and vocabulary | `controlled-vocabularies-v1`, `participant-runtime-behavior-features`, `participant-runtime-feature-support-levels`, existing scope validators, concept bindings, and controlled-vocabulary parity tests. | +| Evidence criteria | `PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS`, `BACKEND_SUPPORTED_CONTRACT_IDS`, `participant_runtime_capability_contract_gaps()`, and their total-coverage tests. | +| Admission | `raes_backend_protocols.capability_admission`, `raes_processor.planner.core`, compiled `backend_feature_support_refs`, planner `Diagnostic` values, and the normal error-severity execution-plan gate. | +| Participant policy facts | ADR-085, SEM-211 admission, API-409 control occurrences, API-423 crossing decisions/operations/posture/loss records, SEM-226 exposure records, and DSL-142 inject bindings. Reference them; never duplicate their fields in a manifest. | +| Backend declarations | `raes_backend_stubs.manifest`, `raes_reference_backend.manifest`, and `raes_backend_libvirt.manifest`. Each declaration must describe delivered behavior honestly; no backend becomes exact merely to keep tests green. | +| Profiles and conformance | `contracts/profiles/backend/`, `BackendProfileModel`, `run_fixture_suite()`, `run_target_conformance()`, `ConformanceCaseResult`, `BackendConformanceReport`, and `BehavioralClaimBindingModel`. | +| Diagnostics and observability | `Diagnostic`, `DiagnosticModel`, `Severity`, `unsupported_capability_gaps`, bounded conformance payloads, and existing CLI/control-plane error redaction. No new exception hierarchy, logger, metric stream, or audit channel. | +| Publication | Hand-governed `contracts/schemas/backend-manifest/backend-manifest-v2.json`, matching `schema_bundle()` output, backend-manifest valid/invalid fixtures, `contracts/schema-publication/entries/backend-manifest-v2.json`, ADR-061 compatibility classification, and generated-schema drift checks. | +| Lineage | The participant section of `docs/explain/sdl/lineage.md`, `SDLLineageLedgerModel`, `tools/check_sdl_lineage.py`, and the source audit. Record delivery/evidence/nonclaims; change the ledger or source audit only for a changed normative derivation or compatibility claim. | +| Workflow | `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, and the repo-policy, requirement-governance, concept-authority, schema-publication, generated-schema, JSON-artifact, lineage, conformance, and full verification gates. | + +## Cross-Cutting Layers And Security Posture + +1. **Manifest file and parser gate.** External manifests continue through the + existing `raes processor plan --manifest` path: a path argument only, a + one-megabyte file limit, UTF-8 JSON decoding, object-shape check, + `BackendManifestV2Model.model_validate()`, and reconstruction through + `backend_manifest_from_v2_model()`. Backend-native loaders must reach the + same models. Do not parse a second manifest fragment or accept policy from + unvalidated mappings. +2. **Closed contract and semantic gate.** JSON Schema and Pydantic reject + unknown fields and malformed refs; model validation owns single-entry and + list contradictions; the capability-admission helper owns manifest-to- + requirement strength/evidence agreement. Do not repeat semantic joins in + schema conditionals, model constructors, planners, registries, and + conformance runners. +3. **Concept and publication gate.** Every standard feature id is governed, + every extension uses the existing `x-:` rule, and every + required contract id is manifest-authorized. The published schema is + hand-governed authority; update its publication entry `last_change` and + content hash, then keep `schema_bundle()` byte-semantically identical. +4. **Authentication and policy gate.** Issue #801 adds no HTTP route or + authentication mechanism. If a later API exposes declarations or admission + decisions, it must enter through `create_control_plane_app()`, + `ControlPlaneSecurityConfig.strict_defaults()`, verified bearer/proxy + identity, `ControlPlaneRole`, target binding, request-size guards, + fingerprints/idempotency for mutations, and `AuditEvent`. Caller + authorization never authorizes participant action, visibility, + declassification, or downgrade. +5. **Secret and hidden-content gate.** Manifests, profiles, fixtures, + diagnostics, conformance reports, lineage, audit, and logs contain only + bounded ids, refs, digests, codes, strengths, limitations, and nonclaims. + They exclude tokens, keys, credentials, prompts/private memory, hidden + answers/world state, policy bodies, raw rejected manifests, backend objects, + environment dumps, and raw conformance payloads. +6. **Diagnostic and error-envelope gate.** Expected admission/conformance + failures use stable, bounded `Diagnostic` codes/messages and + `unsupported_capability_gaps`. They may identify governed feature ids and + missing contract ids, but must not echo rejected ref values or whole + entries. The plan CLI retains its field-path/error-kind summary and generic + deep-validation failure. Unexpected HTTP failures retain + `{"detail":"internal server error"}`. +7. **Configuration and OS/process gate.** The change needs no new environment + binding, secret loader, subprocess, daemon, socket, or host configuration. + Manifest content remains in a bounded file or typed in-process object; no + policy, evidence, credential, hidden value, or full manifest belongs in + process argv, environment variables, filenames, stdout/stderr, or host + logs. A manifest path in existing argv is not authority and must not bypass + validation. +8. **Persistence and observability gate.** Static manifest declarations and + conformance reports do not create a new durable store. Any later realized + downgrade or crossing uses first-class API-409/API-423/runtime/evidence + carriers and existing `RuntimeSnapshot`/`ControlPlaneStore` append-only + paths. It does not live only in snapshot `metadata`, audit `details`, logs, + or a backend-local cache. +9. **Conformance and claim gate.** Profile contract gaps, manifest claim gaps, + finite feature cases, target probes, native-conformance status, evidence + refs, limitations, and explicit nonclaims remain separately visible. + Passing shape validation or finite probes never upgrades the manifest's + strength or establishes production enforcement or universal security. + +## Extensibility Seam + +The stable seam is the existing feature-support entry plus one strength-aware +capability comparison parameterized by feature id, minimum strength, required +contract/evidence set, and optional explicit downgrade authorization/effective +strength. The term-to-contract table and controlled vocabulary supply standard +feature data; callers supply scenario/profile demand and already-resolved policy +authorization. + +This lets the next participant-control feature, backend extension term, +audience-specific downgrade, or stronger evidence profile add vocabulary/data +and cases without adding fields to `RuntimeTarget`, branching the planner, +creating another manifest block, or editing every backend adapter. New wire +meaning still follows ADR-061; an open metadata or `constraints` bag is not the +extension seam. + +## Gotchas And Anti-Patterns + +Avoid: + +- a second manifest section, capability/profile registry, support enum, + evidence map, schema registry, validator stack, fixture runner, conformance + report, exception hierarchy, logger, audit channel, or persistence store; +- adding control features only to `feature_support` while omitting the owning + supported-feature vocabulary/list, concept bindings, required-contract map, + manifest contract allowlist, or profile; +- treating generic admission or projection as unambiguously participant + ingress/egress, or treating every orchestration inject as participant input; +- collapsing constraints, limitations, disclosures, evidence, and nonclaims + into one string list or free-form `constraints`/`metadata`/`details`; +- using the API-423 runtime `backend_posture` enum as a replacement for the + API-407 support-level enum; a crossing posture and a manifest capability + claim are linked but distinct records; +- treating API-405 list presence as `exact`, absence as `unsupported`, or an + API-407 positive entry as realization without contract and conformance + evidence; +- inferring support from method presence, protocol shape, schema publication, + fixture validity, a target component being present, backend logs, or a + successful unrelated probe; +- silently accepting below-required strength, authorizing a downgrade inside + the capability helper, or retaining the stronger claim after downgrade; +- letting one strong component hide a weak adapter, policy evaluator, + clock/order source, projection stage, evidence store, or replay path; +- reporting bounded conformance as native realization, noninterference, + equivalence, bisimulation, model checking, or proof; +- echoing raw manifest entries, refs, policy bodies, hidden content, secrets, + backend object representations, environment/process data, or tracebacks in + diagnostics, audit, reports, or logs; +- hand-generating the published schema without publication-ledger and + compatibility review, or changing Python alone and treating generated output + as authority; and +- changing the lineage ledger/source audit merely because implementation + delivery status changes. + +## Non-Goals And Implementation Boundaries + +- No RUN-319 backend enforcement, participant gateway, transport, endpoint, + policy evaluator, policy expression language, UI, intervention service, + inject delivery, projection, declassification, or transformation execution. +- No new SDL syntax, behavior mode, crossing/control carrier, realization + support mode, validation profile, participant implementation capability, or + general concern-domain disclosure surface. +- No inference of capability from methods and no capability booleans. +- No persistence, replay, migration, credential handling, provider + integration, subprocess/daemon, or host configuration. +- No claim that a declaration is realization evidence, that a required + contract is conformance evidence, or that finite conformance is universal + assurance. +- No lineage-ledger/source-audit change unless implementation changes a + normative external derivation or compatibility claim. diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index e87668c2a..4c54f3c98 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -1072,6 +1072,39 @@ which dynamic queue/log/config details remain evidence or bounded settings. refinement, simulation, bisimulation, epistemic equivalence, or proof. The lineage ledger and source audit remain unchanged because API-423 reuses the recorded SEM-230 derivation and changes no normative external derivation or + compatibility claim. +- API-407's participant-policy capability extension adopts partial design + precedents, not source syntax or compatibility. YANG 1.1 features/deviations + and the YANG Library map to governed feature ids and explicit limitations; + Vulkan device-feature negotiation maps to fail-closed target admission; + XACML's deny-first decision/obligation separation maps to the rule that + capability never grants policy authority; and OGC conformance classes map to + named finite evidence cases and explicit nonclaims. No one source supplies + the complete ACES vector. +- The exact ACES mapping is the existing + `capabilities.participant_runtime.feature_support` entry with + `unsupported < disclosed_weak < bounded < exact`, separate constraint, + limitation, disclosure, and evidence references, the six governed ingress, + egress, declassification, transformation, intervention, and + participant-directed inject-delivery features, and their API-409/API-423 + required-contract evidence. `resolve_participant_feature_support()` is the + single strength comparison used by planner admission; an authorized + downgrade returns the weaker manifest entry unchanged and requires separate + policy and provenance references. +- Delivery status is contract-, profile-, admission-, fixture-, and + bounded-conformance-implemented. Evidence is the backend-manifest schema and + publication entries, controlled vocabulary, stub/reference/libvirt + declarations, full-remote-control-plane profile, planner and conformance + integration, and `implementations/python/tests/test_backend_manifest.py`, + `test_dsl_437_benign_participant_execution.py`, and + `test_runtime_conformance.py`. The existing backends disclose all six + features as unsupported; issue #801 does not implement RUN-319 policy + enforcement. Manifest validity, method presence, finite cases, or adjacent + API-409/API-423 carriers do not establish authorization, delivery, native + realization, noninterference, equivalence, bisimulation, model checking, or + proof. These standards are comparative design precedents only, so this + change introduces no normative external derivation or compatibility claim + and the lineage ledger/source audit remain unchanged. - RUN-310 composes the same participant-interface, append-only event-history, mixed-control, information-flow, and access-control lineage into live supervisory mediation; it introduces no new external semantic source. The diff --git a/docs/research/participant-backend-contracts/prior-art-and-design-criteria.md b/docs/research/participant-backend-contracts/prior-art-and-design-criteria.md index 9866bb6e2..60093a918 100644 --- a/docs/research/participant-backend-contracts/prior-art-and-design-criteria.md +++ b/docs/research/participant-backend-contracts/prior-art-and-design-criteria.md @@ -76,6 +76,37 @@ IETF/IANA registries govern extension terms. Lessons: evidence criteria — the API-405 term-level evidence table generalizes to API-407 constraints. +Issue #801 reviewed four additional standards-level mechanisms against the +participant-policy problem: + +- [YANG 1.1 features and deviations](https://www.rfc-editor.org/rfc/rfc7950.html) + plus the [YANG Library](https://www.rfc-editor.org/rfc/rfc8525.html) partially + solve governed feature advertisement and server-specific deviation + disclosure. ACES adopts the named-feature and explicit-deviation discipline, + but not YANG syntax, datastore semantics, or wire compatibility; YANG has no + equivalent of the ACES ordered support-strength/evidence vector. +- [Vulkan device feature negotiation](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html) + partially solves fail-closed target admission: a requested unsupported + device feature makes device creation fail. ACES adopts that admission + posture, but not Vulkan feature structs, API calls, or driver + compatibility; ACES additionally permits only policy-authorized, + provenance-bound weakening and removes the stronger claim. +- [XACML 3.0](https://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-cos01-en.html) + partially solves deny-first policy evaluation and keeps obligations tied to + the decision that produced them. ACES adopts the fail-closed separation of + capability from authorization, but not the XACML policy language, combining + algorithms, request/response format, or PDP/PEP architecture. +- [OGC API conformance classes](https://docs.ogc.org/is/20-057/20-057.html) + partially solve requirements-class-to-test traceability. ACES adopts named, + finite, falsifiable cases with explicit evidence scope and nonclaims, but + does not claim OGC conformance or promote finite cases to universal runtime + realization. + +None is a full solution. The ACES-specific addition is one governed +per-feature declaration carrying ordered strength, separate constraint, +limitation, disclosure, and evidence references, required portable contracts, +and a shared admission comparison that cannot itself grant policy authority. + ### 2.5 Retrieval surfaces (control planes, replay logs) The repo's own control-plane contract (API-403/404, `runtime-snapshot-v1`, diff --git a/implementations/python/packages/raes_backend_libvirt/manifest.py b/implementations/python/packages/raes_backend_libvirt/manifest.py index 90e4fa3dc..005cbfb7d 100644 --- a/implementations/python/packages/raes_backend_libvirt/manifest.py +++ b/implementations/python/packages/raes_backend_libvirt/manifest.py @@ -6,6 +6,7 @@ from importlib.metadata import version as distribution_version from raes_backend_protocols.capabilities import ( + PARTICIPANT_RUNTIME_POLICY_FEATURES, BackendCapabilitySet, BackendManifest, ParticipantFeatureSupport, @@ -125,6 +126,15 @@ def _participant_runtime_capabilities() -> ParticipantRuntimeCapabilities: support_level=ParticipantFeatureSupportLevel.DISCLOSED_WEAK, disclosure_refs=(disclosure_ref,), ), + *( + ParticipantFeatureSupport( + feature=feature, + support_level=ParticipantFeatureSupportLevel.UNSUPPORTED, + limitation_refs=(f"limitation:{feature}:not-realized",), + disclosure_refs=(disclosure_ref,), + ) + for feature in sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + ), ), constraints={ "simulation_disclosure": ( diff --git a/implementations/python/packages/raes_backend_protocols/capabilities.py b/implementations/python/packages/raes_backend_protocols/capabilities.py index 2639ce4a9..98551b6da 100644 --- a/implementations/python/packages/raes_backend_protocols/capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/capabilities.py @@ -15,6 +15,7 @@ _participant_capabilities.PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS ) PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE = _participant_capabilities.PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE +PARTICIPANT_RUNTIME_POLICY_FEATURES = _participant_capabilities.PARTICIPANT_RUNTIME_POLICY_FEATURES PARTICIPANT_RUNTIME_ROLE_SCOPE = _participant_capabilities.PARTICIPANT_RUNTIME_ROLE_SCOPE ParticipantFeatureSupport = _participant_capabilities.ParticipantFeatureSupport ParticipantRuntimeCapabilities = _participant_capabilities.ParticipantRuntimeCapabilities @@ -259,7 +260,9 @@ def __getattr__(name: str) -> object: if name in { "observation_capability_contract_gaps", "participant_autonomous_execution_capability_gaps", + "participant_feature_support_gaps", "participant_runtime_capability_contract_gaps", + "resolve_participant_feature_support", "require_cleanup_plan_capability", "require_time_model_capability", "time_capability_contract_gaps", diff --git a/implementations/python/packages/raes_backend_protocols/capability_admission.py b/implementations/python/packages/raes_backend_protocols/capability_admission.py index 52af278dc..98e4b8802 100644 --- a/implementations/python/packages/raes_backend_protocols/capability_admission.py +++ b/implementations/python/packages/raes_backend_protocols/capability_admission.py @@ -5,12 +5,16 @@ from collections.abc import Iterable from typing import TYPE_CHECKING, Protocol +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel + from .capabilities import ( OBSERVATION_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, + ParticipantFeatureSupport, ) if TYPE_CHECKING: @@ -56,6 +60,107 @@ def participant_runtime_capability_contract_gaps(manifest: BackendManifest) -> t return tuple(gaps) +_PARTICIPANT_FEATURE_SUPPORT_RANK = { + ParticipantFeatureSupportLevel.UNSUPPORTED: 0, + ParticipantFeatureSupportLevel.DISCLOSED_WEAK: 1, + ParticipantFeatureSupportLevel.BOUNDED: 2, + ParticipantFeatureSupportLevel.EXACT: 3, +} + + +def _participant_feature_required_contracts(feature: str) -> frozenset[str]: + for scope in ( + PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, + ): + contracts = PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[scope].get(feature) + if contracts is not None: + return contracts + return frozenset() + + +def resolve_participant_feature_support( + manifest: BackendManifest, + feature: str, + *, + required_level: ParticipantFeatureSupportLevel = ParticipantFeatureSupportLevel.EXACT, + allowed_downgrade_level: ParticipantFeatureSupportLevel | None = None, + downgrade_policy_ref: str | None = None, + downgrade_provenance_ref: str | None = None, +) -> ParticipantFeatureSupport | None: + """Resolve one required participant feature without inventing support. + + Historical API-405 behavior and interaction requirements remain + presence-based when no API-407 strength entry exists. Participant-policy + features added by issue #801 always require an explicit strength entry. + An accepted downgrade returns that manifest entry unchanged, so callers + cannot retain the stronger requested claim. + """ + + if allowed_downgrade_level is not None and (not downgrade_policy_ref or not downgrade_provenance_ref): + raise ValueError( + f"participant feature '{feature}' requires explicit downgrade authorization " + "with policy and provenance references" + ) + + capability = manifest.participant_runtime + if capability is None: + raise ValueError(f"participant feature '{feature}' requires participant runtime capabilities") + 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: + return None + raise ValueError(f"participant feature '{feature}' has no explicit support declaration") + if declaration.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED: + raise ValueError(f"participant feature '{feature}' is explicitly unsupported") + + missing_contracts = sorted(_participant_feature_required_contracts(feature) - manifest.supported_contract_versions) + if missing_contracts: + 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: + raise ValueError(f"participant feature '{feature}' has no conformance evidence") + + declared_rank = _PARTICIPANT_FEATURE_SUPPORT_RANK[declaration.support_level] + required_rank = _PARTICIPANT_FEATURE_SUPPORT_RANK[required_level] + if declared_rank >= required_rank: + return declaration + if allowed_downgrade_level is None: + raise ValueError( + f"participant feature '{feature}' requires {required_level.value} support; " + f"backend declares {declaration.support_level.value}" + ) + if declaration.support_level != allowed_downgrade_level: + raise ValueError( + f"participant feature '{feature}' authorized downgrade is " + f"{allowed_downgrade_level.value}; backend declares {declaration.support_level.value}" + ) + return declaration + + +def participant_feature_support_gaps( + manifest: BackendManifest, + features: Iterable[str], + *, + required_level: ParticipantFeatureSupportLevel = ParticipantFeatureSupportLevel.EXACT, +) -> tuple[str, ...]: + """Return fail-closed gaps for required participant semantic features.""" + + gaps: list[str] = [] + for feature in sorted(set(features)): + try: + resolve_participant_feature_support( + manifest, + feature, + required_level=required_level, + ) + except ValueError as exc: + gaps.append(str(exc)) + return tuple(gaps) + + def _autonomous_limit_gaps( capability: ParticipantRuntimeCapabilities, policies: tuple[AutonomousExecutionPolicy, ...], diff --git a/implementations/python/packages/raes_backend_protocols/manifest.py b/implementations/python/packages/raes_backend_protocols/manifest.py index ff9545895..afb404bb4 100644 --- a/implementations/python/packages/raes_backend_protocols/manifest.py +++ b/implementations/python/packages/raes_backend_protocols/manifest.py @@ -159,7 +159,9 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod feature=entry.feature, support_level=entry.support_level, constraint_refs=list(entry.constraint_refs), + limitation_refs=list(entry.limitation_refs), disclosure_refs=list(entry.disclosure_refs), + evidence_refs=list(entry.evidence_refs), ) for entry in manifest.participant_runtime.feature_support ], @@ -319,7 +321,9 @@ def _participant_feature_support_from_model(model: ParticipantFeatureSupportMode feature=model.feature, support_level=model.support_level, constraint_refs=tuple(model.constraint_refs), + limitation_refs=tuple(model.limitation_refs), disclosure_refs=tuple(model.disclosure_refs), + evidence_refs=tuple(model.evidence_refs), ) diff --git a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py index aceffd091..10057c2e5 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -4,62 +4,14 @@ from raes_contracts.addressing import require_compiled_address from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_scope_values -from raes_contracts.vocabulary import ParticipantFeatureSupportLevel - -PARTICIPANT_RUNTIME_ROLE_SCOPE = "capabilities.participant_runtime.supported_participant_roles" -PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE = "capabilities.participant_runtime.supported_behavior_features" -PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE = "capabilities.participant_runtime.supported_interaction_features" - -_PARTICIPANT_EPISODE_CONTRACTS = frozenset( - { - "participant-episode-state-envelope-v1", - "participant-episode-history-event-stream-v1", - "runtime-snapshot-v1", - } -) -_PARTICIPANT_BEHAVIOR_CONTRACTS = frozenset( - { - "participant-behavior-history-event-stream-v1", - "runtime-snapshot-v1", - } -) -_PARTICIPANT_INTERACTION_CONTRACTS = frozenset( - { - "participant-behavior-history-event-stream-v1", - "participant-shared-state-record-v1", - "participant-joint-action-record-v1", - "participant-time-management-context-v1", - "runtime-snapshot-v1", - } +from raes_contracts.manifest_authority import ( + PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_POLICY_FEATURES, + PARTICIPANT_RUNTIME_ROLE_SCOPE, ) - -PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS = { - PARTICIPANT_RUNTIME_ROLE_SCOPE: { - "blue": _PARTICIPANT_EPISODE_CONTRACTS, - "green": _PARTICIPANT_EPISODE_CONTRACTS, - "red": _PARTICIPANT_EPISODE_CONTRACTS, - "white": _PARTICIPANT_EPISODE_CONTRACTS, - }, - PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE: { - "action_contracts": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "autonomous_execution": _PARTICIPANT_INTERACTION_CONTRACTS, - "attribution_support": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "behavior_history": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "effects": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "failure_classes": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "observation_boundaries": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "outcome_interpretation": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "preconditions": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "state_transitions": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "temporal_contracts": _PARTICIPANT_BEHAVIOR_CONTRACTS, - }, - PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE: { - "contention": _PARTICIPANT_INTERACTION_CONTRACTS, - "coordination": _PARTICIPANT_INTERACTION_CONTRACTS, - "interference": _PARTICIPANT_INTERACTION_CONTRACTS, - "shared_state_change": _PARTICIPANT_INTERACTION_CONTRACTS, - }, -} +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel def _validate_unique_non_empty_strings(field_name: str, values: tuple[str, ...]) -> None: @@ -92,7 +44,9 @@ class ParticipantFeatureSupport: feature: str support_level: ParticipantFeatureSupportLevel | str constraint_refs: tuple[str, ...] = () + limitation_refs: tuple[str, ...] = () disclosure_refs: tuple[str, ...] = () + evidence_refs: tuple[str, ...] = () def __post_init__(self) -> None: if not self.feature.strip(): @@ -107,16 +61,35 @@ def __post_init__(self) -> None: except ValueError as exc: raise ValueError("ParticipantFeatureSupport.support_level must be a valid support level") from exc constraint_refs = tuple(self.constraint_refs) + limitation_refs = tuple(self.limitation_refs) disclosure_refs = tuple(self.disclosure_refs) + evidence_refs = tuple(self.evidence_refs) _validate_unique_non_empty_strings("ParticipantFeatureSupport.constraint_refs", constraint_refs) + _validate_unique_non_empty_strings("ParticipantFeatureSupport.limitation_refs", limitation_refs) _validate_unique_non_empty_strings("ParticipantFeatureSupport.disclosure_refs", disclosure_refs) + _validate_unique_non_empty_strings("ParticipantFeatureSupport.evidence_refs", evidence_refs) if support_level != ParticipantFeatureSupportLevel.EXACT and not disclosure_refs: raise ValueError( "ParticipantFeatureSupport disclosure_refs must be non-empty when support_level is below exact" ) + if self.feature in PARTICIPANT_RUNTIME_POLICY_FEATURES: + if support_level != ParticipantFeatureSupportLevel.EXACT and not limitation_refs: + raise ValueError( + "ParticipantFeatureSupport limitation_refs must be non-empty for below-exact policy support" + ) + if support_level == ParticipantFeatureSupportLevel.BOUNDED and not constraint_refs: + raise ValueError( + "ParticipantFeatureSupport constraint_refs must be non-empty for bounded policy support" + ) + if support_level != ParticipantFeatureSupportLevel.UNSUPPORTED and not evidence_refs: + raise ValueError( + "ParticipantFeatureSupport evidence_refs must be non-empty for positive policy support" + ) object.__setattr__(self, "support_level", support_level) object.__setattr__(self, "constraint_refs", constraint_refs) + object.__setattr__(self, "limitation_refs", limitation_refs) object.__setattr__(self, "disclosure_refs", disclosure_refs) + object.__setattr__(self, "evidence_refs", evidence_refs) @dataclass(frozen=True) @@ -180,6 +153,14 @@ def _validate_feature_support(self, feature_support: tuple[ParticipantFeatureSup feature_names = tuple(entry.feature for entry in feature_support) _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) + ) + if missing_policy_declarations: + raise ValueError( + "ParticipantRuntimeCapabilities supported participant policy features require explicit " + f"feature_support declarations: {', '.join(missing_policy_declarations)}" + ) for entry in feature_support: if ( entry.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED @@ -188,6 +169,15 @@ def _validate_feature_support(self, feature_support: tuple[ParticipantFeatureSup raise ValueError( "ParticipantRuntimeCapabilities.feature_support cannot declare a supported feature unsupported" ) + if ( + entry.feature in PARTICIPANT_RUNTIME_POLICY_FEATURES + and entry.support_level != ParticipantFeatureSupportLevel.UNSUPPORTED + and entry.feature not in supported_features + ): + raise ValueError( + "ParticipantRuntimeCapabilities positive support for a participant policy feature " + "requires the feature in supported_behavior_features" + ) def _validate_autonomous_execution(self) -> None: declares_autonomous = "autonomous_execution" in self.supported_behavior_features @@ -243,6 +233,7 @@ def _has_autonomous_configuration(self) -> bool: "PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE", "PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS", "PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE", + "PARTICIPANT_RUNTIME_POLICY_FEATURES", "PARTICIPANT_RUNTIME_ROLE_SCOPE", "ParticipantFeatureSupport", "ParticipantRuntimeCapabilities", diff --git a/implementations/python/packages/raes_backend_stubs/manifest.py b/implementations/python/packages/raes_backend_stubs/manifest.py index 6dd2f9258..64864be7d 100644 --- a/implementations/python/packages/raes_backend_stubs/manifest.py +++ b/implementations/python/packages/raes_backend_stubs/manifest.py @@ -22,6 +22,7 @@ PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, TIME_CAPABILITY_REQUIRED_CONTRACTS, BackendCapabilitySet, @@ -30,6 +31,7 @@ EvaluatorCapabilities, ObservationCapabilities, OrchestratorCapabilities, + ParticipantFeatureSupport, ParticipantRuntimeCapabilities, ProvisionerCapabilities, TimeCapabilities, @@ -38,7 +40,7 @@ ) from raes_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration from raes_contracts.manifest_authority import BACKEND_SUPPORTED_CONTRACT_IDS -from raes_contracts.vocabulary import RealizationSupportMode +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel, RealizationSupportMode REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS = frozenset(BACKEND_SUPPORTED_CONTRACT_IDS) - { "experiment-binding-descriptors-v1", @@ -47,9 +49,11 @@ REFERENCE_PARTICIPANT_ROLES = frozenset( PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_ROLE_SCOPE] ) -REFERENCE_PARTICIPANT_BEHAVIOR_FEATURES = frozenset( - PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE] -) - {"autonomous_execution"} +REFERENCE_PARTICIPANT_BEHAVIOR_FEATURES = ( + frozenset(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE]) + - {"autonomous_execution"} + - PARTICIPANT_RUNTIME_POLICY_FEATURES +) REFERENCE_PARTICIPANT_INTERACTION_FEATURES = frozenset( PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE] ) @@ -234,6 +238,15 @@ def _stub_participant_runtime() -> ParticipantRuntimeCapabilities: supported_participant_roles=REFERENCE_PARTICIPANT_ROLES, supported_behavior_features=REFERENCE_PARTICIPANT_BEHAVIOR_FEATURES, supported_interaction_features=REFERENCE_PARTICIPANT_INTERACTION_FEATURES, + feature_support=tuple( + 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_conformance/conformance/report.py b/implementations/python/packages/raes_conformance/conformance/report.py index 7a810e73e..db9af46ac 100644 --- a/implementations/python/packages/raes_conformance/conformance/report.py +++ b/implementations/python/packages/raes_conformance/conformance/report.py @@ -38,6 +38,12 @@ class ConformanceCaseResult: cleanup_verified: bool | None = None residual_state: tuple[str, ...] = () evidence_refs: tuple[str, ...] = () + capability_feature: str | None = None + declared_support_level: str | None = None + effective_support_level: str | None = None + finite_scope: str | None = None + limitations: tuple[str, ...] = () + explicit_non_claims: tuple[str, ...] = () def __post_init__(self) -> None: """Keep the closed outcome vocabulary aligned with the gating boolean.""" @@ -104,6 +110,12 @@ def backend_conformance_report_payload(report: BackendConformanceReport) -> dict "cleanup_verified": case.cleanup_verified, "residual_state": list(case.residual_state), "evidence_refs": list(case.evidence_refs), + "capability_feature": case.capability_feature, + "declared_support_level": case.declared_support_level, + "effective_support_level": case.effective_support_level, + "finite_scope": case.finite_scope, + "limitations": list(case.limitations), + "explicit_non_claims": list(case.explicit_non_claims), "diagnostics": [_diagnostic_payload(diag) for diag in case.diagnostics], } for case in report.cases diff --git a/implementations/python/packages/raes_conformance/conformance/target.py b/implementations/python/packages/raes_conformance/conformance/target.py index 49745ebaf..068d169c7 100644 --- a/implementations/python/packages/raes_conformance/conformance/target.py +++ b/implementations/python/packages/raes_conformance/conformance/target.py @@ -7,13 +7,16 @@ from typing import Any from raes_backend_protocols.capabilities import ( + PARTICIPANT_RUNTIME_POLICY_FEATURES, BackendManifest, observation_capability_contract_gaps, participant_runtime_capability_contract_gaps, + resolve_participant_feature_support, time_capability_contract_gaps, ) from raes_contracts.diagnostics import Diagnostic from raes_contracts.realization_envelope import BackendRealizationEnvelopeModel +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel from raes_processor.reference import ScenarioInput from raes_runtime.registry import RuntimeTarget @@ -210,6 +213,67 @@ def _gap_diagnostics( return diagnostics +def _participant_feature_cases( + target: RuntimeTarget, + profile: BackendProfileSelector, +) -> tuple[ConformanceCaseResult, ...]: + """Project finite API-407 manifest declarations into the existing report.""" + + capability = target.manifest.participant_runtime + if capability is None: + return () + profile_id = _to_profile_id(profile) + cases: list[ConformanceCaseResult] = [] + for entry in capability.feature_support: + if entry.feature not in PARTICIPANT_RUNTIME_POLICY_FEATURES: + continue + diagnostics: tuple[Diagnostic, ...] = () + effective_support_level: str | None = entry.support_level.value + if entry.support_level != ParticipantFeatureSupportLevel.UNSUPPORTED: + try: + resolve_participant_feature_support( + target.manifest, + entry.feature, + required_level=entry.support_level, + ) + except ValueError as exc: + effective_support_level = None + diagnostics = ( + _diagnostic( + "conformance.participant-feature-support-invalid", + entry.feature, + str(exc), + ), + ) + cases.append( + ConformanceCaseResult( + name=f"participant-feature-support-{entry.feature}", + contract_name="backend-manifest-v2", + valid=True, + passed=not diagnostics, + diagnostics=diagnostics, + capability_feature=entry.feature, + declared_support_level=entry.support_level.value, + effective_support_level=effective_support_level, + finite_scope=( + f"Static manifest declaration for target {target.name!r} under profile " + f"{profile_id!r}; no unexecuted participant behavior is quantified." + ), + evidence_refs=entry.evidence_refs, + limitations=( + *entry.limitation_refs, + "Manifest validity and finite conformance do not establish runtime realization.", + ), + explicit_non_claims=( + "Does not establish native participant-policy enforcement.", + "Does not establish noninterference, trace equivalence, or bisimulation.", + "Does not establish behavior outside the named target, profile, corpus, and cases.", + ), + ) + ) + return tuple(cases) + + def _known_profile_report( target: RuntimeTarget, profile: BackendProfileSelector, @@ -235,6 +299,7 @@ def _known_profile_report( ) if target.manifest.realization_envelope is not None: adapter_cases = adapter_cases[:1] + participant_feature_cases = _participant_feature_cases(target, profile) target_cases = tuple(replace(case, execution_basis=options.execution_basis.value) for case in adapter_cases) realization_run = run_realization_conformance( target, @@ -245,12 +310,12 @@ def _known_profile_report( native_conformance=options.native_conformance, ) realization_cases = tuple(_realization_case_result(case) for case in realization_run.cases) - cases = (*fixture_report.cases, *target_cases, *realization_cases) + cases = (*fixture_report.cases, *participant_feature_cases, *target_cases, *realization_cases) passed = ( fixture_report.passed and not contract_gaps and not capability_gaps - and all(case.passed for case in (*target_cases, *realization_cases)) + and all(case.passed for case in (*participant_feature_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 077c8ac9f..0a5a9b8a8 100644 --- a/implementations/python/packages/raes_conformance/conformance/validators.py +++ b/implementations/python/packages/raes_conformance/conformance/validators.py @@ -22,6 +22,7 @@ OrchestrationPlanModel, ParticipantBehaviorHistoryEventModel, ParticipantConfigurationResultModel, + ParticipantControlOccurrenceModel, ParticipantCrossingOccurrenceModel, ParticipantEpisodeHistoryEventModel, ParticipantEpisodeStateModel, @@ -66,6 +67,7 @@ "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_validate, "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_validate, "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_validate, + "participant-control-occurrence-v1": ParticipantControlOccurrenceModel.model_validate, "participant-crossing-occurrence-v1": ParticipantCrossingOccurrenceModel.model_validate, "experiment-capture-spec-v1": ExperimentCaptureSpecModel.model_validate, "experiment-evidence-record-v1": ExperimentEvidenceRecordModel.model_validate, diff --git a/implementations/python/packages/raes_contracts/contracts/feature_support.py b/implementations/python/packages/raes_contracts/contracts/feature_support.py new file mode 100644 index 000000000..7b3177612 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/feature_support.py @@ -0,0 +1,147 @@ +"""Participant feature-support declaration contracts.""" + +from __future__ import annotations + +import re + +from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from ..manifest_authority import PARTICIPANT_RUNTIME_POLICY_FEATURES +from ..vocabulary import ParticipantFeatureSupportLevel +from .base import ContractModel, NonEmptyString +from .validators import _validate_unique_string_values + +_PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS = ( + "participant-runtime-behavior-features", + "participant-runtime-interaction-features", +) + + +def _validate_participant_feature_support_term(feature: str) -> None: + from ..controlled_vocabularies import load_controlled_vocabulary_catalog + + catalog = load_controlled_vocabulary_catalog() + for vocabulary_id in _PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS: + definition = catalog.vocabularies[vocabulary_id] + if feature in definition.terms: + return + if definition.extension_pattern is not None and re.fullmatch(definition.extension_pattern, feature): + return + joined = ", ".join(_PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS) + raise ValueError( + f"feature_support feature '{feature}' is not a governed term of {joined} " + "and does not match the governed extension pattern" + ) + + +class ParticipantFeatureSupportModel(ContractModel): + """API-407 per-feature participant runtime support declaration.""" + + feature: NonEmptyString + support_level: ParticipantFeatureSupportLevel + constraint_refs: list[NonEmptyString] = Field(default_factory=list) + limitation_refs: list[NonEmptyString] = Field(default_factory=list) + disclosure_refs: list[NonEmptyString] = Field(default_factory=list) + evidence_refs: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_feature_support_declaration(self) -> ParticipantFeatureSupportModel: + _validate_participant_feature_support_term(self.feature) + _validate_unique_string_values("constraint_refs", self.constraint_refs) + _validate_unique_string_values("limitation_refs", self.limitation_refs) + _validate_unique_string_values("disclosure_refs", self.disclosure_refs) + _validate_unique_string_values("evidence_refs", self.evidence_refs) + if self.support_level != ParticipantFeatureSupportLevel.EXACT and not self.disclosure_refs: + raise ValueError( + 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.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: + raise ValueError( + f"feature_support entry '{self.feature}' with bounded support must carry constraint_refs" + ) + if self.support_level != ParticipantFeatureSupportLevel.UNSUPPORTED and not self.evidence_refs: + raise ValueError( + f"feature_support entry '{self.feature}' with positive support must carry evidence_refs" + ) + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + below_exact = [ + level.value for level in ParticipantFeatureSupportLevel if level != ParticipantFeatureSupportLevel.EXACT + ] + json_schema.setdefault("allOf", []).append( + { + "if": { + "properties": {"support_level": {"enum": below_exact}}, + "required": ["support_level"], + }, + "then": { + "required": ["disclosure_refs"], + "properties": {"disclosure_refs": {"minItems": 1}}, + }, + } + ) + policy_features = sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + json_schema["allOf"].extend( + [ + { + "if": { + "properties": { + "feature": {"enum": policy_features}, + "support_level": {"enum": below_exact}, + }, + "required": ["feature", "support_level"], + }, + "then": { + "required": ["limitation_refs"], + "properties": {"limitation_refs": {"minItems": 1}}, + }, + }, + { + "if": { + "properties": { + "feature": {"enum": policy_features}, + "support_level": {"const": ParticipantFeatureSupportLevel.BOUNDED.value}, + }, + "required": ["feature", "support_level"], + }, + "then": { + "required": ["constraint_refs"], + "properties": {"constraint_refs": {"minItems": 1}}, + }, + }, + { + "if": { + "properties": { + "feature": {"enum": policy_features}, + "support_level": { + "enum": [ + ParticipantFeatureSupportLevel.DISCLOSED_WEAK.value, + ParticipantFeatureSupportLevel.BOUNDED.value, + ParticipantFeatureSupportLevel.EXACT.value, + ] + }, + }, + "required": ["feature", "support_level"], + }, + "then": { + "required": ["evidence_refs"], + "properties": {"evidence_refs": {"minItems": 1}}, + }, + }, + ] + ) + return json_schema diff --git a/implementations/python/packages/raes_contracts/contracts/manifests.py b/implementations/python/packages/raes_contracts/contracts/manifests.py index 7edf4add3..eca5f98c8 100644 --- a/implementations/python/packages/raes_contracts/contracts/manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/manifests.py @@ -2,7 +2,6 @@ from __future__ import annotations -import re from typing import Literal from pydantic import Field, GetJsonSchemaHandler, SerializerFunctionWrapHandler, model_serializer, model_validator @@ -11,6 +10,7 @@ from ..addressing import require_compiled_address from ..manifest_authority import ( + PARTICIPANT_RUNTIME_POLICY_FEATURES, PROCESSOR_SUPPORTED_CONTRACT_IDS, PROCESSOR_SUPPORTED_SDL_VERSION_IDS, validate_backend_supported_contract_versions, @@ -28,6 +28,7 @@ ProvisionerCapabilitiesModel, ) from .experiment_bindings import ConfigurationTargetRegistryModel +from .feature_support import ParticipantFeatureSupportModel from .trial_cleanup import CleanupActionKind from .validators import ( _validate_canonical_concept_bindings, @@ -67,75 +68,6 @@ def __get_pydantic_json_schema__( return json_schema -_PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS = ( - "participant-runtime-behavior-features", - "participant-runtime-interaction-features", -) - - -def _validate_participant_feature_support_term(feature: str) -> None: - from ..controlled_vocabularies import load_controlled_vocabulary_catalog - - catalog = load_controlled_vocabulary_catalog() - for vocabulary_id in _PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS: - definition = catalog.vocabularies[vocabulary_id] - if feature in definition.terms: - return - if definition.extension_pattern is not None and re.fullmatch(definition.extension_pattern, feature): - return - joined = ", ".join(_PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS) - raise ValueError( - f"feature_support feature '{feature}' is not a governed term of {joined} " - "and does not match the governed extension pattern" - ) - - -class ParticipantFeatureSupportModel(ContractModel): - """API-407 per-feature participant runtime support declaration.""" - - feature: NonEmptyString - support_level: ParticipantFeatureSupportLevel - constraint_refs: list[NonEmptyString] = Field(default_factory=list) - disclosure_refs: list[NonEmptyString] = Field(default_factory=list) - - @model_validator(mode="after") - def _validate_feature_support_declaration(self) -> ParticipantFeatureSupportModel: - _validate_participant_feature_support_term(self.feature) - _validate_unique_string_values("constraint_refs", self.constraint_refs) - _validate_unique_string_values("disclosure_refs", self.disclosure_refs) - if self.support_level != ParticipantFeatureSupportLevel.EXACT and not self.disclosure_refs: - raise ValueError( - 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" - ) - return self - - @classmethod - def __get_pydantic_json_schema__( - cls, - core_schema: CoreSchema, - handler: GetJsonSchemaHandler, - ) -> JsonSchemaValue: - json_schema = handler(core_schema) - json_schema = handler.resolve_ref_schema(json_schema) - below_exact = [ - level.value for level in ParticipantFeatureSupportLevel if level != ParticipantFeatureSupportLevel.EXACT - ] - json_schema.setdefault("allOf", []).append( - { - "if": { - "properties": {"support_level": {"enum": below_exact}}, - "required": ["support_level"], - }, - "then": { - "required": ["disclosure_refs"], - "properties": {"disclosure_refs": {"minItems": 1}}, - }, - } - ) - return json_schema - - class ParticipantRuntimeCapabilitiesModel(ContractModel): """Participant-episode lifecycle capability block (RUN-311). @@ -216,6 +148,15 @@ def _validate_api_407_feature_support(self) -> ParticipantRuntimeCapabilitiesMod 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 + ) + if missing_policy_declarations: + raise ValueError( + "supported participant policy features require explicit feature_support declarations: " + + ", ".join(missing_policy_declarations) + ) for entry in self.feature_support: declared_unsupported = entry.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED if declared_unsupported and entry.feature in supported_features: @@ -223,6 +164,15 @@ def _validate_supported_feature_levels(self) -> None: f"feature_support entry '{entry.feature}' declares support_level 'unsupported' but the " "feature is declared in supported_behavior_features or supported_interaction_features" ) + if ( + entry.feature in PARTICIPANT_RUNTIME_POLICY_FEATURES + and not declared_unsupported + and entry.feature not in supported_features + ): + raise ValueError( + f"feature_support entry '{entry.feature}' declares positive support but the feature " + "is absent from supported_behavior_features" + ) def _validate_autonomous_configuration(self) -> None: declares_autonomous = "autonomous_execution" in self.supported_behavior_features diff --git a/implementations/python/packages/raes_contracts/contracts/participant_manifests.py b/implementations/python/packages/raes_contracts/contracts/participant_manifests.py index 404e8e59e..33a27a042 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_manifests.py @@ -11,6 +11,9 @@ from ..manifest_authority import ( BACKEND_SUPPORTED_CONTRACT_IDS, PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS, + PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_POLICY_FEATURES, validate_backend_supported_contract_versions, validate_participant_implementation_supported_contract_versions, validate_participant_supported_contract_versions, @@ -64,9 +67,27 @@ def _validate_unique_binding_scopes(self) -> BackendManifestV2Model: self._validate_realization_envelope_contract() self._validate_cleanup_contracts() self._validate_time_contracts() + self._validate_participant_policy_contracts() self._validate_concept_bindings() return self + def _validate_participant_policy_contracts(self) -> None: + participant_runtime = self.capabilities.participant_runtime + if participant_runtime is None: + return + declared_contracts = set(self.supported_contract_versions) + required_by_feature = PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[ + 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": + continue + missing = sorted(required_by_feature[entry.feature] - declared_contracts) + if missing: + raise ValueError( + f"feature_support entry '{entry.feature}' is missing required contracts: {', '.join(missing)}" + ) + def _validate_realization_envelope_contract(self) -> None: envelope_contract_declared = "realization-envelope-v1" in self.supported_contract_versions if self.realization_envelope is not None and not envelope_contract_declared: diff --git a/implementations/python/packages/raes_contracts/manifest_authority.py b/implementations/python/packages/raes_contracts/manifest_authority.py index ac265c7d8..f5e523afe 100644 --- a/implementations/python/packages/raes_contracts/manifest_authority.py +++ b/implementations/python/packages/raes_contracts/manifest_authority.py @@ -53,6 +53,8 @@ "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-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", @@ -70,6 +72,86 @@ "realized-time-model-v1", ) +PARTICIPANT_RUNTIME_ROLE_SCOPE = "capabilities.participant_runtime.supported_participant_roles" +PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE = "capabilities.participant_runtime.supported_behavior_features" +PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE = "capabilities.participant_runtime.supported_interaction_features" + +PARTICIPANT_RUNTIME_POLICY_FEATURES = frozenset( + { + "participant_declassification", + "participant_directed_inject_delivery", + "participant_egress_projection", + "participant_ingress_admission", + "participant_intervention", + "participant_transformation", + } +) + +_PARTICIPANT_EPISODE_CONTRACTS = frozenset( + { + "participant-episode-state-envelope-v1", + "participant-episode-history-event-stream-v1", + "runtime-snapshot-v1", + } +) +_PARTICIPANT_BEHAVIOR_CONTRACTS = frozenset( + { + "participant-behavior-history-event-stream-v1", + "runtime-snapshot-v1", + } +) +_PARTICIPANT_INTERACTION_CONTRACTS = frozenset( + { + "participant-behavior-history-event-stream-v1", + "participant-shared-state-record-v1", + "participant-joint-action-record-v1", + "participant-time-management-context-v1", + "runtime-snapshot-v1", + } +) + +PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS = { + PARTICIPANT_RUNTIME_ROLE_SCOPE: { + "blue": _PARTICIPANT_EPISODE_CONTRACTS, + "green": _PARTICIPANT_EPISODE_CONTRACTS, + "red": _PARTICIPANT_EPISODE_CONTRACTS, + "white": _PARTICIPANT_EPISODE_CONTRACTS, + }, + PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE: { + "action_contracts": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "autonomous_execution": _PARTICIPANT_INTERACTION_CONTRACTS, + "attribution_support": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "behavior_history": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "effects": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "failure_classes": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "observation_boundaries": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "outcome_interpretation": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "participant_declassification": frozenset({"participant-crossing-occurrence-v1"}), + "participant_directed_inject_delivery": frozenset( + {"orchestration-plan-v1", "participant-crossing-occurrence-v1"} + ), + "participant_egress_projection": frozenset( + {"participant-observation-envelope-v1", "participant-crossing-occurrence-v1"} + ), + "participant_ingress_admission": frozenset( + {"participant-control-occurrence-v1", "participant-crossing-occurrence-v1"} + ), + "participant_intervention": frozenset( + {"participant-control-occurrence-v1", "participant-crossing-occurrence-v1"} + ), + "participant_transformation": frozenset({"participant-crossing-occurrence-v1"}), + "preconditions": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "state_transitions": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "temporal_contracts": _PARTICIPANT_BEHAVIOR_CONTRACTS, + }, + PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE: { + "contention": _PARTICIPANT_INTERACTION_CONTRACTS, + "coordination": _PARTICIPANT_INTERACTION_CONTRACTS, + "interference": _PARTICIPANT_INTERACTION_CONTRACTS, + "shared_state_change": _PARTICIPANT_INTERACTION_CONTRACTS, + }, +} + PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS = ( "participant-implementation-manifest-v1", "participant-implementation-provenance-v1", diff --git a/implementations/python/packages/raes_processor/planner/core.py b/implementations/python/packages/raes_processor/planner/core.py index 7b9ceaf80..0eb1f0ae5 100644 --- a/implementations/python/packages/raes_processor/planner/core.py +++ b/implementations/python/packages/raes_processor/planner/core.py @@ -6,6 +6,7 @@ from raes_backend_protocols.capabilities import BackendManifest from raes_backend_protocols.capability_admission import ( participant_autonomous_execution_capability_gaps, + participant_feature_support_gaps, time_model_capability_gaps, ) from raes_backend_protocols.domain_topology import domain_topology_plan_diagnostics @@ -50,12 +51,11 @@ def _participant_execution_diagnostics( model: RuntimeModel, manifest: BackendManifest, ) -> list[Diagnostic]: - specifications = tuple( - specification - for specification in model.behavior_specifications.values() - if specification.autonomous_execution is not None + specifications = tuple(model.behavior_specifications.values()) + autonomous_specifications = tuple( + specification for specification in specifications if specification.autonomous_execution is not None ) - policies = tuple(specification.autonomous_execution for specification in specifications) + policies = tuple(specification.autonomous_execution for specification in autonomous_specifications) diagnostics = [ Diagnostic( code="participant.autonomous-execution-unsupported", @@ -65,20 +65,17 @@ def _participant_execution_diagnostics( ) for gap in participant_autonomous_execution_capability_gaps(manifest, policies, model.time_model) ] - capability = manifest.participant_runtime - supported_features = ( - capability.supported_behavior_features | capability.supported_interaction_features - if capability is not None - else frozenset() - ) for specification in specifications: - for feature in sorted(set(specification.backend_feature_support_refs) - supported_features): + for gap in participant_feature_support_gaps( + manifest, + specification.backend_feature_support_refs, + ): diagnostics.append( Diagnostic( - code="participant.autonomous-feature-unsupported", + code="participant.feature-support-insufficient", domain="participant", address=specification.address, - message=f"Backend does not support required participant feature '{feature}'.", + message=gap, ) ) return diagnostics diff --git a/implementations/python/packages/raes_reference_backend/manifest.py b/implementations/python/packages/raes_reference_backend/manifest.py index 27d05e091..efe132034 100644 --- a/implementations/python/packages/raes_reference_backend/manifest.py +++ b/implementations/python/packages/raes_reference_backend/manifest.py @@ -19,6 +19,7 @@ PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, TIME_CAPABILITY_REQUIRED_CONTRACTS, BackendCapabilitySet, @@ -27,6 +28,7 @@ EvaluatorCapabilities, ObservationCapabilities, OrchestratorCapabilities, + ParticipantFeatureSupport, ParticipantRuntimeCapabilities, ProvisionerCapabilities, TimeCapabilities, @@ -35,7 +37,7 @@ ) from raes_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration from raes_contracts.manifest_authority import BACKEND_SUPPORTED_CONTRACT_IDS -from raes_contracts.vocabulary import RealizationSupportMode +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel, RealizationSupportMode REFERENCE_BACKEND_NAME = "reference-emulation" REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS = frozenset( @@ -46,9 +48,11 @@ _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_BEHAVIOR_FEATURES = ( + frozenset(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE]) + - {"autonomous_execution"} + - PARTICIPANT_RUNTIME_POLICY_FEATURES +) _PARTICIPANT_INTERACTION_FEATURES = frozenset( PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE] ) @@ -216,6 +220,15 @@ def _capabilities(*, with_time: bool) -> BackendCapabilitySet: supported_participant_roles=_PARTICIPANT_ROLES, supported_behavior_features=_PARTICIPANT_BEHAVIOR_FEATURES, supported_interaction_features=_PARTICIPANT_INTERACTION_FEATURES, + feature_support=tuple( + 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) + ), ), observation=ObservationCapabilities( name="reference-emulation-observation", diff --git a/implementations/python/tests/test_backend_manifest.py b/implementations/python/tests/test_backend_manifest.py index 836d8ad84..47a1dd4ca 100644 --- a/implementations/python/tests/test_backend_manifest.py +++ b/implementations/python/tests/test_backend_manifest.py @@ -15,6 +15,7 @@ PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, BackendManifest, ObservationCapabilities, @@ -23,7 +24,9 @@ ParticipantRuntimeCapabilities, ProvisionerCapabilities, observation_capability_contract_gaps, + participant_feature_support_gaps, participant_runtime_capability_contract_gaps, + resolve_participant_feature_support, ) from raes_backend_protocols.manifest import backend_manifest_payload from raes_backend_stubs.stubs import create_stub_manifest @@ -156,7 +159,11 @@ def test_backend_manifest_v2_declares_participant_capability_dimensions(): "interference", "shared_state_change", ] - assert participant_runtime["feature_support"] == [] + assert {entry["feature"]: entry["support_level"] for entry in participant_runtime["feature_support"]} == { + feature: "unsupported" for feature in sorted(PARTICIPANT_RUNTIME_POLICY_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"]) assert participant_runtime["supports_autonomous_execution"] is False assert participant_runtime["supported_autonomous_action_contracts"] == [] assert participant_runtime["supported_autonomous_observation_boundaries"] == [] @@ -389,13 +396,17 @@ def test_backend_manifest_payload_renders_api_407_feature_support_entries(): "feature": "behavior_history", "support_level": "bounded", "constraint_refs": ["constraints.behavior-history.retention-window"], + "limitation_refs": [], "disclosure_refs": ["disclosures.behavior-history.bounded.v1"], + "evidence_refs": [], }, { "feature": "coordination", "support_level": "exact", "constraint_refs": [], + "limitation_refs": [], "disclosure_refs": [], + "evidence_refs": [], }, ] BackendManifestV2Model.model_validate(payload) @@ -423,6 +434,9 @@ def test_participant_runtime_capability_evidence_covers_standard_vocabularies(): set(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE]) == terms_by_scope[PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE] ) + assert terms_by_scope[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE] >= PARTICIPANT_RUNTIME_POLICY_FEATURES + for feature in PARTICIPANT_RUNTIME_POLICY_FEATURES: + assert PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE][feature] def test_observation_capability_evidence_covers_standard_vocabularies(): @@ -489,15 +503,265 @@ def test_backend_manifest_v2_accepts_feature_support_declarations(): assert [entry.feature for entry in feature_support] == [ "behavior_history", "coordination", + "participant_transformation", "x-acme:custom-feature", ] assert feature_support[0].support_level == ParticipantFeatureSupportLevel.BOUNDED assert feature_support[0].disclosure_refs == ["disclosures.behavior-history.bounded.v1"] - assert feature_support[2].support_level == ParticipantFeatureSupportLevel.DISCLOSED_WEAK + assert feature_support[2].evidence_refs == ["conformance:participant-transformation:bounded-case-1"] + assert feature_support[3].support_level == ParticipantFeatureSupportLevel.DISCLOSED_WEAK + + +def test_backend_manifest_v2_accepts_evidence_backed_participant_policy_support(): + payload = _stub_payload_with_feature_support( + [ + { + "feature": "participant_ingress_admission", + "support_level": "exact", + "constraint_refs": [], + "limitation_refs": [], + "disclosure_refs": [], + "evidence_refs": ["conformance:participant-ingress-admission:case-1"], + } + ] + ) + participant_runtime = payload["capabilities"]["participant_runtime"] + participant_runtime["supported_behavior_features"].append("participant_ingress_admission") + + model = BackendManifestV2Model.model_validate(payload) + + assert model.capabilities.participant_runtime is not None + declaration = model.capabilities.participant_runtime.feature_support[0] + assert declaration.feature == "participant_ingress_admission" + assert declaration.evidence_refs == ["conformance:participant-ingress-admission:case-1"] + + +def test_backend_manifest_v2_rejects_positive_participant_policy_support_without_evidence(): + payload = _stub_payload_with_feature_support( + [ + { + "feature": "participant_ingress_admission", + "support_level": "exact", + "constraint_refs": [], + "limitation_refs": [], + "disclosure_refs": [], + "evidence_refs": [], + } + ] + ) + payload["capabilities"]["participant_runtime"]["supported_behavior_features"].append( + "participant_ingress_admission" + ) + + with pytest.raises(ValidationError, match="evidence_refs"): + BackendManifestV2Model.model_validate(payload) + + +def test_backend_manifest_v2_rejects_bounded_policy_support_without_constraints(): + payload = _stub_payload_with_feature_support( + [ + { + "feature": "participant_transformation", + "support_level": "bounded", + "constraint_refs": [], + "limitation_refs": ["limitation:participant-transformation:finite-rules"], + "disclosure_refs": ["disclosure:participant-transformation:bounded"], + "evidence_refs": ["conformance:participant-transformation:case-1"], + } + ] + ) + payload["capabilities"]["participant_runtime"]["supported_behavior_features"].append("participant_transformation") + + with pytest.raises(ValidationError, match="constraint_refs"): + BackendManifestV2Model.model_validate(payload) + + +def test_backend_manifest_v2_rejects_weak_policy_support_without_limitations(): + payload = _stub_payload_with_feature_support( + [ + { + "feature": "participant_declassification", + "support_level": "disclosed_weak", + "constraint_refs": [], + "limitation_refs": [], + "disclosure_refs": ["disclosure:participant-declassification:weak"], + "evidence_refs": ["conformance:participant-declassification:case-1"], + } + ] + ) + payload["capabilities"]["participant_runtime"]["supported_behavior_features"].append("participant_declassification") + + with pytest.raises(ValidationError, match="limitation_refs"): + BackendManifestV2Model.model_validate(payload) + + +def test_backend_manifest_v2_rejects_positive_participant_policy_support_without_required_contracts(): + payload = _stub_payload_with_feature_support( + [ + { + "feature": "participant_intervention", + "support_level": "bounded", + "constraint_refs": ["constraint:participant-intervention:bounded"], + "limitation_refs": ["limitation:participant-intervention:finite-scope"], + "disclosure_refs": ["disclosure:participant-intervention:bounded"], + "evidence_refs": ["conformance:participant-intervention:case-1"], + } + ] + ) + payload["capabilities"]["participant_runtime"]["supported_behavior_features"].append("participant_intervention") + payload["supported_contract_versions"].remove("participant-control-occurrence-v1") + + with pytest.raises(ValidationError, match="required contracts"): + BackendManifestV2Model.model_validate(payload) + + +def test_backend_manifest_v2_rejects_positive_policy_support_absent_from_supported_features(): + payload = _stub_payload_with_feature_support( + [ + { + "feature": "participant_egress_projection", + "support_level": "exact", + "constraint_refs": [], + "limitation_refs": [], + "disclosure_refs": [], + "evidence_refs": ["conformance:participant-egress-projection:case-1"], + } + ] + ) + with pytest.raises(ValidationError, match="positive support"): + BackendManifestV2Model.model_validate(payload) + + +def test_backend_manifest_v2_rejects_supported_policy_feature_without_strength_declaration(): + payload = _stub_payload_with_feature_support([]) + payload["capabilities"]["participant_runtime"]["supported_behavior_features"].append( + "participant_ingress_admission" + ) + + with pytest.raises(ValidationError, match="require explicit feature_support declarations"): + BackendManifestV2Model.model_validate(payload) + + +def test_backend_manifest_v2_accepts_honest_unsupported_policy_features(): + entries = [ + { + "feature": feature, + "support_level": "unsupported", + "constraint_refs": [], + "limitation_refs": [f"limitation:{feature}:not-realized"], + "disclosure_refs": [f"disclosure:{feature}:unsupported"], + "evidence_refs": [], + } + for feature in sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + ] + + model = BackendManifestV2Model.model_validate(_stub_payload_with_feature_support(entries)) + + assert model.capabilities.participant_runtime is not None + assert {entry.feature for entry in model.capabilities.participant_runtime.feature_support} == set( + PARTICIPANT_RUNTIME_POLICY_FEATURES + ) + assert all( + entry.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED + for entry in model.capabilities.participant_runtime.feature_support + ) + + +def test_participant_policy_feature_support_admission_fails_closed_and_demotes_authorized_downgrade(): + base = create_stub_manifest() + feature = "participant_transformation" + declaration = ParticipantFeatureSupport( + feature=feature, + support_level=ParticipantFeatureSupportLevel.BOUNDED, + constraint_refs=("constraint:participant-transformation:bounded",), + limitation_refs=("limitation:participant-transformation:finite-rules",), + disclosure_refs=("disclosure:participant-transformation:bounded",), + evidence_refs=("conformance:participant-transformation:case-1",), + ) + participant_runtime = replace( + base.participant_runtime, + supported_behavior_features=base.participant_runtime.supported_behavior_features | {feature}, + feature_support=(declaration,), + ) + manifest = BackendManifest( + identity=base.identity, + supported_contract_versions=base.supported_contract_versions | {"participant-crossing-occurrence-v1"}, + compatibility=base.compatibility, + realization_support=base.realization_support, + concept_bindings=base.concept_bindings, + constraints=base.constraints, + capabilities=replace(base.capabilities, participant_runtime=participant_runtime), + ) + + assert participant_feature_support_gaps(manifest, (feature,)) == ( + "participant feature 'participant_transformation' requires exact support; backend declares bounded", + ) + with pytest.raises(ValueError, match="explicit downgrade authorization"): + resolve_participant_feature_support( + manifest, + feature, + required_level=ParticipantFeatureSupportLevel.EXACT, + allowed_downgrade_level=ParticipantFeatureSupportLevel.BOUNDED, + ) + + effective = resolve_participant_feature_support( + manifest, + feature, + required_level=ParticipantFeatureSupportLevel.EXACT, + allowed_downgrade_level=ParticipantFeatureSupportLevel.BOUNDED, + downgrade_policy_ref="policy:participant-crossing:revision-3", + downgrade_provenance_ref="provenance:participant-crossing:decision-7", + ) + + assert effective is declaration + assert effective.support_level == ParticipantFeatureSupportLevel.BOUNDED + assert effective.support_level != ParticipantFeatureSupportLevel.EXACT + + +def test_participant_policy_feature_support_admission_accepts_evidence_backed_exact_support(): + base = create_stub_manifest() + assert base.participant_runtime is not None + feature = "participant_ingress_admission" + declaration = ParticipantFeatureSupport( + feature=feature, + support_level=ParticipantFeatureSupportLevel.EXACT, + evidence_refs=("conformance:participant-ingress-admission:case-1",), + ) + manifest = replace( + base, + capabilities=replace( + base.capabilities, + participant_runtime=replace( + base.participant_runtime, + supported_behavior_features=base.participant_runtime.supported_behavior_features | {feature}, + feature_support=(declaration,), + ), + ), + ) + + assert resolve_participant_feature_support(manifest, feature) is declaration + assert participant_feature_support_gaps(manifest, (feature,)) == () + + +def test_participant_policy_feature_support_admission_rejects_missing_declaration(): + base = create_stub_manifest() + assert base.participant_runtime is not None + manifest = replace( + base, + capabilities=replace( + base.capabilities, + participant_runtime=replace(base.participant_runtime, feature_support=()), + ), + ) + + assert participant_feature_support_gaps(manifest, ("participant_ingress_admission",)) == ( + "participant feature 'participant_ingress_admission' has no explicit support declaration", + ) def test_backend_manifest_v2_feature_support_defaults_to_empty(): payload = json.loads((V2_VALID_DIR / "stub.json").read_text(encoding="utf-8")) + payload["capabilities"]["participant_runtime"].pop("feature_support") model = BackendManifestV2Model.model_validate(payload) assert model.capabilities.participant_runtime is not None @@ -627,6 +891,19 @@ def test_backend_manifest_v2_schema_publishes_feature_support_disclosure_rule(): "properties": {"disclosure_refs": {"minItems": 1}}, }, } in feature_support_schema["allOf"] + assert {"limitation_refs", "evidence_refs"} <= feature_support_schema["properties"].keys() + assert any( + condition.get("then", {}).get("properties", {}).get("limitation_refs", {}).get("minItems") == 1 + for condition in feature_support_schema["allOf"] + ) + assert any( + condition.get("then", {}).get("properties", {}).get("constraint_refs", {}).get("minItems") == 1 + for condition in feature_support_schema["allOf"] + ) + assert any( + condition.get("then", {}).get("properties", {}).get("evidence_refs", {}).get("minItems") == 1 + for condition in feature_support_schema["allOf"] + ) def test_backend_manifest_v2_requires_manifest_sections(): diff --git a/implementations/python/tests/test_backend_profiles.py b/implementations/python/tests/test_backend_profiles.py index 567d9e27e..d632e635c 100644 --- a/implementations/python/tests/test_backend_profiles.py +++ b/implementations/python/tests/test_backend_profiles.py @@ -70,6 +70,8 @@ def test_full_remote_profile_includes_participant_episode_contracts(): assert "participant-episode-state-envelope-v1" in profile.required_contracts assert "participant-episode-history-event-stream-v1" in profile.required_contracts assert "participant-behavior-history-event-stream-v1" in profile.required_contracts + assert "participant-control-occurrence-v1" in profile.required_contracts + assert "participant-crossing-occurrence-v1" in profile.required_contracts def test_backend_profile_model_rejects_unknown_contract_id(): diff --git a/implementations/python/tests/test_dsl_437_benign_participant_execution.py b/implementations/python/tests/test_dsl_437_benign_participant_execution.py index 096d1010e..158a859e1 100644 --- a/implementations/python/tests/test_dsl_437_benign_participant_execution.py +++ b/implementations/python/tests/test_dsl_437_benign_participant_execution.py @@ -12,6 +12,7 @@ from raes._errors import SDLValidationError from raes.parser import parse_sdl from raes.participant_behavior import ParticipantFailureClass +from raes_backend_protocols.capabilities import ParticipantFeatureSupport from raes_backend_protocols.capability_admission import participant_autonomous_execution_capability_gaps from raes_backend_protocols.participant_runtime_base import BaseParticipantRuntime from raes_backend_stubs.manifest import create_stub_manifest @@ -34,6 +35,7 @@ ParticipantEpisodeResetRequest, ) from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel from raes_processor.compiler import compile_runtime_model from raes_processor.compiler.time_model import time_model_contract_model from raes_processor.planner import plan @@ -613,10 +615,73 @@ def test_planner_enforces_required_participant_features_and_exact_targets() -> N execution_plan = plan(runtime_model, unsupported) messages = [diagnostic.message for diagnostic in execution_plan.diagnostics] - assert any("required participant feature 'action_contracts'" in message for message in messages) + assert any("participant feature 'action_contracts'" in message for message in messages) assert any("unsupported autonomous target addresses" in message for message in messages) +def test_planner_fails_closed_for_policy_features_outside_autonomous_execution() -> None: + runtime_model, _ = _compiled() + address = "participant.behavior-specification.participant-behavior" + specification = replace( + runtime_model.behavior_specifications[address], + autonomous_execution=None, + backend_feature_support_refs=("participant_ingress_admission",), + ) + runtime_model = replace( + runtime_model, + behavior_specifications={**runtime_model.behavior_specifications, address: specification}, + ) + + execution_plan = plan(runtime_model, create_stub_manifest(with_time=True)) + + assert any( + diagnostic.code == "participant.feature-support-insufficient" + and diagnostic.address == address + and "explicitly unsupported" in diagnostic.message + for diagnostic in execution_plan.diagnostics + ) + + +def test_planner_accepts_evidence_backed_exact_policy_feature_support() -> None: + runtime_model, _ = _compiled() + address = "participant.behavior-specification.participant-behavior" + feature = "participant_ingress_admission" + specification = replace( + runtime_model.behavior_specifications[address], + autonomous_execution=None, + backend_feature_support_refs=(feature,), + ) + runtime_model = replace( + runtime_model, + behavior_specifications={**runtime_model.behavior_specifications, address: specification}, + ) + manifest = create_stub_manifest(with_time=True) + assert manifest.participant_runtime is not None + declaration = ParticipantFeatureSupport( + feature=feature, + support_level=ParticipantFeatureSupportLevel.EXACT, + evidence_refs=("conformance:participant-ingress-admission:case-1",), + ) + manifest = replace( + manifest, + capabilities=replace( + manifest.capabilities, + participant_runtime=replace( + manifest.participant_runtime, + supported_behavior_features=manifest.participant_runtime.supported_behavior_features | {feature}, + feature_support=(declaration,), + ), + ), + ) + + execution_plan = plan(runtime_model, manifest) + + assert not any( + diagnostic.code == "participant.feature-support-insufficient" and diagnostic.address == address + for diagnostic in execution_plan.diagnostics + ) + + def test_runtime_manager_drives_due_actions_from_shared_clock_controls() -> None: scenario = parse_sdl(_scenario_yaml()) runtime_model = compile_runtime_model(scenario) diff --git a/implementations/python/tests/test_runtime_conformance.py b/implementations/python/tests/test_runtime_conformance.py index 557f389f8..f05c598f3 100644 --- a/implementations/python/tests/test_runtime_conformance.py +++ b/implementations/python/tests/test_runtime_conformance.py @@ -10,6 +10,7 @@ from raes_backend_protocols.capabilities import ( BackendCapabilitySet, BackendManifest, + ParticipantFeatureSupport, ProvisionerCapabilities, ) from raes_backend_stubs.stubs import create_stub_components, create_stub_manifest, create_stub_target @@ -24,7 +25,7 @@ from raes_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration from raes_contracts.planning import ChangeAction, RuntimeDomain from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot, SnapshotEntry -from raes_contracts.vocabulary import RealizationSupportMode +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel, RealizationSupportMode from raes_runtime.registry import RuntimeTarget API_406_CARRIER_CONTRACTS = { @@ -59,6 +60,15 @@ def test_target_conformance_passes_for_stub_target(): assert "Does not establish trace equivalence or bisimulation." in report.claim.explicit_non_claims assert not report.unsupported_contract_gaps assert not report.unsupported_capability_gaps + feature_cases = [case for case in report.cases if case.capability_feature is not None] + assert len(feature_cases) == 6 + assert all(case.declared_support_level == "unsupported" for case in feature_cases) + assert all(case.effective_support_level == "unsupported" for case in feature_cases) + assert all( + case.finite_scope and "no unexecuted participant behavior" in case.finite_scope for case in feature_cases + ) + assert all(case.limitations for case in feature_cases) + assert all(case.explicit_non_claims for case in feature_cases) # RUN-311 finding 4: the live probe must actually drive every # participant episode control action and end with a non-empty, # consistent snapshot for the conformance participant. @@ -84,6 +94,40 @@ def test_target_conformance_passes_for_stub_target(): ) +def test_target_conformance_records_evidence_backed_exact_policy_support(): + target = create_stub_target() + manifest = target.manifest + assert manifest.participant_runtime is not None + feature = "participant_ingress_admission" + declaration = ParticipantFeatureSupport( + feature=feature, + support_level=ParticipantFeatureSupportLevel.EXACT, + evidence_refs=("conformance:participant-ingress-admission:case-1",), + ) + manifest = replace( + manifest, + capabilities=replace( + manifest.capabilities, + participant_runtime=replace( + manifest.participant_runtime, + supported_behavior_features=manifest.participant_runtime.supported_behavior_features | {feature}, + feature_support=(declaration,), + ), + ), + ) + + report = run_target_conformance(replace(target, manifest=manifest)) + + case = next(case for case in report.cases if case.capability_feature == feature) + assert report.passed is True + assert case.passed is True + assert case.declared_support_level == "exact" + assert case.effective_support_level == "exact" + assert case.evidence_refs == ("conformance:participant-ingress-admission:case-1",) + assert case.finite_scope and "no unexecuted participant behavior" in case.finite_scope + assert case.explicit_non_claims + + def test_full_remote_control_plane_profile_requires_api_406_carriers(): contracts = required_contracts(BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE) @@ -93,6 +137,8 @@ def test_full_remote_control_plane_profile_requires_api_406_carriers(): "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", } <= contracts @@ -253,6 +299,8 @@ def test_fixture_suite_passes_for_full_remote_control_plane_profile(): assert "participant-episode-state-envelope-v1" in contract_names assert "participant-episode-history-event-stream-v1" in contract_names assert "participant-behavior-history-event-stream-v1" in contract_names + assert "participant-control-occurrence-v1" in contract_names + assert "participant-crossing-occurrence-v1" in contract_names def test_runtime_snapshot_semantic_diagnostics_reject_invalid_participant_episode_state(): @@ -843,6 +891,8 @@ def test_target_conformance_fails_when_declared_contracts_do_not_cover_profile_r "operation-status-v1", "orchestration-plan-v1", "participant-behavior-history-event-stream-v1", + "participant-control-occurrence-v1", + "participant-crossing-occurrence-v1", "participant-episode-history-event-stream-v1", "participant-episode-state-envelope-v1", "participant-lifecycle-event-v1", From 7193b1557137c452275a4bbdcf84368772914ef0 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 19:03:07 +0200 Subject: [PATCH 13/55] Fix SonarCloud findings (cycle 1) --- .../capability_admission.py | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/implementations/python/packages/raes_backend_protocols/capability_admission.py b/implementations/python/packages/raes_backend_protocols/capability_admission.py index 98e4b8802..a6357f71a 100644 --- a/implementations/python/packages/raes_backend_protocols/capability_admission.py +++ b/implementations/python/packages/raes_backend_protocols/capability_admission.py @@ -79,33 +79,27 @@ def _participant_feature_required_contracts(feature: str) -> frozenset[str]: return frozenset() -def resolve_participant_feature_support( - manifest: BackendManifest, +def _validate_downgrade_authorization( feature: str, - *, - required_level: ParticipantFeatureSupportLevel = ParticipantFeatureSupportLevel.EXACT, - allowed_downgrade_level: ParticipantFeatureSupportLevel | None = None, - downgrade_policy_ref: str | None = None, - downgrade_provenance_ref: str | None = None, -) -> ParticipantFeatureSupport | None: - """Resolve one required participant feature without inventing support. - - Historical API-405 behavior and interaction requirements remain - presence-based when no API-407 strength entry exists. Participant-policy - features added by issue #801 always require an explicit strength entry. - An accepted downgrade returns that manifest entry unchanged, so callers - cannot retain the stronger requested claim. - """ - + allowed_downgrade_level: ParticipantFeatureSupportLevel | None, + downgrade_policy_ref: str | None, + downgrade_provenance_ref: str | None, +) -> None: if allowed_downgrade_level is not None and (not downgrade_policy_ref or not downgrade_provenance_ref): raise ValueError( f"participant feature '{feature}' requires explicit downgrade authorization " "with policy and provenance references" ) + +def _participant_feature_declaration( + manifest: BackendManifest, + feature: str, +) -> ParticipantFeatureSupport | None: capability = manifest.participant_runtime if capability is None: raise ValueError(f"participant feature '{feature}' requires participant runtime capabilities") + 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: @@ -114,7 +108,14 @@ def resolve_participant_feature_support( raise ValueError(f"participant feature '{feature}' has no explicit support declaration") if declaration.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED: raise ValueError(f"participant feature '{feature}' is explicitly unsupported") + return declaration + +def _validate_participant_feature_evidence( + manifest: BackendManifest, + feature: str, + declaration: ParticipantFeatureSupport, +) -> None: missing_contracts = sorted(_participant_feature_required_contracts(feature) - manifest.supported_contract_versions) if missing_contracts: raise ValueError( @@ -123,6 +124,13 @@ def resolve_participant_feature_support( if not declaration.evidence_refs and feature in PARTICIPANT_RUNTIME_POLICY_FEATURES: raise ValueError(f"participant feature '{feature}' has no conformance evidence") + +def _resolve_participant_feature_strength( + feature: str, + declaration: ParticipantFeatureSupport, + required_level: ParticipantFeatureSupportLevel, + allowed_downgrade_level: ParticipantFeatureSupportLevel | None, +) -> ParticipantFeatureSupport: declared_rank = _PARTICIPANT_FEATURE_SUPPORT_RANK[declaration.support_level] required_rank = _PARTICIPANT_FEATURE_SUPPORT_RANK[required_level] if declared_rank >= required_rank: @@ -140,6 +148,42 @@ def resolve_participant_feature_support( return declaration +def resolve_participant_feature_support( + manifest: BackendManifest, + feature: str, + *, + required_level: ParticipantFeatureSupportLevel = ParticipantFeatureSupportLevel.EXACT, + allowed_downgrade_level: ParticipantFeatureSupportLevel | None = None, + downgrade_policy_ref: str | None = None, + downgrade_provenance_ref: str | None = None, +) -> ParticipantFeatureSupport | None: + """Resolve one required participant feature without inventing support. + + Historical API-405 behavior and interaction requirements remain + presence-based when no API-407 strength entry exists. Participant-policy + features added by issue #801 always require an explicit strength entry. + An accepted downgrade returns that manifest entry unchanged, so callers + cannot retain the stronger requested claim. + """ + + _validate_downgrade_authorization( + feature, + allowed_downgrade_level, + downgrade_policy_ref, + downgrade_provenance_ref, + ) + declaration = _participant_feature_declaration(manifest, feature) + if declaration is None: + return None + _validate_participant_feature_evidence(manifest, feature, declaration) + return _resolve_participant_feature_strength( + feature, + declaration, + required_level, + allowed_downgrade_level, + ) + + def participant_feature_support_gaps( manifest: BackendManifest, features: Iterable[str], From ecac57dd5331ea9e80ee010ee6fcce403102f269 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 19:12:29 +0200 Subject: [PATCH 14/55] feat!: define exact-cut participant decision surfaces --- .../behavioral-relations-v1.json | 173 +- .../controlled-vocabularies-v1.json | 4 + .../invalid/missing-taxonomy-id.json | 2 +- .../valid/reference.json | 2493 ++++++++++++++++- .../valid/reference.json | 4 + .../valid/human-candidate.json | 60 +- .../invalid/delivered-without-delivery.json | 260 ++ ...episode-local-without-reset-authority.json | 259 ++ .../invalid/legacy-observation-order.json | 261 ++ .../valid/delivered-initial.json | 281 ++ .../valid/projected-initial.json | 260 ++ .../experiment-study-v1/valid/reference.json | 2 +- .../valid/reference.json | 6 +- .../valid/reference.json | 3 +- .../valid/minimal.json | 2 +- ...scientific-scenario-completeness-rev1.json | 10 +- .../provenance/sdl-lineage-ledger-v1.json | 209 +- .../entries/behavioral-relations-v1.json | 6 +- .../participant-decision-surface-v1.json | 6 +- .../participant-decision-surface-v2.json | 10 + ...articipant-implementation-manifest-v1.json | 6 +- .../behavioral-relations-v1.json | 3 +- .../participant-decision-surface-v1.json | 200 -- .../participant-decision-surface-v2.json | 1482 ++++++++++ ...articipant-implementation-manifest-v1.json | 3 +- docs/decisions/adrs/README.md | 2 + ...decision-surface-and-exposure-semantics.md | 80 +- ...articipant-information-flow-and-control.md | 9 +- ...-epoch-state-cut-and-delivery-semantics.md | 390 +++ docs/decisions/adrs/adr-index.yaml | 8 +- ...311-sem-220-order-zero-anchor-preflight.md | 351 --- docs/explain/reference/README.md | 3 + ...rticipant-decision-surface-v2-migration.md | 86 + .../reference/shared-semantic-integrity.md | 2 +- docs/explain/sdl/lineage.md | 137 +- docs/explain/sdl/precedents.md | 3 + .../lineage/source-audit-2026-07-12.md | 62 +- .../raes_conformance/conformance/report.py | 2 +- .../raes_contracts/behavioral_relations.py | 2 +- .../raes_contracts/contracts/__init__.py | 25 +- .../raes_contracts/contracts/bundle.py | 2 + .../contracts/participant_decision_surface.py | 31 - .../participant_decision_surface_anchor.py | 63 - ...articipant_decision_surface_exposure_v2.py | 106 + .../participant_decision_surface_v2.py | 482 ++++ .../raes_contracts/manifest_authority.py | 1 + .../raes_contracts/participant_binding_v2.py | 153 + .../participant_decision_surface_delivery.py | 67 + .../raes_operations/_evidence_run_artifact.py | 2 +- .../raes_processor/models/__init__.py | 28 +- .../raes_processor/models/decision_surface.py | 46 +- .../models/decision_surface_anchor.py | 306 -- .../models/decision_surface_anchor_v2.py | 274 ++ .../models/decision_surface_v2.py | 323 +++ .../participant_exposure_authority_v2.py | 129 + .../models/participant_exposure_v2.py | 278 ++ .../raes_runtime/participant_control.py | 38 +- .../participant_control_diagnostics.py | 22 + ...participant_decision_surface_control_v2.py | 78 + .../tests/sem230_information_flow_model.py | 110 +- .../tests/test_backend_conformance_cli.py | 2 +- .../tests/test_behavioral_relation_claims.py | 2 +- .../python/tests/test_behavioral_relations.py | 63 +- ...est_participant_implementation_manifest.py | 3 + ...st_sem_220_participant_decision_surface.py | 571 +--- ...sem_220_participant_decision_surface_v2.py | 378 +++ ...participant_decision_surface_v2_runtime.py | 548 ++++ .../test_sem_230_information_flow_control.py | 171 +- specs/formal/behavioral-relations/README.md | 72 +- specs/formal/participant-runtime/README.md | 59 +- specs/formal/participant-semantics/README.md | 174 +- .../information-flow-control.md | 198 +- 72 files changed, 9840 insertions(+), 2107 deletions(-) create mode 100644 contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/delivered-without-delivery.json create mode 100644 contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/episode-local-without-reset-authority.json create mode 100644 contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/legacy-observation-order.json create mode 100644 contracts/fixtures/control-plane/participant-decision-surface-v2/valid/delivered-initial.json create mode 100644 contracts/fixtures/control-plane/participant-decision-surface-v2/valid/projected-initial.json create mode 100644 contracts/schema-publication/entries/participant-decision-surface-v2.json create mode 100644 contracts/schemas/control-plane/participant-decision-surface-v2.json create mode 100644 docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md delete mode 100644 docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md create mode 100644 docs/explain/reference/participant-decision-surface-v2-migration.md delete mode 100644 implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py create mode 100644 implementations/python/packages/raes_contracts/participant_binding_v2.py create mode 100644 implementations/python/packages/raes_contracts/participant_decision_surface_delivery.py delete mode 100644 implementations/python/packages/raes_processor/models/decision_surface_anchor.py create mode 100644 implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py create mode 100644 implementations/python/packages/raes_processor/models/decision_surface_v2.py create mode 100644 implementations/python/packages/raes_processor/models/participant_exposure_authority_v2.py create mode 100644 implementations/python/packages/raes_processor/models/participant_exposure_v2.py create mode 100644 implementations/python/packages/raes_runtime/participant_control_diagnostics.py create mode 100644 implementations/python/packages/raes_runtime/participant_decision_surface_control_v2.py create mode 100644 implementations/python/tests/test_sem_220_participant_decision_surface_v2.py create mode 100644 implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py diff --git a/contracts/concept-authority/behavioral-relations-v1.json b/contracts/concept-authority/behavioral-relations-v1.json index 762f91ec1..97428ff73 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": "aces-behavioral-relations", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "bibliography": [ { "source_id": "park-1981", @@ -122,6 +122,54 @@ "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", @@ -1547,23 +1595,23 @@ "relation_id": "policy-noninterference", "display_name": "Participant-policy noninterference", "relation_class": "behavioral", - "definition": "For a fixed participant, episode scope, model, environment class, scheduler class, order model, policy-revision sequence, and permitted declassification schedule, low-equivalent initial states and input histories with equal admitted low inputs and equal participant-policy purge results produce equal support sets of projected participant-visible histories despite variation in unauthorized high inputs.", - "left_carrier": "The support set of valid labelled participant-policy runs from one low-equivalent initial state and input history.", - "right_carrier": "The support set of valid labelled participant-policy runs from another low-equivalent initial state and input history.", - "initial_states": "Initial world, participant-view, participant-history, archival-evidence, controller, authority, marking, and policy states related by the SEM-230 low-equivalence relation at the declared initial policy/order point.", + "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 effective policy revision, marking/declassification intersection, and declared order-point projection.", - "hidden_actions": "Only labels mapped to tau by the named participant-, audience-, policy-revision-, and order-relative projection; backend-internal actions are not intrinsically hidden.", + "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-revision sequence and effective order points", + "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." @@ -1571,10 +1619,10 @@ "projection_required": true, "direction": "symmetric", "quantification": { - "states": "For every pair of initial and reachable states related by low equivalence at the applicable participant-policy/order point.", - "traces": "For all valid run support sets generated from equal admitted low inputs, equal dynamic-purge results, and equal permitted declassification schedules.", + "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": "No strategic or coalitional quantification; alternating-strategic equivalence is a separate 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." }, @@ -1601,15 +1649,15 @@ } }, "preservation": { - "property": "Unauthorized high-input variation does not change the support set of participant-visible histories after dynamic purge, except at equal explicitly governed declassification events.", - "proof_obligation": "Prove support-set equality for every quantified low-equivalent pair under the fixed participant, policy sequence, declassification schedule, model, scheduler, environment, and order assumptions, or report only the bounded counterexamples actually checked." + "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." ], "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, purge, declassification, scheduler, environment, and quantifier obligations.", + "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." ], "incompatible_claim_surfaces": [ @@ -1629,6 +1677,8 @@ ] }, "source_refs": [ + "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "clarkson-schneider-2010", "fagin-halpern-moses-vardi-1995", "goguen-meseguer-1982", "milner-1980", @@ -1636,6 +1686,94 @@ "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", @@ -2156,9 +2294,10 @@ "intended_relation_ids": [ "realization-envelope-membership", "bounded-probe-success", - "trace-inclusion" + "trace-inclusion", + "io-alternating-refinement" ], - "evidence_boundary": "Envelope checks and named target probes are bounded evidence; projected trace inclusion is the intended universal obligation and remains deliberately unproved.", + "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", @@ -2204,7 +2343,7 @@ "intended_relation_ids": [ "policy-noninterference" ], - "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, policy-revision sequence, low-equivalence, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. Current executable evidence is limited to the finite test-local model.", + "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.", "prohibited_relation_ids": [ "participant-projected-history-equivalence", "trace-equivalence", diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index 24ca22534..e06e81744 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -85,6 +85,10 @@ "title": "Participant Behavior History Event Stream V1", "description": "Participant behavior history event stream contract." }, + "participant-decision-surface-v2": { + "title": "Participant Decision Surface V2", + "description": "Exact-cut participant decision view, assurance, delivery, and selection contract." + }, "participant-episode-history-event-stream-v1": { "title": "Participant Episode History Event Stream V1", "description": "Participant episode history event stream contract." diff --git a/contracts/fixtures/concept-authority/behavioral-relations-v1/invalid/missing-taxonomy-id.json b/contracts/fixtures/concept-authority/behavioral-relations-v1/invalid/missing-taxonomy-id.json index d682abdaa..e4bde8224 100644 --- a/contracts/fixtures/concept-authority/behavioral-relations-v1/invalid/missing-taxonomy-id.json +++ b/contracts/fixtures/concept-authority/behavioral-relations-v1/invalid/missing-taxonomy-id.json @@ -1,6 +1,6 @@ { "schema_version": "behavioral-relations/v1", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "bibliography": [], "relations": {}, "claim_surfaces": [], 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 efc61063d..97428ff73 100644 --- a/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json @@ -1,107 +1,2494 @@ { "schema_version": "behavioral-relations/v1", "taxonomy_id": "aces-behavioral-relations", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "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"], + "authors": [ + "Robin Milner" + ], "publication_year": 1980, - "publication_venue": "Lecture Notes in Computer Science", - "edition_or_version": "First edition", - "immutable_locator": {"kind": "doi", "value": "10.1007/3-540-10235-3"} + "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": "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" + } } ], "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 ACES 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 probe success", - "relation_class": "predicate", - "definition": "Every named finite case has its expected result.", + "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 named case set.", - "initial_states": "The setup state of each named case.", + "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": "Named case actions.", - "transition_relation": "Transitions exercised by each case.", - "observable_actions": "Case-reported actions.", - "hidden_actions": "Harness actions not reported by the case.", - "stuttering_actions": "Declared no-op harness actions." + "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": "Only named case outputs are retained.", - "order_treatment": "Case order is preserved.", - "simultaneity_treatment": "Only explicitly declared simultaneity is retained." + "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": "unary", + "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" + ] + }, + "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": "States reached by named cases.", - "traces": "Enumerated finite traces.", - "schedulers": "Schedulers exercised by the harness.", - "strategies": "Strategies exercised by the harness.", - "environments": "Named environments.", - "observations": "Case-reported observations." + "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": "parameterized", "treatment": "Only sampled choices are covered."}, - "concurrency": {"status": "parameterized", "treatment": "Only exercised interleavings are covered."}, - "probability": {"status": "outside-scope", "treatment": "No distribution claim is made."}, - "time": {"status": "parameterized", "treatment": "Only the case clock is covered."}, - "partial_order": {"status": "outside-scope", "treatment": "No event-structure claim is made."} + "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": "Expected outcomes of named cases.", - "proof_obligation": "Execute each named case and compare its bounded expected result." + "property": "Set inclusion of declared realization support.", + "proof_obligation": "Prove or decide inclusion for every governed envelope dimension." }, - "bounded_evidence": ["A passing fixture suite."], - "explicit_non_claims": ["Finite success does not establish trace equivalence or bisimulation."], - "incompatible_claim_surfaces": ["Universal behavioral equivalence."], + "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 ACES 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": ["implementations/python/packages/raes_conformance/conformance.py"] + "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." }, - "source_refs": ["milner-1980"] + "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" + ] + }, + "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." + ], + "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." + ], + "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": "not-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" + ] + }, + "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": "fixture-surface", - "intended_relation_ids": ["bounded-probe-success"], - "evidence_boundary": "The one relation in this minimal fixture.", - "prohibited_relation_ids": ["bounded-probe-success"], - "explicit_non_claims": ["This fixture is not the canonical claim-surface policy."] + "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.", + "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." + ] + }, + { + "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": "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": { - "minimal-probe": { - "example_id": "minimal-probe", - "purpose": "Exercise the transition-system fixture shape.", + "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"], + "states": [ + "l0", + "l1", + "l2" + ], "initial_state": "l0", - "transitions": [{"source": "l0", "action": "a", "target": "l1"}] + "transitions": [ + { + "source": "l0", + "action": "a", + "target": "l1" + }, + { + "source": "l0", + "action": "b", + "target": "l2" + } + ] }, "right_system": { - "states": ["r0", "r1"], + "states": [ + "r0", + "r1" + ], "initial_state": "r0", - "transitions": [{"source": "r0", "action": "a", "target": "r1"}] + "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 ACES 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": ["a"], + "tested_visible_trace": [ + "send" + ], "hidden_action": "tau", - "expected_strong_bisimulation": true, + "expected_strong_bisimulation": false, "expected_weak_matching": true, - "evidence_boundary": "One visible action in a minimal fixture.", - "explicit_non_claims": ["The fixture does not prove the canonical catalog's relation set."] + "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 ACES backend relation." + ] } } } 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 24ca22534..e06e81744 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -85,6 +85,10 @@ "title": "Participant Behavior History Event Stream V1", "description": "Participant behavior history event stream contract." }, + "participant-decision-surface-v2": { + "title": "Participant Decision Surface V2", + "description": "Exact-cut participant decision view, assurance, delivery, and selection contract." + }, "participant-episode-history-event-stream-v1": { "title": "Participant Episode History Event Stream V1", "description": "Participant episode history event stream contract." diff --git a/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json b/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json index df9464ca9..8ace14172 100644 --- a/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json +++ b/contracts/fixtures/control-plane/participant-decision-surface-v1/valid/human-candidate.json @@ -1,18 +1,18 @@ { - "surface_id": "decision-surfaces.red.human.order-1", + "surface_id": "decision-surfaces.red.human.order-4", "participant_address": "participant.behavior.red-agent", "episode_id": "episode-1", "observation_point": "behavior-history:4", - "observation_order": 1, + "observation_order": 4, "behavior_specification_address": "participant.behavior-specification.red-surface", "observation_boundary_address": "participant.observation-boundary.red-view", - "context_view_ref": "context-views.red.episode-1.order-1", + "context_view_ref": "context-views.red.episode-1.order-4", "implementation_selection_ref": "participant-selections.red.human.v1", "decision_control_mode": "human-supervised", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", "exposure_policy_ref": "exposure-policy.red.v1", - "visibility_projection_ref": "visibility-projection.red.order-1", + "visibility_projection_ref": "visibility-projection.red.order-4", "visible_context_refs": [ "context.public" ], @@ -54,11 +54,10 @@ "open_extension_binding_ref": null }, "evidence_refs": [ - "evidence.surface.red.order-1" + "evidence.surface.red.order-4" ], "provenance_refs": [ - "provenance.surface.red.human.order-1", - "participant-behavior-event:sha256:4444444444444444444444444444444444444444444444444444444444444444" + "provenance.surface.red.human.order-4" ], "marking_definition_refs": [ "markings.participant-visible.v1" @@ -67,23 +66,6 @@ "semantic_limitations": [ "Candidate membership does not imply eligibility or admission" ], - "projection_anchor": { - "participant_address": "participant.behavior.red-agent", - "episode_id": "episode-1", - "decision_surface_order": 1, - "event_ref": "participant-behavior-event:sha256:4444444444444444444444444444444444444444444444444444444444444444", - "anchor_order": 4, - "evidence_refs": [ - "evidence.surface.red.order-1" - ], - "provenance_refs": [ - "participant-behavior-event:sha256:4444444444444444444444444444444444444444444444444444444444444444" - ], - "anchor_kind": "behavior_event", - "event_type": "observation_emitted", - "action_instance_id": "scan-1", - "history_prefix_length": 5 - }, "audience_scope_ref": "audience.participant.behavior.red-agent", "exposure_bindings": [ { @@ -94,7 +76,7 @@ "episode_id": "episode-1", "audience_scope_ref": "audience.participant.behavior.red-agent", "observation_point": "behavior-history:4", - "observation_order": 1, + "observation_order": 4, "visibility_basis_ref": "visibility-bases.context.public", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", @@ -113,19 +95,19 @@ "markings.participant-visible.v1" ], "source_provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "result_provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "declassification_basis_ref": null, "redaction_policy_ref": null, "transformation_rule_ref": null, "evidence_refs": [ - "evidence.surface.red.order-1" + "evidence.surface.red.order-4" ], "provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "loss_and_limitations": [ "No known projection loss" @@ -141,7 +123,7 @@ "episode_id": "episode-1", "audience_scope_ref": "audience.participant.behavior.red-agent", "observation_point": "behavior-history:4", - "observation_order": 1, + "observation_order": 4, "visibility_basis_ref": "visibility-bases.participant.action-contract.scan", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", @@ -160,19 +142,19 @@ "markings.participant-visible.v1" ], "source_provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "result_provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "declassification_basis_ref": null, "redaction_policy_ref": null, "transformation_rule_ref": null, "evidence_refs": [ - "evidence.surface.red.order-1" + "evidence.surface.red.order-4" ], "provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "loss_and_limitations": [ "No known projection loss" @@ -188,7 +170,7 @@ "episode_id": "episode-1", "audience_scope_ref": "audience.participant.behavior.red-agent", "observation_point": "behavior-history:4", - "observation_order": 1, + "observation_order": 4, "visibility_basis_ref": "visibility-bases.participant.behavior-specification.red-surface.tool-affordance.scanner", "projection_policy_ref": "projection-policy.red.v1", "projection_policy_revision": "1", @@ -207,19 +189,19 @@ "markings.participant-visible.v1" ], "source_provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "result_provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "declassification_basis_ref": null, "redaction_policy_ref": null, "transformation_rule_ref": null, "evidence_refs": [ - "evidence.surface.red.order-1" + "evidence.surface.red.order-4" ], "provenance_refs": [ - "provenance.surface.red.human.order-1" + "provenance.surface.red.human.order-4" ], "loss_and_limitations": [ "No known projection loss" diff --git a/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/delivered-without-delivery.json b/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/delivered-without-delivery.json new file mode 100644 index 000000000..b5b50788e --- /dev/null +++ b/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/delivered-without-delivery.json @@ -0,0 +1,260 @@ +{ + "schema_version": "participant-decision-surface/v2", + "surface_state": "delivered", + "participant_view": { + "surface_id": "decision-surfaces.red.episode-1.epoch-0", + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "information_state_ref": "information-states.red.episode-1.epoch-0", + "context_view_ref": "context-views.red.episode-1.epoch-0", + "decision_control_mode": "agent", + "visible_context_refs": [ + "context.network-segment" + ], + "action_entries": [ + { + "entry_id": "scan", + "action_contract_address": "actions.scan", + "presentation_basis_ref": "presentation.scan", + "visibility": "observable", + "eligibility": "eligible", + "eligibility_reason_refs": [], + "constraint_refs": [ + "constraints.scan" + ], + "selection_shape_ref": "argument-shapes.scan", + "support": "supported", + "support_refs": [ + "support.scan" + ], + "affordance_refs": [ + "affordances.scanner" + ], + "realization_refs": [ + "realizations.scan" + ] + } + ], + "affordance_refs": [ + "affordances.scanner" + ], + "form": { + "surface_form": "candidate_action_set", + "selection_meaning_ref": "selection-meaning.candidate.v1", + "candidate_entry_ids": [ + "scan" + ], + "open_extension_binding_ref": null + }, + "marking_definition_refs": [ + "markings.participant" + ], + "redaction_policy_ref": "redaction.participant", + "semantic_limitations": [ + "limitations.bounded-surface" + ] + }, + "assurance": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "behavior_specification_address": "behavior-specifications.red", + "observation_boundary_address": "observation-boundaries.red", + "implementation_selection_ref": "implementation-selections.red", + "audience_scope_ref": "audiences.red", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "visibility_projection_ref": "visibility-projections.red.initial", + "participant_memory_scope": "persistent_across_episodes", + "memory_reset_authority_ref": null, + "participant_view_digest": "sha256:e17647852a76dfbb242ac3f66e057b4925f6b3ac9e5f2d195d5efa29ae8e0f30", + "derivation_anchor": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "participant-state-cuts.red.episode-1.initial", + "history_domain": "participant_episode_lifecycle", + "order_model": "control_plane_order", + "anchor_event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "anchor_order": 1, + "history_prefix_length": 2, + "predecessor_event_refs": [ + "participant-episode-event:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + }, + "evidence_refs": [ + "evidence.episode-running" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "anchor_kind": "episode_readiness", + "event_type": "episode_running", + "episode_sequence_number": 0 + }, + "exposure_bindings": [ + { + "item_ref": "context.network-segment", + "authorization_record_ref": "authorizations.context.network-segment", + "source_ref": "context.network-segment", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.context.network-segment" + ], + "result_provenance_refs": [ + "provenance.context.network-segment" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.context.network-segment" + ], + "provenance_refs": [ + "provenance.context.network-segment" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "actions.scan", + "authorization_record_ref": "authorizations.scan", + "source_ref": "actions.scan", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.scan" + ], + "result_provenance_refs": [ + "provenance.scan" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.scan" + ], + "provenance_refs": [ + "provenance.scan" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "affordances.scanner", + "authorization_record_ref": "authorizations.affordances.scanner", + "source_ref": "affordances.scanner", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.affordances.scanner" + ], + "result_provenance_refs": [ + "provenance.affordances.scanner" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.affordances.scanner" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + } + ], + "evidence_refs": [ + "evidence.episode-running", + "evidence.authorization.context.network-segment", + "evidence.authorization.scan", + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "provenance.context.network-segment", + "provenance.scan", + "provenance.affordances.scanner" + ] + }, + "delivery": null +} diff --git a/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/episode-local-without-reset-authority.json b/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/episode-local-without-reset-authority.json new file mode 100644 index 000000000..4c539b326 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/episode-local-without-reset-authority.json @@ -0,0 +1,259 @@ +{ + "schema_version": "participant-decision-surface/v2", + "surface_state": "projected", + "participant_view": { + "surface_id": "decision-surfaces.red.episode-1.epoch-0", + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "information_state_ref": "information-states.red.episode-1.epoch-0", + "context_view_ref": "context-views.red.episode-1.epoch-0", + "decision_control_mode": "agent", + "visible_context_refs": [ + "context.network-segment" + ], + "action_entries": [ + { + "entry_id": "scan", + "action_contract_address": "actions.scan", + "presentation_basis_ref": "presentation.scan", + "visibility": "observable", + "eligibility": "eligible", + "eligibility_reason_refs": [], + "constraint_refs": [ + "constraints.scan" + ], + "selection_shape_ref": "argument-shapes.scan", + "support": "supported", + "support_refs": [ + "support.scan" + ], + "affordance_refs": [ + "affordances.scanner" + ], + "realization_refs": [ + "realizations.scan" + ] + } + ], + "affordance_refs": [ + "affordances.scanner" + ], + "form": { + "surface_form": "candidate_action_set", + "selection_meaning_ref": "selection-meaning.candidate.v1", + "candidate_entry_ids": [ + "scan" + ], + "open_extension_binding_ref": null + }, + "marking_definition_refs": [ + "markings.participant" + ], + "redaction_policy_ref": "redaction.participant", + "semantic_limitations": [ + "limitations.bounded-surface" + ] + }, + "assurance": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "behavior_specification_address": "behavior-specifications.red", + "observation_boundary_address": "observation-boundaries.red", + "implementation_selection_ref": "implementation-selections.red", + "audience_scope_ref": "audiences.red", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "visibility_projection_ref": "visibility-projections.red.initial", + "participant_memory_scope": "episode_local_reset", + "participant_view_digest": "sha256:e17647852a76dfbb242ac3f66e057b4925f6b3ac9e5f2d195d5efa29ae8e0f30", + "derivation_anchor": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "participant-state-cuts.red.episode-1.initial", + "history_domain": "participant_episode_lifecycle", + "order_model": "control_plane_order", + "anchor_event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "anchor_order": 1, + "history_prefix_length": 2, + "predecessor_event_refs": [ + "participant-episode-event:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + }, + "evidence_refs": [ + "evidence.episode-running" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "anchor_kind": "episode_readiness", + "event_type": "episode_running", + "episode_sequence_number": 0 + }, + "exposure_bindings": [ + { + "item_ref": "context.network-segment", + "authorization_record_ref": "authorizations.context.network-segment", + "source_ref": "context.network-segment", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.context.network-segment" + ], + "result_provenance_refs": [ + "provenance.context.network-segment" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.context.network-segment" + ], + "provenance_refs": [ + "provenance.context.network-segment" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "actions.scan", + "authorization_record_ref": "authorizations.scan", + "source_ref": "actions.scan", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.scan" + ], + "result_provenance_refs": [ + "provenance.scan" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.scan" + ], + "provenance_refs": [ + "provenance.scan" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "affordances.scanner", + "authorization_record_ref": "authorizations.affordances.scanner", + "source_ref": "affordances.scanner", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.affordances.scanner" + ], + "result_provenance_refs": [ + "provenance.affordances.scanner" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.affordances.scanner" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + } + ], + "evidence_refs": [ + "evidence.episode-running", + "evidence.authorization.context.network-segment", + "evidence.authorization.scan", + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "provenance.context.network-segment", + "provenance.scan", + "provenance.affordances.scanner" + ] + }, + "delivery": null +} diff --git a/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/legacy-observation-order.json b/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/legacy-observation-order.json new file mode 100644 index 000000000..5e5bdb406 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-decision-surface-v2/invalid/legacy-observation-order.json @@ -0,0 +1,261 @@ +{ + "schema_version": "participant-decision-surface/v2", + "surface_state": "projected", + "participant_view": { + "surface_id": "decision-surfaces.red.episode-1.epoch-0", + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "information_state_ref": "information-states.red.episode-1.epoch-0", + "context_view_ref": "context-views.red.episode-1.epoch-0", + "decision_control_mode": "agent", + "visible_context_refs": [ + "context.network-segment" + ], + "action_entries": [ + { + "entry_id": "scan", + "action_contract_address": "actions.scan", + "presentation_basis_ref": "presentation.scan", + "visibility": "observable", + "eligibility": "eligible", + "eligibility_reason_refs": [], + "constraint_refs": [ + "constraints.scan" + ], + "selection_shape_ref": "argument-shapes.scan", + "support": "supported", + "support_refs": [ + "support.scan" + ], + "affordance_refs": [ + "affordances.scanner" + ], + "realization_refs": [ + "realizations.scan" + ] + } + ], + "affordance_refs": [ + "affordances.scanner" + ], + "form": { + "surface_form": "candidate_action_set", + "selection_meaning_ref": "selection-meaning.candidate.v1", + "candidate_entry_ids": [ + "scan" + ], + "open_extension_binding_ref": null + }, + "marking_definition_refs": [ + "markings.participant" + ], + "redaction_policy_ref": "redaction.participant", + "semantic_limitations": [ + "limitations.bounded-surface" + ] + }, + "assurance": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "behavior_specification_address": "behavior-specifications.red", + "observation_boundary_address": "observation-boundaries.red", + "implementation_selection_ref": "implementation-selections.red", + "audience_scope_ref": "audiences.red", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "visibility_projection_ref": "visibility-projections.red.initial", + "participant_memory_scope": "persistent_across_episodes", + "memory_reset_authority_ref": null, + "participant_view_digest": "sha256:e17647852a76dfbb242ac3f66e057b4925f6b3ac9e5f2d195d5efa29ae8e0f30", + "derivation_anchor": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "participant-state-cuts.red.episode-1.initial", + "history_domain": "participant_episode_lifecycle", + "order_model": "control_plane_order", + "anchor_event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "anchor_order": 1, + "history_prefix_length": 2, + "predecessor_event_refs": [ + "participant-episode-event:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + }, + "evidence_refs": [ + "evidence.episode-running" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "anchor_kind": "episode_readiness", + "event_type": "episode_running", + "episode_sequence_number": 0 + }, + "exposure_bindings": [ + { + "item_ref": "context.network-segment", + "authorization_record_ref": "authorizations.context.network-segment", + "source_ref": "context.network-segment", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.context.network-segment" + ], + "result_provenance_refs": [ + "provenance.context.network-segment" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.context.network-segment" + ], + "provenance_refs": [ + "provenance.context.network-segment" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "actions.scan", + "authorization_record_ref": "authorizations.scan", + "source_ref": "actions.scan", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.scan" + ], + "result_provenance_refs": [ + "provenance.scan" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.scan" + ], + "provenance_refs": [ + "provenance.scan" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "affordances.scanner", + "authorization_record_ref": "authorizations.affordances.scanner", + "source_ref": "affordances.scanner", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.affordances.scanner" + ], + "result_provenance_refs": [ + "provenance.affordances.scanner" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.affordances.scanner" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + } + ], + "evidence_refs": [ + "evidence.episode-running", + "evidence.authorization.context.network-segment", + "evidence.authorization.scan", + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "provenance.context.network-segment", + "provenance.scan", + "provenance.affordances.scanner" + ] + }, + "delivery": null, + "observation_order": 0 +} diff --git a/contracts/fixtures/control-plane/participant-decision-surface-v2/valid/delivered-initial.json b/contracts/fixtures/control-plane/participant-decision-surface-v2/valid/delivered-initial.json new file mode 100644 index 000000000..60493e791 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-decision-surface-v2/valid/delivered-initial.json @@ -0,0 +1,281 @@ +{ + "schema_version": "participant-decision-surface/v2", + "surface_state": "delivered", + "participant_view": { + "surface_id": "decision-surfaces.red.episode-1.epoch-0", + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "information_state_ref": "information-states.red.episode-1.epoch-0", + "context_view_ref": "context-views.red.episode-1.epoch-0", + "decision_control_mode": "agent", + "visible_context_refs": [ + "context.network-segment" + ], + "action_entries": [ + { + "entry_id": "scan", + "action_contract_address": "actions.scan", + "presentation_basis_ref": "presentation.scan", + "visibility": "observable", + "eligibility": "eligible", + "eligibility_reason_refs": [], + "constraint_refs": [ + "constraints.scan" + ], + "selection_shape_ref": "argument-shapes.scan", + "support": "supported", + "support_refs": [ + "support.scan" + ], + "affordance_refs": [ + "affordances.scanner" + ], + "realization_refs": [ + "realizations.scan" + ] + } + ], + "affordance_refs": [ + "affordances.scanner" + ], + "form": { + "surface_form": "candidate_action_set", + "selection_meaning_ref": "selection-meaning.candidate.v1", + "candidate_entry_ids": [ + "scan" + ], + "open_extension_binding_ref": null + }, + "marking_definition_refs": [ + "markings.participant" + ], + "redaction_policy_ref": "redaction.participant", + "semantic_limitations": [ + "limitations.bounded-surface" + ] + }, + "assurance": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "behavior_specification_address": "behavior-specifications.red", + "observation_boundary_address": "observation-boundaries.red", + "implementation_selection_ref": "implementation-selections.red", + "audience_scope_ref": "audiences.red", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "visibility_projection_ref": "visibility-projections.red.initial", + "participant_memory_scope": "persistent_across_episodes", + "memory_reset_authority_ref": null, + "participant_view_digest": "sha256:e17647852a76dfbb242ac3f66e057b4925f6b3ac9e5f2d195d5efa29ae8e0f30", + "derivation_anchor": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "participant-state-cuts.red.episode-1.initial", + "history_domain": "participant_episode_lifecycle", + "order_model": "control_plane_order", + "anchor_event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "anchor_order": 1, + "history_prefix_length": 2, + "predecessor_event_refs": [ + "participant-episode-event:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + }, + "evidence_refs": [ + "evidence.episode-running" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "anchor_kind": "episode_readiness", + "event_type": "episode_running", + "episode_sequence_number": 0 + }, + "exposure_bindings": [ + { + "item_ref": "context.network-segment", + "authorization_record_ref": "authorizations.context.network-segment", + "source_ref": "context.network-segment", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.context.network-segment" + ], + "result_provenance_refs": [ + "provenance.context.network-segment" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.context.network-segment" + ], + "provenance_refs": [ + "provenance.context.network-segment" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "actions.scan", + "authorization_record_ref": "authorizations.scan", + "source_ref": "actions.scan", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.scan" + ], + "result_provenance_refs": [ + "provenance.scan" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.scan" + ], + "provenance_refs": [ + "provenance.scan" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "affordances.scanner", + "authorization_record_ref": "authorizations.affordances.scanner", + "source_ref": "affordances.scanner", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.affordances.scanner" + ], + "result_provenance_refs": [ + "provenance.affordances.scanner" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.affordances.scanner" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + } + ], + "evidence_refs": [ + "evidence.episode-running", + "evidence.authorization.context.network-segment", + "evidence.authorization.scan", + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "provenance.context.network-segment", + "provenance.scan", + "provenance.affordances.scanner" + ] + }, + "delivery": { + "delivery_ref": "decision-surface-deliveries.red.episode-1.epoch-0", + "surface_id": "decision-surfaces.red.episode-1.epoch-0", + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "participant_view_digest": "sha256:e17647852a76dfbb242ac3f66e057b4925f6b3ac9e5f2d195d5efa29ae8e0f30", + "delivery_basis": "emission_is_delivery", + "delivery_cut_ref": "participant-state-cuts.red.episode-1.initial", + "delivery_authorization_ref": "delivery-authorizations.red.episode-1.epoch-0", + "delivery_policy_decision_ref": "delivery-policy-decisions.red.episode-1.epoch-0", + "observation_ref": "participant-observations.red.episode-1.epoch-0", + "evidence_refs": [ + "evidence.surface-delivery" + ], + "provenance_refs": [ + "provenance.surface-delivery" + ], + "limitations": [ + "limitations.synchronous-delivery" + ] + } +} diff --git a/contracts/fixtures/control-plane/participant-decision-surface-v2/valid/projected-initial.json b/contracts/fixtures/control-plane/participant-decision-surface-v2/valid/projected-initial.json new file mode 100644 index 000000000..16ba3cc1e --- /dev/null +++ b/contracts/fixtures/control-plane/participant-decision-surface-v2/valid/projected-initial.json @@ -0,0 +1,260 @@ +{ + "schema_version": "participant-decision-surface/v2", + "surface_state": "projected", + "participant_view": { + "surface_id": "decision-surfaces.red.episode-1.epoch-0", + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "information_state_ref": "information-states.red.episode-1.epoch-0", + "context_view_ref": "context-views.red.episode-1.epoch-0", + "decision_control_mode": "agent", + "visible_context_refs": [ + "context.network-segment" + ], + "action_entries": [ + { + "entry_id": "scan", + "action_contract_address": "actions.scan", + "presentation_basis_ref": "presentation.scan", + "visibility": "observable", + "eligibility": "eligible", + "eligibility_reason_refs": [], + "constraint_refs": [ + "constraints.scan" + ], + "selection_shape_ref": "argument-shapes.scan", + "support": "supported", + "support_refs": [ + "support.scan" + ], + "affordance_refs": [ + "affordances.scanner" + ], + "realization_refs": [ + "realizations.scan" + ] + } + ], + "affordance_refs": [ + "affordances.scanner" + ], + "form": { + "surface_form": "candidate_action_set", + "selection_meaning_ref": "selection-meaning.candidate.v1", + "candidate_entry_ids": [ + "scan" + ], + "open_extension_binding_ref": null + }, + "marking_definition_refs": [ + "markings.participant" + ], + "redaction_policy_ref": "redaction.participant", + "semantic_limitations": [ + "limitations.bounded-surface" + ] + }, + "assurance": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "behavior_specification_address": "behavior-specifications.red", + "observation_boundary_address": "observation-boundaries.red", + "implementation_selection_ref": "implementation-selections.red", + "audience_scope_ref": "audiences.red", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "visibility_projection_ref": "visibility-projections.red.initial", + "participant_memory_scope": "persistent_across_episodes", + "memory_reset_authority_ref": null, + "participant_view_digest": "sha256:e17647852a76dfbb242ac3f66e057b4925f6b3ac9e5f2d195d5efa29ae8e0f30", + "derivation_anchor": { + "participant_address": "participants.red", + "episode_id": "episode-1", + "decision_epoch": 0, + "event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "participant-state-cuts.red.episode-1.initial", + "history_domain": "participant_episode_lifecycle", + "order_model": "control_plane_order", + "anchor_event_ref": "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "anchor_order": 1, + "history_prefix_length": 2, + "predecessor_event_refs": [ + "participant-episode-event:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + }, + "evidence_refs": [ + "evidence.episode-running" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "anchor_kind": "episode_readiness", + "event_type": "episode_running", + "episode_sequence_number": 0 + }, + "exposure_bindings": [ + { + "item_ref": "context.network-segment", + "authorization_record_ref": "authorizations.context.network-segment", + "source_ref": "context.network-segment", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.context.network-segment" + ], + "result_provenance_refs": [ + "provenance.context.network-segment" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.context.network-segment" + ], + "provenance_refs": [ + "provenance.context.network-segment" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "actions.scan", + "authorization_record_ref": "authorizations.scan", + "source_ref": "actions.scan", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.scan" + ], + "result_provenance_refs": [ + "provenance.scan" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.scan" + ], + "provenance_refs": [ + "provenance.scan" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + }, + { + "item_ref": "affordances.scanner", + "authorization_record_ref": "authorizations.affordances.scanner", + "source_ref": "affordances.scanner", + "source_layer_ref": "source-layer.scenario", + "participant_address": "participants.red", + "episode_id": "episode-1", + "audience_scope_ref": "audiences.red", + "decision_epoch": 0, + "decision_cut_ref": "participant-state-cuts.red.episode-1.initial", + "visibility_basis_ref": "view-rules.scan", + "projection_policy_ref": "projection-policies.red", + "projection_policy_revision": "revision-1", + "projection_policy_decision_ref": "policy-decisions.red.initial", + "exposure_policy_ref": "exposure-policies.red", + "exposure_policy_version": "1", + "exposure_policy_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "operation": "projection", + "operation_basis_ref": "operations.project", + "actor_ref": "actors.runtime", + "controller_ref": "controllers.runtime", + "authority_basis_ref": "authority.participant", + "source_marking_definition_refs": [ + "markings.participant" + ], + "result_marking_definition_refs": [ + "markings.participant" + ], + "source_provenance_refs": [ + "provenance.affordances.scanner" + ], + "result_provenance_refs": [ + "provenance.affordances.scanner" + ], + "declassification_basis_ref": null, + "redaction_policy_ref": null, + "transformation_rule_ref": null, + "evidence_refs": [ + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.affordances.scanner" + ], + "loss_and_limitations": [ + "limitations.authorization" + ] + } + ], + "evidence_refs": [ + "evidence.episode-running", + "evidence.authorization.context.network-segment", + "evidence.authorization.scan", + "evidence.authorization.affordances.scanner" + ], + "provenance_refs": [ + "provenance.episode-running", + "participant-episode-event:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "provenance.context.network-segment", + "provenance.scan", + "provenance.affordances.scanner" + ] + }, + "delivery": null +} diff --git a/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json index 247de33ab..861e9114d 100644 --- a/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json @@ -13,7 +13,7 @@ "behavioral_claims": [ { "taxonomy_id": "aces-behavioral-relations", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "relation_id": "empirical-adequacy", "subject": "TechVault baseline study", "left_carrier_ref": "study-techvault-baseline@1.0.0", diff --git a/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json b/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json index 0b70db930..12f2cb856 100644 --- a/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json +++ b/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json @@ -12,7 +12,8 @@ "participant-configuration-result-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", - "participant-behavior-history-event-stream-v1" + "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2" ], "compatibility": { "participant_runtimes": [ @@ -54,7 +55,8 @@ "supported_participant_contracts": [ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", - "participant-behavior-history-event-stream-v1" + "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2" ], "supported_decision_surface_modes": [ "autonomous", diff --git a/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json b/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json index f9c4c26b1..47d5ddb1d 100644 --- a/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json +++ b/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json @@ -15,7 +15,8 @@ "selected_decision_surface_mode": "policy-directed", "participant_contract_versions": [ "participant-episode-state-envelope-v1", - "participant-behavior-history-event-stream-v1" + "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2" ], "exposure_policy": { "policy_id": "red-agent-policy", diff --git a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json index d1a2251b0..c14fdb7d4 100644 --- a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json +++ b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json @@ -19,7 +19,7 @@ "behavioral_claims": [ { "taxonomy_id": "aces-behavioral-relations", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "relation_id": "semantic-validity", "subject": "One SDL source", "left_carrier_ref": "fixture:source", diff --git a/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json index fe609d562..8f2dd0420 100644 --- a/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json +++ b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json @@ -56,7 +56,7 @@ "explicit_non_claims": ["Does not claim deployability, realization, experimental control, reproducibility, backend conformance, or behavioral equivalence."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "semantic-validity", + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev3", "relation_id": "semantic-validity", "subject": "An artifact admitted by the valid-sdl-fragment profile", "left_carrier_ref": "scientific-scenario-completeness@rev1:valid-sdl-fragment", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "Parser and semantic validator acceptance for the submitted SDL artifact under its declared profile.", "assurance_status": "tested", "evidence_refs": ["implementations/python/packages/raes/validator/", "implementations/python/tests/"], @@ -87,7 +87,7 @@ "explicit_non_claims": ["Does not claim that every backend can realize the scenario, that deployment succeeded, or that realized behavior is equivalent."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev3", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against deployable-scenario-intent", "left_carrier_ref": "candidate-scenario", "right_carrier_ref": "scientific-scenario-completeness@rev1:deployable-scenario-intent", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The required concern dispositions for one candidate artifact and its named external contract witnesses.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], @@ -118,7 +118,7 @@ "explicit_non_claims": ["Does not claim participant implementations are strategically equivalent, that hidden global state is visible, or that finite probes prove bisimulation."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev3", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against participant-evaluation-scenario", "left_carrier_ref": "candidate-scenario-and-experiment-bindings", "right_carrier_ref": "scientific-scenario-completeness@rev1:participant-evaluation-scenario", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The required concern dispositions and contract witnesses for one bound participant-evaluation scenario.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], @@ -148,7 +148,7 @@ "explicit_non_claims": ["Does not claim causal validity, successful execution, adequate power, reproducibility, or backend behavioral equivalence."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev3", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against controlled-experiment-scenario", "left_carrier_ref": "candidate-scenario-and-study-design", "right_carrier_ref": "scientific-scenario-completeness@rev1:controlled-experiment-scenario", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The pre-run design declarations and required concern witnesses for one controlled-experiment input.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], @@ -178,7 +178,7 @@ "explicit_non_claims": ["Does not guarantee bitwise replay, equal outcomes, scientific validity, generalizability, or behavioral equivalence across apparatuses."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev3", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against reproducible-benchmark-study-input", "left_carrier_ref": "candidate-scenario-and-governed-experiment-bundle", "right_carrier_ref": "scientific-scenario-completeness@rev1:reproducible-benchmark-study-input", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The preserved design, apparatus, participant, evidence, lineage, and analysis declarations for one benchmark or study input.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], diff --git a/contracts/provenance/sdl-lineage-ledger-v1.json b/contracts/provenance/sdl-lineage-ledger-v1.json index f370e2945..acf7694e1 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-24", + "reviewed_on": "2026-07-26", "citations": [ { "citation_id": "ocr-sdl-v0.21.2", @@ -229,6 +229,94 @@ "verified_on": "2026-07-17", "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#participant-information-flow-sources" }, + { + "citation_id": "abadi-lamport-1991", + "title": "The Existence of Refinement Mappings", + "authors_or_maintainer": [ + "Martín Abadi", + "Leslie Lamport" + ], + "year": 1991, + "container_title": "Theoretical Computer Science 82(2)", + "doi": "10.1016/0304-3975(91)90224-P", + "canonical_url": "https://doi.org/10.1016/0304-3975(91)90224-P", + "verified_on": "2026-07-26", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-909-refinement-io-and-reactive-information-flow-sources" + }, + { + "citation_id": "lynch-vaandrager-1995", + "title": "Forward and Backward Simulations, Part I: Untimed Systems", + "authors_or_maintainer": [ + "Nancy A. Lynch", + "Frits W. Vaandrager" + ], + "year": 1995, + "container_title": "Information and Computation 121(2)", + "doi": "10.1006/inco.1995.1134", + "canonical_url": "https://doi.org/10.1006/inco.1995.1134", + "verified_on": "2026-07-26", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-909-refinement-io-and-reactive-information-flow-sources" + }, + { + "citation_id": "alur-henzinger-kupferman-vardi-1998", + "title": "Alternating Refinement Relations", + "authors_or_maintainer": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman", + "Moshe Y. Vardi" + ], + "year": 1998, + "container_title": "CONCUR 1998, LNCS 1466", + "doi": "10.1007/BFb0055622", + "canonical_url": "https://doi.org/10.1007/BFb0055622", + "verified_on": "2026-07-26", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-909-refinement-io-and-reactive-information-flow-sources" + }, + { + "citation_id": "lynch-tuttle-1989", + "title": "An Introduction to Input/Output Automata", + "authors_or_maintainer": [ + "Nancy A. Lynch", + "Mark R. Tuttle" + ], + "year": 1989, + "container_title": "CWI Quarterly 2(3), 219-246; MIT/LCS/TM-373", + "canonical_url": "https://groups.csail.mit.edu/tds/papers/Lynch/CWI89.html", + "verified_on": "2026-07-26", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-909-refinement-io-and-reactive-information-flow-sources" + }, + { + "citation_id": "clarkson-schneider-2010", + "title": "Hyperproperties", + "authors_or_maintainer": [ + "Michael R. Clarkson", + "Fred B. Schneider" + ], + "year": 2010, + "container_title": "Journal of Computer Security 18(6)", + "doi": "10.3233/JCS-2009-0393", + "canonical_url": "https://doi.org/10.3233/JCS-2009-0393", + "verified_on": "2026-07-26", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-909-refinement-io-and-reactive-information-flow-sources" + }, + { + "citation_id": "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "title": "Reactive Noninterference", + "authors_or_maintainer": [ + "Aaron Bohannon", + "Benjamin C. Pierce", + "Vilhelm Sjöberg", + "Stephanie Weirich", + "Steve Zdancewic" + ], + "year": 2009, + "container_title": "Proceedings of the 16th ACM Conference on Computer and Communications Security", + "doi": "10.1145/1653662.1653673", + "canonical_url": "https://doi.org/10.1145/1653662.1653673", + "verified_on": "2026-07-26", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-909-refinement-io-and-reactive-information-flow-sources" + }, { "citation_id": "crack-nca-2018", "title": "Scenario Design and Validation for Next Generation Cyber Ranges", @@ -440,6 +528,54 @@ "version_or_edition": "published conference chapter", "canonical_url": "https://doi.org/10.1007/BFb0039066", "citation_ref": "van-glabbeek-1990" + }, + { + "source_id": "abadi-lamport-1991", + "kind": "publication", + "title": "The Existence of Refinement Mappings", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.1016/0304-3975(91)90224-P", + "citation_ref": "abadi-lamport-1991" + }, + { + "source_id": "lynch-vaandrager-1995", + "kind": "publication", + "title": "Forward and Backward Simulations, Part I: Untimed Systems", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.1006/inco.1995.1134", + "citation_ref": "lynch-vaandrager-1995" + }, + { + "source_id": "alur-henzinger-kupferman-vardi-1998", + "kind": "publication", + "title": "Alternating Refinement Relations", + "version_or_edition": "published conference chapter", + "canonical_url": "https://doi.org/10.1007/BFb0055622", + "citation_ref": "alur-henzinger-kupferman-vardi-1998" + }, + { + "source_id": "lynch-tuttle-1989", + "kind": "publication", + "title": "An Introduction to Input/Output Automata", + "version_or_edition": "CWI Quarterly article and MIT/LCS/TM-373", + "canonical_url": "https://groups.csail.mit.edu/tds/papers/Lynch/CWI89.html", + "citation_ref": "lynch-tuttle-1989" + }, + { + "source_id": "clarkson-schneider-2010", + "kind": "publication", + "title": "Hyperproperties", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.3233/JCS-2009-0393", + "citation_ref": "clarkson-schneider-2010" + }, + { + "source_id": "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "kind": "publication", + "title": "Reactive Noninterference", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.1145/1653662.1653673", + "citation_ref": "bohannon-pierce-sjoberg-weirich-zdancewic-2009" } ], "subjects": [ @@ -2907,7 +3043,9 @@ "goguen-meseguer-1982", "sabelfeld-sands-2009", "milner-1980", - "van-glabbeek-1990" + "van-glabbeek-1990", + "clarkson-schneider-2010", + "bohannon-pierce-sjoberg-weirich-zdancewic-2009" ], "aces_boundaries": [ { @@ -2939,25 +3077,88 @@ { "artifact": "The Linear Time-Branching Time Spectrum", "symbol_or_pointer": "separation of trace and branching-time relations" + }, + { + "artifact": "Hyperproperties", + "symbol_or_pointer": "sets-of-traces account of information-flow properties" + }, + { + "artifact": "Reactive Noninterference", + "symbol_or_pointer": "strategy-sensitive reactive information-flow comparison" } ], - "divergence": "ACES composes interpreted-system local-state indistinguishability, policy noninterference and purge, explicit declassification dimensions, and labelled-transition hidden-action discipline into a participant-, audience-, policy-revision-, scheduler/environment-, and order-relative support-set relation over ACES-native carriers. Its ordering construction remains indirectly derived through ADR-054 from Lamport happened-before, Winskel event structures, and Mazurkiewicz trace theory rather than being redefined here. ACES adds only carrier, policy-revision, marking, controller/authority, ordering, evidence, and assurance coordinates needed by the governed participant model. It does not copy source syntax, define source-compatible wire contracts, claim universal proof, or claim runtime/backend realization.", + "divergence": "ACES composes interpreted-system local-state indistinguishability, policy noninterference and purge, explicit declassification dimensions, labelled-transition hidden-action discipline, the hyperproperty distinction, and reactive strategy quantification into a participant-, audience-, exact-cut policy-decision-, memory-scope-, scheduler/environment-, and order-relative support-set relation over ACES-native carriers. Its ordering construction remains indirectly derived through ADR-054 from Lamport happened-before, Winskel event structures, and Mazurkiewicz trace theory rather than being redefined here. ACES adds only carrier, decision-cut, memory, marking, controller/authority, evidence, and assurance coordinates needed by the governed participant model. It does not copy source syntax, define source-compatible wire contracts, claim universal proof, or claim runtime/backend realization.", "compatibility": "none", "citation_refs": [ "fagin-halpern-moses-vardi-1995", "goguen-meseguer-1982", "sabelfeld-sands-2009", "milner-1980", - "van-glabbeek-1990" + "van-glabbeek-1990", + "clarkson-schneider-2010", + "bohannon-pierce-sjoberg-weirich-zdancewic-2009" ], "internal_authority_refs": [ "docs/decisions/adrs/adr-085-participant-information-flow-and-control.md", + "docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md", "docs/decisions/adrs/adr-054-participant-runtime-observable-lifecycle.md", "specs/formal/participant-semantics/information-flow-control.md", "specs/formal/participant-runtime/README.md", "specs/formal/behavioral-relations/README.md" ], "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "abadi-lamport-1991", + "lynch-vaandrager-1995", + "lynch-tuttle-1989", + "alur-henzinger-kupferman-vardi-1998" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/behavioral-relations-v1.json", + "symbol_or_pointer": "#/relations/io-alternating-refinement" + }, + { + "artifact": "docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md", + "symbol_or_pointer": "Backend realization, simulation, and bisimulation" + } + ], + "source_boundaries": [ + { + "artifact": "The Existence of Refinement Mappings", + "symbol_or_pointer": "lower-level to higher-level state/behavior mapping" + }, + { + "artifact": "Forward and Backward Simulations, Part I: Untimed Systems", + "symbol_or_pointer": "directional simulation proof methods" + }, + { + "artifact": "An Introduction to Input/Output Automata", + "symbol_or_pointer": "input/output ownership and input-enabled component model" + }, + { + "artifact": "Alternating Refinement Relations", + "symbol_or_pointer": "separate quantification over system and environment choices" + } + ], + "divergence": "ACES uses these results to state a directional backend obligation: concrete participant traces must remain admitted by the abstract RAES semantics, while participant/environment inputs, participant-facing outputs, ownership, availability, fairness, and exact delivery remain explicit alternating obligations. The v2 contract is not an I/O-automaton wire format, does not make bisimulation the default conformance relation, and does not claim the universal relation from finite tests. Bisimulation remains optional, projection-relative evidence for a separately declared claim.", + "compatibility": "none", + "citation_refs": [ + "abadi-lamport-1991", + "lynch-vaandrager-1995", + "lynch-tuttle-1989", + "alur-henzinger-kupferman-vardi-1998" + ], + "internal_authority_refs": [ + "docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md", + "specs/formal/behavioral-relations/README.md", + "specs/formal/participant-semantics/README.md" + ], + "compatibility_direction": "aces_relative_to_source" } ] }, diff --git a/contracts/schema-publication/entries/behavioral-relations-v1.json b/contracts/schema-publication/entries/behavioral-relations-v1.json index 479825dd1..ad6d777ac 100644 --- a/contracts/schema-publication/entries/behavioral-relations-v1.json +++ b/contracts/schema-publication/entries/behavioral-relations-v1.json @@ -2,9 +2,9 @@ "contract_id": "behavioral-relations-v1", "schema_path": "contracts/schemas/concept-authority/behavioral-relations-v1.json", "stability": "draft", - "content_hash": "e480fac32221c864dc953eef2700459a3db4b8a435abd02e69fc767ed7e46d76", + "content_hash": "4192c58c13d05830c73a089ea9fce462f2aa28d79209a6a1043bdbf8ddbd41d4", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "e480fac32221c864dc953eef2700459a3db4b8a435abd02e69fc767ed7e46d76" + "summary": "Added an immutable report locator for the revision-3 I/O-automata lineage used by exact-cut reactive information flow and I/O alternating refinement.", + "content_hash": "4192c58c13d05830c73a089ea9fce462f2aa28d79209a6a1043bdbf8ddbd41d4" } } diff --git a/contracts/schema-publication/entries/participant-decision-surface-v1.json b/contracts/schema-publication/entries/participant-decision-surface-v1.json index 2df8e6bd0..0e1554909 100644 --- a/contracts/schema-publication/entries/participant-decision-surface-v1.json +++ b/contracts/schema-publication/entries/participant-decision-surface-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-decision-surface-v1", "schema_path": "contracts/schemas/control-plane/participant-decision-surface-v1.json", "stability": "draft", - "content_hash": "e65ab17f47266f0b21964e5d97517b3d5e9ff998454c48605d2fc40057630c13", + "content_hash": "77920c1edfa6fe1785d50d6d33a194acd59fdc41845c016eedbf88af342bd2e2", "last_change": { - "summary": "Breaking semantic and runtime-admission change to the draft v1 contract: added a structurally optional tagged projection anchor, defined observation_order as the derived per-episode decision-surface order distinct from anchor-local history order, and required a current resolved anchor for runtime admission.", - "content_hash": "e65ab17f47266f0b21964e5d97517b3d5e9ff998454c48605d2fc40057630c13" + "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", + "content_hash": "77920c1edfa6fe1785d50d6d33a194acd59fdc41845c016eedbf88af342bd2e2" } } diff --git a/contracts/schema-publication/entries/participant-decision-surface-v2.json b/contracts/schema-publication/entries/participant-decision-surface-v2.json new file mode 100644 index 000000000..21794665e --- /dev/null +++ b/contracts/schema-publication/entries/participant-decision-surface-v2.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-decision-surface-v2", + "schema_path": "contracts/schemas/control-plane/participant-decision-surface-v2.json", + "stability": "draft", + "content_hash": "216548cdffc0b81d057a87d4932374cc83d00abd0b023510044d52d1945fa9d4", + "last_change": { + "summary": "Published decision-surface v2 with independent decision epochs and state cuts, participant-view and assurance separation, exact-cut exposure authorization, explicit memory scope and delivery state, and delivery-bound selection.", + "content_hash": "216548cdffc0b81d057a87d4932374cc83d00abd0b023510044d52d1945fa9d4" + } +} diff --git a/contracts/schema-publication/entries/participant-implementation-manifest-v1.json b/contracts/schema-publication/entries/participant-implementation-manifest-v1.json index f6578bf51..11da33b8c 100644 --- a/contracts/schema-publication/entries/participant-implementation-manifest-v1.json +++ b/contracts/schema-publication/entries/participant-implementation-manifest-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-implementation-manifest-v1", "schema_path": "contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json", "stability": "draft", - "content_hash": "5b7c636a98c540e1afc74ca74a1e9a421609f498c3b85c473a52c1516e398984", + "content_hash": "c74cee900e2e0e89b442f54e9fe597beb88393bf0aa6ef27ee3d76642354817f", "last_change": { - "summary": "Added typed participant configuration targets, aliases, defaults, sensitivity, and governed validator ownership.", - "content_hash": "5b7c636a98c540e1afc74ca74a1e9a421609f498c3b85c473a52c1516e398984" + "summary": "Allowed participant implementations to declare exact-cut participant-decision-surface-v2 capability.", + "content_hash": "c74cee900e2e0e89b442f54e9fe597beb88393bf0aa6ef27ee3d76642354817f" } } diff --git a/contracts/schemas/concept-authority/behavioral-relations-v1.json b/contracts/schemas/concept-authority/behavioral-relations-v1.json index 262535f64..a1749dee3 100644 --- a/contracts/schemas/concept-authority/behavioral-relations-v1.json +++ b/contracts/schemas/concept-authority/behavioral-relations-v1.json @@ -381,7 +381,8 @@ "kind": { "enum": [ "doi", - "isbn" + "isbn", + "report" ], "title": "Kind", "type": "string" diff --git a/contracts/schemas/control-plane/participant-decision-surface-v1.json b/contracts/schemas/control-plane/participant-decision-surface-v1.json index 52f448346..2b9bfb315 100644 --- a/contracts/schemas/control-plane/participant-decision-surface-v1.json +++ b/contracts/schemas/control-plane/participant-decision-surface-v1.json @@ -153,90 +153,6 @@ "title": "ParticipantDecisionSurfaceActionEntryModel", "type": "object" }, - "ParticipantDecisionSurfaceBehaviorAnchorModel": { - "additionalProperties": false, - "description": "One terminal observation and exact prefix anchoring a later surface.", - "properties": { - "action_instance_id": { - "minLength": 1, - "title": "Action Instance Id", - "type": "string" - }, - "anchor_kind": { - "const": "behavior_event", - "title": "Anchor Kind", - "type": "string" - }, - "anchor_order": { - "minimum": 0, - "title": "Anchor Order", - "type": "integer" - }, - "decision_surface_order": { - "minimum": 0, - "title": "Decision Surface Order", - "type": "integer" - }, - "episode_id": { - "minLength": 1, - "title": "Episode Id", - "type": "string" - }, - "event_ref": { - "minLength": 1, - "title": "Event Ref", - "type": "string" - }, - "event_type": { - "const": "observation_emitted", - "title": "Event Type", - "type": "string" - }, - "evidence_refs": { - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "title": "Evidence Refs", - "type": "array" - }, - "history_prefix_length": { - "minimum": 1, - "title": "History Prefix Length", - "type": "integer" - }, - "participant_address": { - "minLength": 1, - "title": "Participant Address", - "type": "string" - }, - "provenance_refs": { - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "title": "Provenance Refs", - "type": "array" - } - }, - "required": [ - "participant_address", - "episode_id", - "decision_surface_order", - "event_ref", - "anchor_order", - "evidence_refs", - "provenance_refs", - "anchor_kind", - "event_type", - "action_instance_id", - "history_prefix_length" - ], - "title": "ParticipantDecisionSurfaceBehaviorAnchorModel", - "type": "object" - }, "ParticipantDecisionSurfaceCandidateSetFormModel": { "additionalProperties": false, "properties": { @@ -370,84 +286,6 @@ "title": "ParticipantDecisionSurfaceConstrainedFormModel", "type": "object" }, - "ParticipantDecisionSurfaceEpisodeReadinessAnchorModel": { - "additionalProperties": false, - "description": "RUN-311 ``episode_running`` anchor for one episode's initial surface.", - "properties": { - "anchor_kind": { - "const": "episode_readiness", - "title": "Anchor Kind", - "type": "string" - }, - "anchor_order": { - "minimum": 0, - "title": "Anchor Order", - "type": "integer" - }, - "decision_surface_order": { - "minimum": 0, - "title": "Decision Surface Order", - "type": "integer" - }, - "episode_id": { - "minLength": 1, - "title": "Episode Id", - "type": "string" - }, - "episode_sequence_number": { - "minimum": 0, - "title": "Episode Sequence Number", - "type": "integer" - }, - "event_ref": { - "minLength": 1, - "title": "Event Ref", - "type": "string" - }, - "event_type": { - "const": "episode_running", - "title": "Event Type", - "type": "string" - }, - "evidence_refs": { - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "title": "Evidence Refs", - "type": "array" - }, - "participant_address": { - "minLength": 1, - "title": "Participant Address", - "type": "string" - }, - "provenance_refs": { - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "title": "Provenance Refs", - "type": "array" - } - }, - "required": [ - "participant_address", - "episode_id", - "decision_surface_order", - "event_ref", - "anchor_order", - "evidence_refs", - "provenance_refs", - "anchor_kind", - "event_type", - "episode_sequence_number" - ], - "title": "ParticipantDecisionSurfaceEpisodeReadinessAnchorModel", - "type": "object" - }, "ParticipantDecisionSurfaceExposureBindingModel": { "additionalProperties": false, "description": "Resolved SEM-226 basis for one item admitted to a surface.", @@ -950,32 +788,6 @@ "title": "Participant Address", "type": "string" }, - "projection_anchor": { - "anyOf": [ - { - "discriminator": { - "mapping": { - "behavior_event": "#/$defs/ParticipantDecisionSurfaceBehaviorAnchorModel", - "episode_readiness": "#/$defs/ParticipantDecisionSurfaceEpisodeReadinessAnchorModel" - }, - "propertyName": "anchor_kind" - }, - "oneOf": [ - { - "$ref": "#/$defs/ParticipantDecisionSurfaceEpisodeReadinessAnchorModel" - }, - { - "$ref": "#/$defs/ParticipantDecisionSurfaceBehaviorAnchorModel" - } - ] - }, - { - "type": "null" - } - ], - "default": null, - "title": "Projection Anchor" - }, "projection_policy_ref": { "minLength": 1, "title": "Projection Policy Ref", @@ -1099,18 +911,6 @@ ], "level": "error", "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_surface_relations" - }, - { - "description": "A typed projection anchor, when present, must agree with the surface participant, episode, anchor-local decision-surface order, evidence, and provenance without merging lifecycle and behavior histories.", - "id": "decision-surface-projection-anchor-agreement", - "inputs": [ - { - "contract_id": "participant-decision-surface-v1", - "instance_path": "#/projection_anchor" - } - ], - "level": "error", - "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_projection_anchor" } ], "x-aces-semantic-profile": { diff --git a/contracts/schemas/control-plane/participant-decision-surface-v2.json b/contracts/schemas/control-plane/participant-decision-surface-v2.json new file mode 100644 index 000000000..d1934f837 --- /dev/null +++ b/contracts/schemas/control-plane/participant-decision-surface-v2.json @@ -0,0 +1,1482 @@ +{ + "$defs": { + "ParticipantDecisionSurfaceActionEntryModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "eligibility": { + "enum": [ + "ineligible", + "unknown", + "unsupported" + ] + } + }, + "required": [ + "eligibility" + ] + }, + "then": { + "properties": { + "eligibility_reason_refs": { + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "support": { + "const": "supported" + } + }, + "required": [ + "support" + ] + }, + "then": { + "properties": { + "support_refs": { + "minItems": 1 + } + } + } + } + ], + "description": "One presented or generable action without lifecycle implications.", + "properties": { + "action_contract_address": { + "minLength": 1, + "title": "Action Contract Address", + "type": "string" + }, + "affordance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Affordance Refs", + "type": "array" + }, + "constraint_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Constraint Refs", + "type": "array" + }, + "eligibility": { + "enum": [ + "eligible", + "ineligible", + "unknown", + "unsupported" + ], + "title": "Eligibility", + "type": "string" + }, + "eligibility_reason_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Eligibility Reason Refs", + "type": "array" + }, + "entry_id": { + "minLength": 1, + "title": "Entry Id", + "type": "string" + }, + "presentation_basis_ref": { + "minLength": 1, + "title": "Presentation Basis Ref", + "type": "string" + }, + "realization_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Realization Refs", + "type": "array" + }, + "selection_shape_ref": { + "minLength": 1, + "title": "Selection Shape Ref", + "type": "string" + }, + "support": { + "enum": [ + "supported", + "unsupported", + "unknown" + ], + "title": "Support", + "type": "string" + }, + "support_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Support Refs", + "type": "array" + }, + "visibility": { + "enum": [ + "observable", + "discovered", + "inferred", + "disclosed", + "deceptive" + ], + "title": "Visibility", + "type": "string" + } + }, + "required": [ + "entry_id", + "action_contract_address", + "presentation_basis_ref", + "visibility", + "eligibility", + "constraint_refs", + "selection_shape_ref", + "support" + ], + "title": "ParticipantDecisionSurfaceActionEntryModel", + "type": "object" + }, + "ParticipantDecisionSurfaceAssuranceV2Model": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "participant_memory_scope": { + "const": "episode_local_reset" + } + }, + "required": [ + "participant_memory_scope" + ] + }, + "then": { + "properties": { + "memory_reset_authority_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "memory_reset_authority_ref" + ] + } + }, + { + "if": { + "properties": { + "participant_memory_scope": { + "const": "persistent_across_episodes" + } + }, + "required": [ + "participant_memory_scope" + ] + }, + "then": { + "properties": { + "memory_reset_authority_ref": { + "type": "null" + } + } + } + } + ], + "description": "Trusted derivation, policy, provenance, and evidence for one view.", + "properties": { + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "behavior_specification_address": { + "minLength": 1, + "title": "Behavior Specification Address", + "type": "string" + }, + "decision_epoch": { + "minimum": 0, + "title": "Decision Epoch", + "type": "integer" + }, + "derivation_anchor": { + "discriminator": { + "mapping": { + "behavior_event": "#/$defs/ParticipantDecisionSurfaceBehaviorAnchorV2Model", + "episode_readiness": "#/$defs/ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model" + }, + "propertyName": "anchor_kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model" + }, + { + "$ref": "#/$defs/ParticipantDecisionSurfaceBehaviorAnchorV2Model" + } + ], + "title": "Derivation Anchor" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "exposure_bindings": { + "items": { + "$ref": "#/$defs/ParticipantDecisionSurfaceExposureBindingV2Model" + }, + "minItems": 1, + "title": "Exposure Bindings", + "type": "array" + }, + "exposure_policy_ref": { + "minLength": 1, + "title": "Exposure Policy Ref", + "type": "string" + }, + "implementation_selection_ref": { + "minLength": 1, + "title": "Implementation Selection Ref", + "type": "string" + }, + "memory_reset_authority_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Memory Reset Authority Ref" + }, + "observation_boundary_address": { + "minLength": 1, + "title": "Observation Boundary Address", + "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" + }, + "participant_view_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Participant View Digest", + "type": "string" + }, + "projection_policy_decision_ref": { + "minLength": 1, + "title": "Projection Policy Decision Ref", + "type": "string" + }, + "projection_policy_ref": { + "minLength": 1, + "title": "Projection Policy Ref", + "type": "string" + }, + "projection_policy_revision": { + "minLength": 1, + "title": "Projection Policy Revision", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "visibility_projection_ref": { + "minLength": 1, + "title": "Visibility Projection Ref", + "type": "string" + } + }, + "required": [ + "participant_address", + "episode_id", + "decision_epoch", + "behavior_specification_address", + "observation_boundary_address", + "implementation_selection_ref", + "audience_scope_ref", + "projection_policy_ref", + "projection_policy_revision", + "projection_policy_decision_ref", + "exposure_policy_ref", + "visibility_projection_ref", + "participant_memory_scope", + "participant_view_digest", + "derivation_anchor", + "exposure_bindings", + "evidence_refs", + "provenance_refs" + ], + "title": "ParticipantDecisionSurfaceAssuranceV2Model", + "type": "object" + }, + "ParticipantDecisionSurfaceBehaviorAnchorV2Model": { + "additionalProperties": false, + "description": "A later decision epoch derived from a terminal participant observation.", + "properties": { + "action_instance_id": { + "minLength": 1, + "title": "Action Instance Id", + "type": "string" + }, + "anchor_kind": { + "const": "behavior_event", + "title": "Anchor Kind", + "type": "string" + }, + "decision_epoch": { + "minimum": 0, + "title": "Decision Epoch", + "type": "integer" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "event_ref": { + "minLength": 1, + "title": "Event Ref", + "type": "string" + }, + "event_type": { + "const": "observation_emitted", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "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" + } + }, + "required": [ + "participant_address", + "episode_id", + "decision_epoch", + "event_ref", + "state_cut", + "evidence_refs", + "provenance_refs", + "anchor_kind", + "event_type", + "action_instance_id" + ], + "title": "ParticipantDecisionSurfaceBehaviorAnchorV2Model", + "type": "object" + }, + "ParticipantDecisionSurfaceCandidateSetFormModel": { + "additionalProperties": false, + "properties": { + "candidate_entry_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Candidate Entry Ids", + "type": "array" + }, + "open_extension_binding_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Open Extension Binding Ref" + }, + "selection_meaning_ref": { + "minLength": 1, + "title": "Selection Meaning Ref", + "type": "string" + }, + "surface_form": { + "const": "candidate_action_set", + "title": "Surface Form", + "type": "string" + } + }, + "required": [ + "surface_form", + "selection_meaning_ref", + "candidate_entry_ids" + ], + "title": "ParticipantDecisionSurfaceCandidateSetFormModel", + "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" + }, + "ParticipantDecisionSurfaceConstrainedFormModel": { + "additionalProperties": false, + "properties": { + "action_entry_id": { + "minLength": 1, + "title": "Action Entry Id", + "type": "string" + }, + "argument_shape_ref": { + "minLength": 1, + "title": "Argument Shape Ref", + "type": "string" + }, + "constraint_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Constraint Refs", + "type": "array" + }, + "default_disclosure_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Default Disclosure Refs", + "type": "array" + }, + "loss_disclosure_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss Disclosure Refs", + "type": "array" + }, + "normalization_disclosure_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Normalization Disclosure Refs", + "type": "array" + }, + "omission_disclosure_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Omission Disclosure Refs", + "type": "array" + }, + "selection_meaning_ref": { + "minLength": 1, + "title": "Selection Meaning Ref", + "type": "string" + }, + "surface_form": { + "const": "constrained_form", + "title": "Surface Form", + "type": "string" + }, + "validation_policy_ref": { + "minLength": 1, + "title": "Validation Policy Ref", + "type": "string" + } + }, + "required": [ + "surface_form", + "selection_meaning_ref", + "action_entry_id", + "argument_shape_ref", + "validation_policy_ref", + "constraint_refs", + "default_disclosure_refs", + "normalization_disclosure_refs", + "omission_disclosure_refs", + "loss_disclosure_refs" + ], + "title": "ParticipantDecisionSurfaceConstrainedFormModel", + "type": "object" + }, + "ParticipantDecisionSurfaceDeliveryV2Model": { + "additionalProperties": false, + "description": "Evidence that the exact participant view became available to its subject.", + "properties": { + "decision_epoch": { + "minimum": 0, + "title": "Decision Epoch", + "type": "integer" + }, + "delivery_authorization_ref": { + "minLength": 1, + "title": "Delivery Authorization Ref", + "type": "string" + }, + "delivery_basis": { + "enum": [ + "emission_is_delivery", + "runtime_delivery", + "participant_acknowledgement", + "external_delivery", + "unknown", + "unsupported" + ], + "title": "Delivery Basis", + "type": "string" + }, + "delivery_cut_ref": { + "minLength": 1, + "title": "Delivery Cut Ref", + "type": "string" + }, + "delivery_policy_decision_ref": { + "minLength": 1, + "title": "Delivery Policy Decision Ref", + "type": "string" + }, + "delivery_ref": { + "minLength": 1, + "title": "Delivery Ref", + "type": "string" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "observation_ref": { + "minLength": 1, + "title": "Observation Ref", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "participant_view_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Participant View Digest", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "surface_id": { + "minLength": 1, + "title": "Surface Id", + "type": "string" + } + }, + "required": [ + "delivery_ref", + "surface_id", + "participant_address", + "episode_id", + "decision_epoch", + "participant_view_digest", + "delivery_basis", + "delivery_cut_ref", + "delivery_authorization_ref", + "delivery_policy_decision_ref", + "observation_ref", + "evidence_refs", + "provenance_refs", + "limitations" + ], + "title": "ParticipantDecisionSurfaceDeliveryV2Model", + "type": "object" + }, + "ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model": { + "additionalProperties": false, + "description": "The initial decision epoch derived from a trusted ``episode_running`` cut.", + "properties": { + "anchor_kind": { + "const": "episode_readiness", + "title": "Anchor Kind", + "type": "string" + }, + "decision_epoch": { + "minimum": 0, + "title": "Decision Epoch", + "type": "integer" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "episode_sequence_number": { + "minimum": 0, + "title": "Episode Sequence Number", + "type": "integer" + }, + "event_ref": { + "minLength": 1, + "title": "Event Ref", + "type": "string" + }, + "event_type": { + "const": "episode_running", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "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" + } + }, + "required": [ + "participant_address", + "episode_id", + "decision_epoch", + "event_ref", + "state_cut", + "evidence_refs", + "provenance_refs", + "anchor_kind", + "event_type", + "episode_sequence_number" + ], + "title": "ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model", + "type": "object" + }, + "ParticipantDecisionSurfaceExposureBindingV2Model": { + "additionalProperties": false, + "description": "One exact-cut authorization for an item admitted to a projected view.\n\n``decision_epoch`` orders participant choices. ``decision_cut_ref`` and\n``projection_policy_decision_ref`` identify the state and policy decision\nthat authorized this item. None of those coordinates is a delivery fact.", + "properties": { + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authority_basis_ref": { + "minLength": 1, + "title": "Authority Basis Ref", + "type": "string" + }, + "authorization_record_ref": { + "minLength": 1, + "title": "Authorization Record Ref", + "type": "string" + }, + "controller_ref": { + "minLength": 1, + "title": "Controller Ref", + "type": "string" + }, + "decision_cut_ref": { + "minLength": 1, + "title": "Decision Cut Ref", + "type": "string" + }, + "decision_epoch": { + "minimum": 0, + "title": "Decision Epoch", + "type": "integer" + }, + "declassification_basis_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declassification Basis Ref" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "exposure_policy_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Exposure Policy Digest", + "type": "string" + }, + "exposure_policy_ref": { + "minLength": 1, + "title": "Exposure Policy Ref", + "type": "string" + }, + "exposure_policy_version": { + "minLength": 1, + "title": "Exposure Policy Version", + "type": "string" + }, + "item_ref": { + "minLength": 1, + "title": "Item Ref", + "type": "string" + }, + "loss_and_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Loss And Limitations", + "type": "array" + }, + "operation": { + "enum": [ + "projection", + "masking", + "redaction", + "declassification", + "disclosure", + "transformation" + ], + "title": "Operation", + "type": "string" + }, + "operation_basis_ref": { + "minLength": 1, + "title": "Operation Basis Ref", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "projection_policy_decision_ref": { + "minLength": 1, + "title": "Projection Policy Decision Ref", + "type": "string" + }, + "projection_policy_ref": { + "minLength": 1, + "title": "Projection Policy Ref", + "type": "string" + }, + "projection_policy_revision": { + "minLength": 1, + "title": "Projection Policy Revision", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "redaction_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "result_marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Result Marking Definition Refs", + "type": "array" + }, + "result_provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Result Provenance Refs", + "type": "array" + }, + "source_layer_ref": { + "minLength": 1, + "title": "Source Layer Ref", + "type": "string" + }, + "source_marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Source Marking Definition Refs", + "type": "array" + }, + "source_provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Source Provenance Refs", + "type": "array" + }, + "source_ref": { + "minLength": 1, + "title": "Source Ref", + "type": "string" + }, + "transformation_rule_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transformation Rule Ref" + }, + "visibility_basis_ref": { + "minLength": 1, + "title": "Visibility Basis Ref", + "type": "string" + } + }, + "required": [ + "item_ref", + "authorization_record_ref", + "source_ref", + "source_layer_ref", + "participant_address", + "episode_id", + "audience_scope_ref", + "decision_epoch", + "decision_cut_ref", + "visibility_basis_ref", + "projection_policy_ref", + "projection_policy_revision", + "projection_policy_decision_ref", + "exposure_policy_ref", + "exposure_policy_version", + "exposure_policy_digest", + "operation", + "operation_basis_ref", + "actor_ref", + "controller_ref", + "authority_basis_ref", + "source_provenance_refs", + "result_provenance_refs", + "evidence_refs", + "provenance_refs", + "loss_and_limitations" + ], + "title": "ParticipantDecisionSurfaceExposureBindingV2Model", + "type": "object" + }, + "ParticipantDecisionSurfaceOpenEndedFormModel": { + "additionalProperties": false, + "properties": { + "allowed_action_contract_addresses": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Allowed Action Contract Addresses", + "type": "array" + }, + "argument_shape_ref": { + "minLength": 1, + "title": "Argument Shape Ref", + "type": "string" + }, + "proposal_binding_ref": { + "minLength": 1, + "title": "Proposal Binding Ref", + "type": "string" + }, + "selection_meaning_ref": { + "minLength": 1, + "title": "Selection Meaning Ref", + "type": "string" + }, + "surface_form": { + "const": "open_ended_generation", + "title": "Surface Form", + "type": "string" + }, + "validation_policy_ref": { + "minLength": 1, + "title": "Validation Policy Ref", + "type": "string" + } + }, + "required": [ + "surface_form", + "selection_meaning_ref", + "proposal_binding_ref", + "argument_shape_ref", + "validation_policy_ref", + "allowed_action_contract_addresses" + ], + "title": "ParticipantDecisionSurfaceOpenEndedFormModel", + "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" + }, + "ParticipantDecisionSurfaceViewV2Model": { + "additionalProperties": false, + "description": "The complete payload made available to one participant for one choice.", + "properties": { + "action_entries": { + "items": { + "$ref": "#/$defs/ParticipantDecisionSurfaceActionEntryModel" + }, + "minItems": 1, + "title": "Action Entries", + "type": "array" + }, + "affordance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Affordance Refs", + "type": "array" + }, + "context_view_ref": { + "minLength": 1, + "title": "Context View Ref", + "type": "string" + }, + "decision_control_mode": { + "minLength": 1, + "title": "Decision Control Mode", + "type": "string" + }, + "decision_epoch": { + "minimum": 0, + "title": "Decision Epoch", + "type": "integer" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "form": { + "discriminator": { + "mapping": { + "candidate_action_set": "#/$defs/ParticipantDecisionSurfaceCandidateSetFormModel", + "constrained_form": "#/$defs/ParticipantDecisionSurfaceConstrainedFormModel", + "open_ended_generation": "#/$defs/ParticipantDecisionSurfaceOpenEndedFormModel" + }, + "propertyName": "surface_form" + }, + "oneOf": [ + { + "$ref": "#/$defs/ParticipantDecisionSurfaceOpenEndedFormModel" + }, + { + "$ref": "#/$defs/ParticipantDecisionSurfaceConstrainedFormModel" + }, + { + "$ref": "#/$defs/ParticipantDecisionSurfaceCandidateSetFormModel" + } + ], + "title": "Form" + }, + "information_state_ref": { + "minLength": 1, + "title": "Information State Ref", + "type": "string" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "redaction_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "semantic_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Semantic Limitations", + "type": "array" + }, + "surface_id": { + "minLength": 1, + "title": "Surface Id", + "type": "string" + }, + "visible_context_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Visible Context Refs", + "type": "array" + } + }, + "required": [ + "surface_id", + "participant_address", + "episode_id", + "decision_epoch", + "information_state_ref", + "context_view_ref", + "decision_control_mode", + "visible_context_refs", + "action_entries", + "form", + "semantic_limitations" + ], + "title": "ParticipantDecisionSurfaceViewV2Model", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/participant-decision-surface-v2.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "surface_state": { + "const": "delivered" + } + }, + "required": [ + "surface_state" + ] + }, + "then": { + "properties": { + "delivery": { + "not": { + "type": "null" + } + } + }, + "required": [ + "delivery" + ] + } + }, + { + "if": { + "properties": { + "surface_state": { + "const": "projected" + } + }, + "required": [ + "surface_state" + ] + }, + "then": { + "properties": { + "delivery": { + "type": "null" + } + } + } + } + ], + "description": "A projected or delivered v2 decision surface with separated trust planes.", + "properties": { + "assurance": { + "$ref": "#/$defs/ParticipantDecisionSurfaceAssuranceV2Model" + }, + "delivery": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantDecisionSurfaceDeliveryV2Model" + }, + { + "type": "null" + } + ], + "default": null + }, + "participant_view": { + "$ref": "#/$defs/ParticipantDecisionSurfaceViewV2Model" + }, + "schema_version": { + "const": "participant-decision-surface/v2", + "title": "Schema Version", + "type": "string" + }, + "surface_state": { + "enum": [ + "projected", + "delivered" + ], + "title": "Surface State", + "type": "string" + } + }, + "required": [ + "schema_version", + "surface_state", + "participant_view", + "assurance" + ], + "title": "ParticipantDecisionSurfaceV2Model", + "type": "object", + "x-aces-invariants": [ + { + "description": "The participant view contains only participant-available choice material; derivation, policy, evidence, provenance, and delivery remain in separate assurance and delivery planes.", + "id": "decision-surface-v2-plane-separation", + "inputs": [ + { + "contract_id": "participant-decision-surface-v2", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface" + }, + { + "description": "Each exposed item is bound to the derivation state cut and exact policy decision, independently of the participant decision epoch.", + "id": "decision-surface-v2-exact-cut-policy", + "inputs": [ + { + "contract_id": "participant-decision-surface-v2", + "instance_path": "#/assurance" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface" + }, + { + "description": "A surface is actionable only in delivered state, with delivery bound to the canonical participant view.", + "id": "decision-surface-v2-delivery-before-selection", + "inputs": [ + { + "contract_id": "participant-decision-surface-v2", + "instance_path": "#/delivery" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface" + }, + { + "description": "Assurance declares whether participant-visible memory persists across episodes; an episode-local claim requires an authoritative reset of every participant-visible memory channel.", + "id": "decision-surface-v2-explicit-memory-scope", + "inputs": [ + { + "contract_id": "participant-decision-surface-v2", + "instance_path": "#/assurance" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceAssuranceV2Model._validate_assurance" + } + ], + "x-aces-semantic-profile": { + "contract_id": "participant-decision-surface-v2", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json b/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json index 71adec30d..f6505c3bc 100644 --- a/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json +++ b/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json @@ -361,7 +361,8 @@ "participant-configuration-result-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", - "participant-behavior-history-event-stream-v1" + "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2" ], "minLength": 1, "type": "string" diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index e4b740373..4c66ed7be 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -138,6 +138,7 @@ adr-091-portable-time-capability-control-and-provenance-contracts adr-092-autonomous-benign-participants-under-shared-time adr-093-raes-rename-and-compatibility-boundaries adr-094-authoritative-cross-plane-experiment-bindings +adr-095-participant-decision-epoch-state-cut-and-delivery-semantics ``` | ADR | Title | Status | Date | @@ -236,3 +237,4 @@ adr-094-authoritative-cross-plane-experiment-bindings | [092](adr-092-autonomous-benign-participants-under-shared-time.md) | Autonomous Benign Participants Under Shared Time | accepted | 2026-07-24 | | [093](adr-093-raes-rename-and-compatibility-boundaries.md) | RAES Rename and Compatibility Boundaries | accepted | 2026-07-23 | | [094](adr-094-authoritative-cross-plane-experiment-bindings.md) | Authoritative Cross-Plane Experiment Bindings | accepted | 2026-07-26 | +| [095](adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md) | Participant Decision Epoch, State-Cut, And Delivery Semantics | accepted | 2026-07-26 | diff --git a/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md b/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md index 7533b8e2e..e5121ac48 100644 --- a/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md +++ b/docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md @@ -4,6 +4,12 @@ proposed +ADR-095 is the accepted amendment for executable decision-surface coordinates, +participant/assurance separation, exact-cut policy decisions, delivery, and +runtime admission. The `D(p,e,o)` notation in this ADR describes the original +abstract and v1 behavior-history-indexed design; it must not be read as +authority to reinterpret v1 `observation_order` as a decision ordinal. + ## Date 2026-07-14 @@ -232,80 +238,6 @@ implement their rows through existing package ownership and must update the matrix when a carrier or enforcement point changes. They may strengthen a row but may not redefine the joint relations independently. -### 8. Ground the first surface in RUN-311 episode readiness - -A participant episode's first decision surface is grounded by the existing -RUN-311 lifecycle rather than by a fabricated behavior event. -`episode_initialized`, `episode_reset`, and `episode_restarted` establish an -episode generation; the following `episode_running` event is the authoritative -readiness anchor. Only then may the runtime derive the initial context from -compiled `V_p,0` and project `D(p,e,0)`. - -The public projection anchor is tagged as either episode readiness or a -behavior event. It references one event in one owning history and carries the -participant, episode, per-episode decision-surface order, anchor-local order, -stable event reference, evidence, and provenance. Episode lifecycle and -participant behavior remain separate histories. The tagged anchor makes the -surface `observation_order` the per-episode decision-surface coordinate, while -`anchor_order` remains the referenced lifecycle- or behavior-history -coordinate. RUN-311 `sequence_number` remains only the episode-generation -coordinate. Readiness derives decision-surface order zero. Each terminal -`observation_emitted` event advances the derived decision-surface order by one; -callers cannot choose that value independently. - -The portable order is: - -```text -episode_initialized | episode_reset | episode_restarted - -> episode_running - -> V_p,0 context - -> D(p,e,0) - -> proposal and selection - -> admitted action_attempted - -> state_transition_recorded - -> terminal observation_emitted - -> D(p,e,1) -``` - -Proposal and selection do not create participant behavior. Admission creates -the first behavior event. A reset or restart creates a new episode id and a new -order-zero surface with no behavior prefix from the new episode. - -Anchor shape is not authority. Projection and admission resolve the anchor -against the current trusted runtime snapshot and complete participant-local -history. A standalone event, isolated fragment, final snapshot, prior-episode -surface, surface superseded by later behavior, or unanchored surface presented -to the runtime admission path fails closed. Initial projection continues to use -the compiled initial view relation; later projection continues to use the -existing behavior-anchor indexes and effective view-relation selector. - -### 9. Classify the anchor integration as a breaking semantic change - -The `participant-decision-surface-v1` schema remains in the `draft` stability -class. ADR-061 therefore permits an in-place v1 change, and adding -`projection_anchor` is structurally additive because the serialized property is -optional. That structural fact is not an end-to-end compatibility claim. - -This amendment is a breaking semantic and runtime-admission change: - -- surface `observation_order` is the derived per-episode decision-surface - coordinate, not the referenced behavior-history index; -- an anchored projector requires the current trusted runtime snapshot; and -- runtime admission rejects an unanchored surface even though an older payload - can still pass structural schema validation. - -The Python distribution release carrying this amendment must therefore use the -repository's breaking-change release classification. The publication ledger -must describe the structural-versus-semantic distinction and must not claim -backward, forward, behavioral, or operational compatibility. - -Consumers migrate by resolving a readiness or behavior anchor from the current -trusted snapshot, carrying it on the projected surface, passing that snapshot -to anchored projection, using `observation_order` for `D(p,e,o)`, and using -`anchor_order` only for the referenced lifecycle or behavior-history event. -Cached or independently constructed unanchored surfaces cannot be migrated by -copying an event ref; they must be reprojected from current authority. - ## Alternatives Considered ### Add a flat participant `tools` list diff --git a/docs/decisions/adrs/adr-085-participant-information-flow-and-control.md b/docs/decisions/adrs/adr-085-participant-information-flow-and-control.md index f51eeedc9..e51c812dd 100644 --- a/docs/decisions/adrs/adr-085-participant-information-flow-and-control.md +++ b/docs/decisions/adrs/adr-085-participant-information-flow-and-control.md @@ -4,6 +4,12 @@ accepted +ADR-095 amends this decision for participant decision-surface v2: policy +resolution is relative to an exact state cut, noninterference quantifies over +adaptive low-participant strategies, and every cross-episode information-flow +claim declares participant-memory scope. Historical scalar-order contracts +retain their published meaning. + ## Date 2026-07-15 @@ -185,7 +191,7 @@ assumptions, and evidence exist. A finite negative-leakage suite, passing backend probes, or equal sampled histories remains bounded evidence. The relation SHALL be published in the existing -`aces-behavioral-relations` catalog at taxonomy revision `rev2`, with a +`aces-behavioral-relations` catalog at taxonomy revision `rev3`, with a dedicated participant-information-flow claim surface. The closed catalog contract remains `behavioral-relations/v1`; adding the relation changes the taxonomy contents, not the contract shape. Current in-repository claim @@ -302,3 +308,4 @@ risks and are published with issue #794. | Date | Commit/PR | Summary | |------|-----------|---------| | 2026-07-17 | #831 | Recorded ADR-085 acceptance with the SEM-230 formal authority, behavioral taxonomy revision, bounded evidence, and explicit literature-derived lineage and nonclaims. | +| 2026-07-26 | #909 | Adopted ADR-095 exact-cut policy, reactive-strategy, and explicit participant-memory semantics and advanced the behavioral taxonomy to revision 3. | diff --git a/docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md b/docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md new file mode 100644 index 000000000..da93491b9 --- /dev/null +++ b/docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md @@ -0,0 +1,390 @@ +# ADR-095: Participant Decision Epoch, State-Cut, And Delivery Semantics + +## Status + +accepted + +## Date + +2026-07-26 + +## Classification + +Classification: FM3 + +Required artifacts: an explicit labelled-transition model, separately typed +order coordinates, a versioned participant-decision contract, participant and +assurance projections, cut-relative information-flow decisions, delivery and +admission bindings, positive and adversarial fixtures, bounded behavioral +models, migration guidance, revision-pinned lineage, and clause-to-test +evidence. + +Waivers: issue #909 does not claim a universal simulation, refinement, +bisimulation, noninterference, epistemic, strategic, probabilistic, timed, or +partial-order proof. It does not add a theorem prover, model checker, scheduler, +agent implementation, backend transport, policy engine, world-state store, or +participant-history authority. + +## Context + +ADR-013 separates participant episodes from workflow, control-plane, and +backend lifecycles. ADR-022 separates world truth, participant-relative view, +participant local history, and archival evidence. ADR-054 defines participant +runtime occurrences, delivery bases, local-history order, partial order, and +participant-relative hiding. ADR-083 defines a participant decision surface +`D(p,e,o)`. ADR-085 defines revisioned participant information-flow crossings. +ADR-081 prevents bounded execution evidence from being reported as simulation, +refinement, bisimulation, or universal noninterference. + +The first executable SEM-220 contract made the abstract coordinate `o` an +integer named `observation_order`. Its reference implementation used that +integer as: + +- the index of one participant behavior-history occurrence; +- the order coordinate for selecting effective visibility; +- the order coordinate for projection-policy and authorization validity; +- the upper bound for realized delivery; +- the identity of a participant decision surface and its selection; and +- a label embedded in context, evidence, and provenance references. + +Those meanings agree only in a single serialized fixture where every semantic +transition advances one shared counter. They do not agree in the RAES runtime. +One participant action produces several behavior occurrences, policy changes +need not coincide with participant decisions, and partial or causal order need +not have one privileged linearization. + +The conflation also excluded the initial participant decision. Immediately +after RUN-311 reaches `episode_running`, compiled `V_p,0` exists but the new +episode has no participant behavior occurrence. Requiring a behavior-history +index either fabricates behavior or leaves the first action outside the +governed decision loop. + +Changing `observation_order` in place to mean a per-episode decision ordinal +would repair that one symptom while silently changing SEM-226 policy timing, +delivery timing, historical v1 payload meaning, and the observation projection +used by future simulation and noninterference arguments. Issue #909 therefore +requires a new semantic contract rather than another interpretation of the v1 +field. + +## Decision + +### 1. Type every order domain independently + +For participant `p`, episode `e`, decision epoch `k`, runtime state `q`, and +participant-relative state cut `c`, the decision surface is: + +```text +D(p, e, k) = Pi[p, audience, policy-decision, c](q) +``` + +The following coordinates are distinct: + +| Coordinate | Meaning | +| --- | --- | +| episode identity and generation | The RUN-311 scope created by initialize, reset, or restart. | +| decision epoch `k` | Zero-based participant choice opportunity within one episode. | +| state cut `c` | The exact total-order prefix or partial/causal frontier from which the surface is derived. | +| derivation anchor | A trusted lifecycle or terminal-observation occurrence witnessing `c`. | +| policy decision | The projection/exposure-policy revision resolved as effective at exactly `c`. | +| surface occurrence | The identity of the derived participant view and its assurance projection. | +| disclosure decision | The SEM-226 authorization for releasing each participant-view item. | +| delivery occurrence | Evidence that the participant-facing view crossed the participant boundary. | +| participant observation | The declared delivery/acknowledgement fact admitted to local history. | + +An integer from one row cannot satisfy another row merely because the current +reference backend serializes both. Every serialized order field names its +domain. A scalar is valid only for a declared total or backend-serialized order. +A partial or causal cut carries its frontier and relation identity. + +### 2. Establish an explicit reactive decision lifecycle + +The portable sequential lifecycle is: + +```text +episode_initialized | episode_reset | episode_restarted + -> episode_running + -> derive D(p,e,0) from V_p,0 and the empty behavior cut + -> authorize participant projection + -> disclose + -> deliver or explicitly declare emission-is-delivery + -> participant observes the surface + -> participant proposes/selects from that delivered surface + -> validate and admit the proposal + -> action_attempted + -> state_transition_recorded + -> terminal observation_emitted + -> derive D(p,e,1) from that exact terminal-observation cut +``` + +Projection is not disclosure. Disclosure is not delivery. Delivery is not +participant acknowledgement or interpretation. Presentation is not selection. +Selection is not admission. Admission is not an attempt or outcome. A carrier +or runtime helper may combine occurrences only when it declares the applicable +ADR-054 delivery basis and preserves the individual semantic facts. + +`episode_running` is the readiness anchor for `D(p,e,0)`. Its state cut is the +complete current participant-episode lifecycle prefix and an empty +current-episode participant-behavior history. A later surface is anchored by +the exact current terminal `observation_emitted` occurrence and the complete +current-episode behavior prefix. The derived decision epoch equals the number +of completed terminal observations in that episode. Callers do not author it. + +### 3. Publish v2 and preserve historical v1 meaning + +`participant-decision-surface-v1` retains its published historical meaning: +`observation_order` identifies the selected occurrence in the supplied +time-indexed behavior history. It remains available for historical validation +and migration but is not actionable through the new runtime-admission path. + +`participant-decision-surface-v2` is the actionable contract. It does not carry +`observation_order`. It contains: + +1. a participant-facing view with participant, episode, `decision_epoch`, + information-state identity, visible context, action entries, affordances, + selection form, markings, redaction, and disclosed limitations; +2. an assurance projection with the typed derivation anchor and state cut, + policy-at-cut decision, apparatus and boundary refs, exact exposure + bindings, participant-memory scope and reset authority when applicable, + evidence, provenance, and the canonical participant-view digest; +3. an explicit lifecycle state, initially `projected`; and +4. a delivery occurrence when the lifecycle state is `delivered`. + +The canonical digest uses the incumbent RFC 8785/JCS contract-digest helper. It +binds the exact participant-facing bytes and meaning to assurance, delivery, +selection, and admission without exposing assurance-only metadata. + +V1-to-v2 migration is not a field rename. A v2 surface must be reprojected from +the current trusted runtime snapshot and exact policy authorities. A historical +v1 record may remain historical evidence; it cannot be made actionable by +copying its `observation_order`. + +### 4. Separate participant information from verifier evidence + +The participant-facing view is the low observation. Derivation event ids, +history prefix length, anchor-local order, policy-decision records, raw +authorization records, evidence refs, and provenance refs belong to the +assurance projection. They enter the participant-facing view only through an +independent SEM-226 exposure decision. + +This separation is semantic, not cosmetic. Action presence, absence, ordering, +eligibility, constraint values, rejection detail, refresh behavior, surface +identity, and metadata can all convey information. The participant projection +therefore includes every field actually delivered to the participant, and the +assurance projection cannot be serialized as participant payload by default. + +### 5. Resolve information-flow decisions at exact cuts + +Projection-policy selection, exposure-policy selection, item authorization, +declassification, marking, and realized delivery are evaluated at an exact +state or delivery cut. Their authoritative resolver returns a decision bound to +that cut. A decision epoch is never used as a policy-effective order unless a +future policy contract explicitly declares `decision_epoch` as its order +domain. + +A later policy revision cannot authorize an earlier projection or delivery. A +delivery-time decision is resolved again at the delivery cut; it cannot inherit +the derivation-time result merely because both occur within one decision epoch. +Unknown, stale, cross-policy, cross-cut, and incomparable coordinates fail +closed. + +### 6. Make delivery a prerequisite for actionable selection + +A projected surface is a valid derived artifact but is not yet an actionable +participant occurrence. A delivered surface carries a trusted delivery record +that agrees on: + +- participant, episode, decision epoch, and surface identity; +- canonical participant-view digest; +- delivery basis and exact delivery cut; +- participant observation reference; and +- evidence, provenance, markings, and limitations. + +A v2 selection binds surface identity, decision epoch, participant-view digest, +and delivery ref. Runtime admission re-resolves both the derivation anchor and +delivery record against current authority before action-shape, apparatus, +SEM-211, and backend admission checks. An undelivered, stale, replayed, reset, +restarted, terminated, superseded, forged, or cross-scope surface fails before +participant behavior is written. + +### 7. Define the backend realization obligation directionally + +Let `A` be the abstract RAES participant transition system and `B` a conformant +backend realization. The intended universal soundness relation remains: + +```text +ParticipantProject(Traces(B)) subseteq Traces(A) +``` + +A concrete-to-abstract refinement mapping or forward simulation may establish +that inclusion by relating every concrete start state and step to an abstract +state and step sequence. `D(p,e,0)` supplies the required initial related +decision state. The decision epoch is stable under insertion of governed +backend-internal or evidence `tau` steps; the derivation cut witnesses the +concrete state used by the relation. + +Trace inclusion alone permits an implementation that refuses required inputs. +For actionable participant interaction, a conformance or proof claim must also +state input/output ownership and action-availability obligations. Participant +proposals are inputs to the runtime; participant views and observations are +outputs; backend/environment/scheduler choices are separately controlled. +I/O simulation or alternating refinement is the intended stronger seam. + +Current conformance remains bounded-probe evidence. It does not establish the +universal trace or simulation obligation. + +### 8. Keep bisimulation optional and projection-relative + +Two conformant backends may realize different behaviors allowed by RAES and +need not be bisimilar. Bisimulation is appropriate only for an explicitly +claimed substitutability or behavioral-equivalence result. + +Strong bisimulation requires exact bidirectional label matching. Weak or +branching bisimulation requires a governed participant-relative `tau` set and +matching through hidden closure. A claim also declares divergence, termination, +progress, timing, probability, strategy, scheduler, and partial-order +treatment. Adding a hidden internal occurrence must not change decision epoch +or participant-view identity. Equal sampled traces, equal digests, or one +successful participant loop are not bisimulation. + +### 9. Treat participant control as reactive information flow + +SEM-230 noninterference is a hyperproperty over sets of runs. For autonomous or +human participants, future inputs depend on prior participant-visible history. +A complete reactive claim therefore quantifies over participant strategies +that map local histories to action choices or distributions, not only over +fixed open-loop input sequences. + +The baseline strategy-relative obligation fixes participant, audience, policy +sequence, environment class, scheduler class, order model, declassification +schedule, and a class of low strategies. High-state or unauthorized-high-input +variation must preserve the permitted participant-visible history support sets +for every strategy in that class. Probabilistic strategies require a separately +governed probability kernel and relation. + +Bounded tests may falsify this relation on their enumerated model. They do not +prove universal reactive noninterference. + +### 10. Separate episode-local progress from participant memory + +Reset and restart create a new episode identity and restart its decision epoch +at zero. They do not erase an observation already delivered to a persistent +human, agent process, shared memory, or external controller. + +Every information-flow, epistemic, replay, and strategy claim declares a +participant-memory scope: + +- `episode_local_reset`, when the participant implementation and every + participant-visible memory channel are authoritatively reset; or +- `persistent_across_episodes`, when prior local history remains part of the + participant information state. + +Omitting the memory scope invalidates a positive noninterference, perfect +recall, or epistemic claim across reset. + +## Invariants + +- `decision_epoch` is derived independently of lifecycle-history and + behavior-history indexes. +- Every v2 surface has one participant, episode, state cut, policy decision, + and canonical participant-view digest. +- An initial surface has decision epoch zero, an `episode_running` readiness + anchor, and an empty current-episode behavior cut. +- A later surface is anchored by the exact current terminal observation and + complete current-episode behavior cut. +- V1 `observation_order` is never reinterpreted as v2 `decision_epoch`. +- Policy, authorization, and delivery decisions match an exact cut; no + decision-epoch comparison substitutes for cut ordering. +- Assurance-only coordinates are absent from the participant-facing view + unless independently exposed. +- A projected surface cannot be selected or admitted until a trusted delivery + occurrence is bound. +- Selection and admission bind the canonical participant-view digest and + delivery ref. +- Reset/restart invalidates the prior episode's actionable surface and does not + silently erase persistent participant knowledge. +- A total-order implementation does not foreclose a future partial/causal + frontier carrier. +- Bounded execution evidence never promotes a simulation, bisimulation, + noninterference, epistemic, strategic, timed, or probabilistic assurance + state. + +## Consequences + +### Positive + +- The participant loop has a truthful initial state and an inductive transition + boundary suitable for simulation/refinement arguments. +- Backend-internal stuttering no longer changes participant decision identity. +- Dynamic policy and delivery timing cannot be manufactured from a decision + ordinal. +- Participant payloads stop inheriting verifier-only history and evidence + metadata. +- Adaptive agents fit the same participant semantics as humans, scripts, and + RL policies without pretending their choices are deterministic. +- Future partial-order, probabilistic, strategic, and proof-bearing work has + typed extension seams instead of another overloaded integer. + +### Negative + +- V2 is a new contract and Python/runtime API. Existing actionable consumers + must reproject rather than relabel v1 data. +- Projection, delivery, and admission require more explicit authority + resolution and evidence. +- The reference implementation and every SEM-220/SEM-226 consumer must migrate + together. +- Strong assurance remains visibly unproved until its complete model and proof + artifacts exist. + +## Rejected Alternatives + +### Redefine v1 `observation_order` + +Rejected. Structural draft status does not make two incompatible semantic +domains identical. It would preserve a filename while breaking historical +meaning, SEM-226 timing, and behavioral comparability. + +### Use the behavior-history head as the decision id + +Rejected. One decision creates several behavior occurrences, hidden stuttering +changes the index, and partial order may have no unique head. + +### Use decision epoch for policy validity + +Rejected. Policies, interventions, and deliveries may change between decisions +or concurrently with backend work. + +### Put the anchor in the participant payload + +Rejected. Event ids, prefix lengths, and evidence topology can disclose hidden +activity. Assurance and participant observation have different audiences. + +### Treat projection as delivery + +Rejected as a default. A specific boundary may declare +`emission_is_delivery`, but the declaration and delivery occurrence remain +explicit and evidence-backed. + +### Require bisimulation for every conformant backend + +Rejected. RAES intentionally permits realization choice and nondeterminism. +Directional soundness plus declared availability obligations is the baseline; +bisimulation is a stronger optional claim. + +## References + +- [ADR-013](adr-013-participant-episode-lifecycle-boundaries.md) +- [ADR-022](adr-022-participant-behavior-and-interaction-semantics.md) +- [ADR-054](adr-054-participant-runtime-observable-lifecycle.md) +- [ADR-061](adr-061-published-schema-evolution-policy.md) +- [ADR-075](adr-075-ecosystem-versioning-deprecation-and-migration-governance.md) +- [ADR-081](adr-081-behavioral-relation-taxonomy-and-claim-discipline.md) +- [ADR-083](adr-083-participant-tool-decision-surface-and-exposure-semantics.md) +- [ADR-085](adr-085-participant-information-flow-and-control.md) +- [Participant semantics](../../../specs/formal/participant-semantics/README.md) +- [Participant information-flow control](../../../specs/formal/participant-semantics/information-flow-control.md) +- [Participant runtime semantics](../../../specs/formal/participant-runtime/README.md) +- [Behavioral-relation taxonomy](../../../specs/formal/behavioral-relations/README.md) +- [V2 migration guidance](../../explain/reference/participant-decision-surface-v2-migration.md) + +Primary intellectual lineage is revision-pinned in +`contracts/provenance/sdl-lineage-ledger-v1.json`. diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 64d3fc73e..cc29fb7cb 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -391,9 +391,12 @@ adrs: pin: e4be391affc09e1f0db3c5004fb2dfa0c4138c5c2f98086b3a6b8d341b4fd9d8 - id: ADR-085 path: docs/decisions/adrs/adr-085-participant-information-flow-and-control.md - pin: 75143315eba5f4e71d5f8c4bfa3468e46060e4ad95a593c21e8d100387500245 + pin: e31c5926696581e4af0115b7ba2a28e5ae23d04481baa4a60d73fd5cef5e7cd3 amendments: - ref: "#831" + - date: 2026-07-26 + ref: "#909" + summary: "Adopted ADR-095 exact-cut policy, reactive-strategy, and explicit participant-memory semantics and advanced the behavioral taxonomy to revision 3." - id: ADR-086 path: docs/decisions/adrs/adr-086-governed-whole-scenario-satisfiability.md pin: c34101fb3b5ddeba1e11f02666fb7747a38f52784087514ec76867df152aefa0 @@ -438,3 +441,6 @@ adrs: - id: ADR-094 path: docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md pin: 90578754323795ca8775c47dd6095752ccb5a769a0f932ebe60abbe1d8954246 + - id: ADR-095 + path: docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md + pin: 914e7f7ce5d65ac75f2cbfe70ff17e6740f1157575c48802d85cbe692e78ea17 diff --git a/docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md b/docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md deleted file mode 100644 index 2e2328551..000000000 --- a/docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md +++ /dev/null @@ -1,351 +0,0 @@ -# Issue 909 RUN-311 / SEM-220 Order-Zero Anchor Preflight - -Date: 2026-07-26 - -Issue: #909. - -Requirements: none. The GitHub issue is the authoritative contract. - -This note records architecture guardrails for integrating RUN-311 episode -readiness with the existing SEM-210/SEM-220 projection. It is non-normative -preflight guidance. It does not implement a projector, contract, schema, -control-plane operation, persistence path, or test. - -## Existing Authorities - -- ADR-013 and `raes_contracts.participant_episode` own participant-episode - identity, state, control actions, lifecycle history, reset/restart, and - `sequence_number`. -- ADR-022 and `specs/formal/participant-semantics/README.md` own action-linked - participant behavior history, `V_p,t`, observation boundaries, visibility - transitions, and the separation of participant-visible state from world truth - and archival evidence. -- ADR-054 owns the separation between episode lifecycle and observable behavior - lifecycle. A silent episode with empty behavior history is valid; no bootstrap - action may be invented to make a history non-empty. -- ADR-083, `ParticipantDecisionSurfaceModel`, - `ParticipantDecisionSurfaceProjectionInput`, and - `project_participant_decision_surface()` own `D(p,e,o)`, including its - participant/episode scope, compiled semantic inputs, context/exposure - projection, order/event/evidence basis, and selection meaning. -- The issue-119 preflight remains the cross-cutting authority for SEM-219, - SEM-220, and SEM-226 reuse, security, persistence, errors, and package - boundaries. This note narrows the unresolved initial-anchor and ordering - relationship; it does not fork that design. - -## Architecture Decision And Guardrails - -### Use one typed projection anchor, not a third history - -The projection boundary needs a small, closed, public projection-anchor value -or equivalent tagged contract. It references exactly one event from exactly one -of the two existing histories: - -- an **episode-readiness anchor** references a RUN-311 - `ParticipantEpisodeHistoryEvent`; or -- a **behavior-event anchor** references a - `ParticipantBehaviorHistoryEvent`. - -The two histories remain separate typed inputs. Do not create a heterogeneous -event list, add episode lifecycle values to -`ParticipantBehaviorHistoryEventType`, add action fields to episode history, or -depend on Python duck typing. - -The anchor must carry or resolve, without caller-authored semantic strings: - -- participant address and episode id; -- per-episode decision-surface order; -- anchor kind and stable event reference; -- the anchor's order in its owning history domain; -- evidence/provenance references supporting the projection; and -- for a behavior anchor, the exact behavior-history prefix used to derive - `V_p,o`. - -Lifecycle order, decision-surface order, and behavior-history order are -different coordinates. A field may not silently change meaning by anchor kind. -If the published decision-surface v1 carrier cannot represent those coordinates -without overloading `observation_order` or encoding structure into -`observation_point`, the contract must be compatibility-classified and -versioned under ADR-061. Do not silently reinterpret an existing v1 field. - -A typed event proves shape, not authority. The anchor must be resolved against -the current trusted `RuntimeSnapshot` lifecycle result/history and, for later -surfaces, the exact participant-local behavior-history prefix. A caller-created -event object, event-ref string, final snapshot, or isolated history fragment is -not sufficient. - -### `episode_running` is the readiness anchor - -`episode_initialized` records creation of the first episode identity and its -initialize control action. It precedes readiness and is not sufficient to expose -participant context or accept a decision. - -`episode_running` is the authoritative readiness anchor for every new episode: - -- for the first episode it follows `episode_initialized`; -- for reset it follows `episode_reset`; and -- for restart it follows `episode_restarted`. - -The readiness resolver must require exact participant and episode agreement, a -RUNNING live result, a valid lifecycle history whose matching -`episode_running` event is in the current episode scope, and agreement between -the live result and history head under -`iter_participant_episode_snapshot_violations()`. A terminal, superseded, -cross-participant, or previous-episode running event fails closed. - -RUN-311 `sequence_number` identifies the participant's episode generation. It is -not lifecycle-event order, decision-surface order, behavior-step order, an -action count, or a visibility-transition order. - -### Portable ordering relationship - -For a newly initialized episode, the required causal order is: - -| Domain | Order | Meaning | -| --- | ---: | --- | -| episode lifecycle | first event in the new scope | `episode_initialized` for the first episode, or `episode_reset` / `episode_restarted` for a successor episode | -| episode lifecycle | next event in the same scope | `episode_running`; authoritative readiness anchor | -| projection | derived from the readiness anchor | initial participant context from SEM-210 `V_p,0`, with the existing boundary, audience, exposure, marking, redaction, evidence, provenance, and apparatus gates | -| decision surface | 0 | `D(p,e,0)`, sharing the readiness/event/evidence basis with its context view; behavior history is still empty | -| decision lifecycle | after surface 0 | proposal and selection reference surface 0; neither is an action attempt | -| behavior history | 0 | the first admitted `action_attempted` event | -| behavior history | subsequent ordered events | the admitted action's state transition and terminal observation under the existing action-instance discipline | -| decision surface | 1 | the next surface, anchored to the applicable terminal observation and its behavior-history prefix | - -The current canonical admission path appends -`action_attempted -> state_transition_recorded -> observation_emitted` -consecutively. The projection abstraction must nevertheless key later surfaces -to the exact behavior event reference and behavior-history order, not to -arithmetic such as `3*n+2`; that preserves the seam for long-running, -orphaned-action, partial-order, or future lifecycle variants. - -Reset and restart create a new `episode_id`, increment RUN-311 -`sequence_number`, and restart decision-surface order at zero with empty -behavior history for the new episode. They never reuse the prior episode's -initial anchor, surface, selection, proposal, behavior prefix, or exposure -authorization. - -### Preserve the existing visibility and selection paths - -The initial anchor changes only how the initial relation is grounded. `V_p,0` -continues to come from compiled `view_rules`. Later surfaces continue to use -`_participant_behavior_history_anchor_indexes()` and -`participant_observation_effective_relation()` so a visibility transition is -effective only when its declared action-linked anchor exists at or before the -selected behavior-history order. - -Initial context and surface projection must still pass the same: - -- compiled behavior-specification, action-contract, argument-shape, - observation-boundary, and affordance resolution; -- audience, source-layer, transformation, marking, redaction, withholding, - exposure-policy revision, authorization, evidence, provenance, and apparatus - checks; -- context-view / decision-surface relational agreement; and -- SEM-220 selection-shape, surface membership, eligibility, apparatus, and - admission checks. - -`bind_participant_decision_surface_selection()` and -`RuntimeControlPlane.admit_participant_decision_surface_selection()` remain the -selection path. The live runtime must additionally reject a surface whose -participant, episode, readiness/behavior anchor, or decision order is no longer -current. A surface from a prior reset/restart must not be admitted into the -current episode merely because its action and participant addresses still -resolve. - -Invalid proposal, selection, argument-shape, apparatus, exposure, or admission -inputs create no participant behavior event. Only the existing admitted-action -path may append `action_attempted`, its transition, and its observation. - -## Canonical Incumbents To Reuse - -- **Lifecycle contracts and validation:** - `ParticipantEpisodeExecutionState`, `ParticipantEpisodeHistoryEvent`, - `ParticipantEpisodeHistoryEventModel`, - `iter_participant_episode_snapshot_violations()`, and - `BaseParticipantRuntime.initialize()`, `reset()`, and `restart()`. -- **Runtime state and persistence:** `RuntimeSnapshot`, - `RuntimeControlPlane`, `execute_participant_action()`, `ControlPlaneStore`, - `InMemoryControlPlaneStore`, and `LocalControlPlaneStore`. Projection state - must not move into a new current-surface store or snapshot `metadata`. -- **Compiled semantic scope:** `RuntimeModel`, - `ParticipantBehaviorSpecificationRuntime`, - `ParticipantActionContractRuntime`, - `ParticipantObservationBoundaryRuntime`, - `ParticipantToolAffordanceRuntime`, and the compiler-produced - `participant.*` addresses. -- **Visibility:** `_participant_behavior_initial_view_relation()`, - `_participant_behavior_history_anchor_indexes()`, - `participant_observation_effective_relation()`, and the compiled - `view_relation_timeline`. There must be one effective-relation algorithm. -- **Decision and exposure:** `ParticipantDecisionSurfaceProjectionInput`, - `project_participant_decision_surface()`, - `ParticipantExposureResolvers`, projection-policy revision selection, - exposure authorization/occurrence validation, - `ParticipantDecisionSurfaceModel`, `ParticipantContextViewModel`, and - `validate_participant_decision_surface_context()`. -- **Selection and admission:** `ParticipantDecisionSurfaceSelectionModel`, - `ParticipantValidatedActionSelection`, - `ParticipantDecisionSurfaceBindingResolvers`, - `bind_participant_decision_surface_selection()`, - `ParticipantActionAdmissionRequest`, - `participant_action_admission_request_violations()`, and - `ParticipantControlMixin.admit_participant_action()`. -- **Backend transition gate:** `_call_backend_apply()`, - participant runtime state/history transition diagnostics, - `ParticipantBehaviorHistoryEventModel`, and - `iter_participant_behavior_history_violations()`. -- **Contracts and schema governance:** `ContractModel`, - `schema_bundle()`, `contracts/schemas/`, `contracts/fixtures/`, - `contracts/schema-publication-manifest.json`, and - `contracts/schema-publication/entries/`. -- **Diagnostics and observability:** `Diagnostic`, `Severity`, - `OperationReceipt`, `OperationStatus`, `AuditEvent`, and existing - control-plane audit recording. Audit and raw logs are operational records, - not substitutes for the semantic lifecycle/behavior anchor. - -## Cross-Cutting Layers The Design Must Pass - -### Shape and semantic validation - -1. Public payloads remain closed `ContractModel` shapes with - `extra="forbid"` behavior and JSON Schema parity. -2. Lifecycle payloads normalize through the existing RUN-311 types and complete - snapshot invariants; accepting a standalone structurally valid event is - insufficient. -3. Participant, episode, behavior, boundary, action, affordance, argument-shape, - and anchor refs resolve against the compiled runtime model and trusted - snapshot. -4. Initial projection selects only compiled `V_p,0`; later projection uses the - existing behavior-anchor indexes and effective-relation selector. -5. Exposure resolvers validate immutable policy version/digest, effective - revision, exact participant/episode/order/apparatus coordinates, - authorization, markings, evidence, provenance, and any realized occurrence. -6. The context-view relational validator agrees with the surface on scope, - observation point, payload ref, projection, evidence, provenance, markings, - redaction, and limitations. -7. Selection binding validates surface identity/order, membership, eligibility, - support, argument shape, proposal coordinates, apparatus selection, exposure - policy, and admission-request agreement before normal action admission. -8. Backend apply validation preserves the predecessor snapshot, append-only - lifecycle/behavior histories, action-instance uniqueness, exact event order, - live episode scope, and terminal observation contract. - -### Authentication, authorization, secrets, and errors - -- No new HTTP endpoint is required by this integration. If an HTTP surface is - later added or an existing route is extended, it must enter through - `create_control_plane_app()`, - `ControlPlaneSecurityConfig.strict_defaults()`, bearer or verified-proxy - identity, target binding, `ControlPlaneRole` read/mutation authorization, - request-size guards, request fingerprints, idempotency, and `AuditEvent`. -- Control-plane caller authorization, scenario participant authority, - participant visibility, exposure authorization, and action admission are five - separate gates. Success at one does not imply another. -- Bearer tokens, credentials, hidden prompts, answer material, raw evidence, - raw exposure/configuration bodies, backend object representations, and full - tracebacks must not enter the anchor, surface, context view, snapshot, - diagnostic, audit details, or public error detail. Use stable refs, digests, - markings, redaction policies, and governed evidence/provenance. -- Expected contract and semantic failures use existing `ValueError`/`TypeError` - normalization at library boundaries, structured `Diagnostic` values and - operation envelopes at the control plane, or bounded existing 4xx details at - HTTP boundaries. Unexpected HTTP failures keep the redacted - `{"detail": "internal server error"}` envelope. Do not add an anchor-specific - exception hierarchy. -- This design needs no new environment binding, secret provider, config file, - subprocess, socket, filesystem path, or command-line option. A later backend - adapter must not place tokens, credentials, proposal payloads, hidden context, - or policy bodies in process argv, environment dumps, shell strings, - stdout/stderr, or logs. Existing typed adapter calls and injected resolvers are - the boundary; no `shell=True` execution is justified. - -### Persistence, replay, and observability - -- The current `RuntimeSnapshot` and `ControlPlaneStore` remain authoritative for - live episode and behavior history. Local persistence keeps its atomic snapshot - writes and append-only audit path. -- An initial anchor is valid only while it resolves to the current running - episode. Reset, restart, termination, participant mismatch, history - truncation, or live-result/history disagreement invalidates it. -- A later anchor must identify exactly one event in the supplied complete - participant/episode behavior prefix. Empty behavior history is permitted only - with a valid readiness anchor for decision order zero. -- Operation idempotency/request fingerprints and action-instance uniqueness - remain the mutation replay guards. Semantic surface replay also requires live - participant/episode/anchor agreement; idempotency alone does not make an old - surface current. -- Projection failures and authorization denials use existing diagnostics and - audit events. Do not add a decision-surface log schema, replay database, - side-channel cache, or treat logs/audit events as participant-visible - evidence. - -## Extensibility Seam - -The seam is the tagged, participant/episode-scoped projection anchor plus its -trusted resolver. Adding another truthful pre-action readiness source, partial -ordering basis, or externally realized participant must add a new governed -anchor variant/resolver rule without: - -- editing the RUN-311 or behavior-history event enums; -- changing `V_p,0`; -- changing action, argument-shape, exposure, or admission meaning; -- adding another history or visibility algorithm; or -- overloading lifecycle `sequence_number`, decision-surface order, or behavior - order. - -Anchor identity/order/evidence is the parameter. Backend kind, participant kind, -UI form, prompt format, and APTL scenario are not. - -## Gotchas And Anti-Patterns - -Avoid: - -- using `episode_initialized` as readiness; -- treating the simultaneous timestamps currently emitted for - `episode_initialized` and `episode_running` as ordering evidence; -- using lifecycle list position or `sequence_number` as behavior order; -- inserting a fake setup action, observation, choice, or state transition to - make behavior history non-empty; -- passing a lifecycle object through a - `Sequence[ParticipantBehaviorHistoryEvent]` and relying on shared attributes; -- merging lifecycle and behavior events into one union history; -- allowing an arbitrary empty history without a current readiness anchor; -- accepting a final snapshot, standalone event DTO, caller-owned event ref, or - backend-private bootstrap record as projection authority; -- encoding anchor structure in free-form `details`, snapshot `metadata`, - `observation_point`, audit details, or a naming convention; -- copying the initial view relation or visibility-transition walk into a second - projector; -- using future behavior, policy, authorization, or disclosure to justify the - initial surface; -- allowing a stale surface from a terminated, reset, or restarted episode to - bind to the current episode; -- treating presentation as proposal, selection, admission, action, result, or - participant choice; -- weakening argument-shape, apparatus, exposure, SEM-211, or normal admission - checks for the first selection; -- treating `AuditEvent`, `OperationReceipt`, backend logs, or timestamps alone - as semantic evidence; -- adding a duplicate DTO family, schema registry, validator stack, exception - hierarchy, persistence store, audit/log path, or backend-specific bootstrap - protocol; or -- changing a published schema without generator parity, fixtures, publication - ledger updates, and ADR-061 compatibility classification. - -## Non-Goals And Implementation Boundaries - -- No second participant, episode, visibility, context, decision, exposure, or - history model. -- No arbitrary snapshot projection and no weakening of later time-indexed - behavior-history semantics. -- No participant UI, prompt, agent framework, tool runner, backend setup action, - credential broker, policy engine, or OS sandbox. -- No APTL-, TechVault-, coding-agent-, LLM-, RL-, human-, or backend-specific - initialization semantics. -- No exposure of evaluator-only state, hidden truth, private answer material, - raw evidence, credentials, or backend-private identifiers. -- No redesign of RUN-311 lifecycle, SEM-210 transitions, SEM-211 applicability, - SEM-214/216 context/audience views, SEM-226 exposure authorization, control - plane authentication, persistence, audit, diagnostics, or experiment - provenance. diff --git a/docs/explain/reference/README.md b/docs/explain/reference/README.md index 2707b63f2..f2c161c30 100644 --- a/docs/explain/reference/README.md +++ b/docs/explain/reference/README.md @@ -33,3 +33,6 @@ themselves normative specifications or ADRs. - [experiment-binding-contracts.md](experiment-binding-contracts.md) - Authoritative experiment-to-scenario, participant, and apparatus binding contracts and configuration realization +- [participant-decision-surface-v2-migration.md](participant-decision-surface-v2-migration.md) + - Reprojection, delivery, capability declaration, admission, reset, memory, + and evidence guidance for participant decision surface v2 diff --git a/docs/explain/reference/participant-decision-surface-v2-migration.md b/docs/explain/reference/participant-decision-surface-v2-migration.md new file mode 100644 index 000000000..391c992cf --- /dev/null +++ b/docs/explain/reference/participant-decision-surface-v2-migration.md @@ -0,0 +1,86 @@ +# Participant Decision Surface V2 Migration + +This note is the implementation-facing migration guide for ADR-095 and issue +#909. The normative semantics are in ADR-095 and the participant formal +specification. + +## Compatibility Rule + +`participant-decision-surface-v1` keeps its historical meaning: +`observation_order` selects an occurrence in a supplied time-indexed behavior +history. Do not reinterpret it as a participant decision ordinal. + +`participant-decision-surface-v2` is the actionable runtime path. Migration is +reprojection, not field renaming. A historical v1 record remains valid +historical evidence under its published schema, but copying +`observation_order` into `decision_epoch` does not create a valid v2 surface. + +## Producer Migration + +For each participant choice: + +1. Resolve the current authoritative participant episode. It must be running. +2. Resolve the derivation state cut. + - For decision epoch zero, use the current `episode_running` lifecycle head + and require empty behavior history for the new episode. + - For a later epoch, use the exact current terminal + `observation_emitted` occurrence and the complete current-episode behavior + prefix. + - Use a typed causal frontier when the backend claims partial-order + realization; do not substitute a maximum scalar order. +3. Derive `decision_epoch` independently. Callers do not author it. +4. Resolve apparatus, projection policy, and every item authorization at the + exact state cut. Persist the exact policy-decision and authorization refs. +5. Construct only participant-available material in `participant_view`. +6. Construct trusted anchor, policy, exposure, evidence, provenance, canonical + view digest, and participant-memory scope in `assurance`. +7. Emit the surface in `projected` state. +8. Resolve an authoritative delivery occurrence for that exact view and + transition to `delivered`. +9. Accept a selection only when it binds surface id, decision epoch, canonical + participant-view digest, and delivery ref. +10. At admission, re-resolve the derivation anchor, delivery record, apparatus, + and existing participant-action constraints before writing behavior. + +The reference entry points are: + +- `resolve_participant_episode_readiness_anchor_v2()`; +- `resolve_participant_behavior_projection_anchor_v2()`; +- `project_participant_decision_surface_v2()`; +- `deliver_participant_decision_surface_v2()`; and +- `admit_participant_decision_surface_selection_v2()`. + +## Consumer And Apparatus Migration + +A participant implementation that consumes v2 must declare +`participant-decision-surface-v2` in both manifest +`supported_contract_versions` and +`capabilities.supported_participant_contracts`. The selected apparatus record +must also include it in `participant_contract_versions`. Admission fails closed +when that declaration is absent. + +Serialize `participant_view` to the participant. Do not serialize the +assurance plane as if it were part of the participant observation. Evidence or +provenance may enter a participant view only through an independent exposure +decision. + +## Reset, Restart, Replay, And Memory + +Reset and restart create a new episode identity and decision epoch zero. A +surface from the prior episode is stale and cannot be replayed. These lifecycle +operations do not establish that a human, agent process, external controller, +or shared memory forgot a prior delivery. + +Every v2 assurance declares one memory scope: + +- `persistent_across_episodes`, with no reset-authority claim; or +- `episode_local_reset`, with a `memory_reset_authority_ref` that covers every + participant-visible memory channel. + +## Evidence Boundary + +The v2 contract, runtime path, fixtures, and finite transition/information-flow +tests establish executable bounded evidence for the covered cases. They do not +prove universal trace inclusion, alternating refinement, bisimulation, or +reactive noninterference. Those claims require their separately declared +relations, quantifier scopes, and proof evidence. diff --git a/docs/explain/reference/shared-semantic-integrity.md b/docs/explain/reference/shared-semantic-integrity.md index 8ebe1a1e7..34535f35c 100644 --- a/docs/explain/reference/shared-semantic-integrity.md +++ b/docs/explain/reference/shared-semantic-integrity.md @@ -251,7 +251,7 @@ so they are tracked by their own requirements, not here. | 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 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, and exposure/visibility-boundary semantics | SEM-219, SEM-220, SEM-226 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-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`, `docs/decisions/issue-909-run-311-sem-220-order-zero-anchor-preflight.md`, `contracts/schemas/control-plane/participant-decision-surface-v1.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_anchor.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_anchor.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_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 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 | diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index 45c7a8f82..67b8272c5 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -794,7 +794,7 @@ which dynamic queue/log/config details remain evidence or bounded settings. syntax or wire compatibility. The normative participant-policy model is `specs/formal/participant-semantics/information-flow-control.md`; the machine-readable relation is `policy-noninterference` in behavioral taxonomy - revision `rev2`; and the claim surface is + revision `rev3`; and the claim surface is `participant-information-flow-policy`. Existing `W`, `V`, qualified `H`, `X`, participant action/admission, visibility transition, ordering, marking, controller, authority, evidence, and provenance objects remain the mapped @@ -805,14 +805,17 @@ which dynamic queue/log/config details remain evidence or bounded settings. semantics; Goguen and Meseguer supply noninterference and purge; Sabelfeld and Sands supply the declassification dimensions; Milner and van Glabbeek supply labelled-transition, hidden-action, and relation-separation - discipline. Lamport happened-before, Winskel event structures, and - Mazurkiewicz trace theory enter indirectly through the already governed - ADR-054 visible-order model. RAES extends those sources only with the - participant/audience, policy-revision/effective-order, controller/authority, - marking, and evidence/provenance coordinates needed to bind existing RAES - carriers. It does not fork their settled definitions. + discipline; Clarkson and Schneider distinguish trace properties from + hyperproperties; and Bohannon et al. supply the reactive strategy-sensitive + information-flow precedent. Lamport happened-before, Winskel event + structures, and Mazurkiewicz trace theory enter indirectly through the + already governed ADR-054 visible-order model. RAES extends those sources + only with participant/audience, exact-cut policy-decision, participant-memory, + controller/authority, marking, and evidence/provenance coordinates needed to + bind existing RAES carriers. It does not fork their settled definitions. - Delivery status is definition-complete, catalogued, policy-checked, and - bounded-tested. Evidence is the SEM-230 formal specification, + bounded-tested over finite open-loop and adaptive-strategy cases. Evidence + is the SEM-230 formal specification, `contracts/concept-authority/behavioral-relations-v1.json`, `tools/check_behavioral_relation_claims.py`, and `implementations/python/tests/test_sem_230_information_flow_control.py`. @@ -952,77 +955,55 @@ which dynamic queue/log/config details remain evidence or bounded settings. external derivation or compatibility claim. - SEM-220's executable participant decision surface adopts the existing action/observation-interface lineage above without importing a UI, prompt, - command, or backend-native parameter language. RAES maps one participant, - episode, per-episode decision-surface order, and typed lifecycle- or - behavior-history anchor to `ParticipantDecisionSurfaceModel`. The current - RUN-311 `episode_running` event grounds `D(p,e,0)` while behavior history is - empty; later surfaces use the exact terminal `observation_emitted` event and - complete behavior-history prefix without treating that event's history order - as the decision-surface order. RAES maps the three portable selection forms - to discriminated open-ended, constrained-form, and candidate-set payloads; - maps governed action meaning to compiled - `ParticipantActionContractRuntime` addresses; maps participant-local - presentation to the shared observation-boundary effective-view selector; - maps candidate applicability to explicit SEM-211 eligibility state and - reason refs; maps apparatus variation to implementation-selection, support, - and realization refs; and maps a chosen proposal through governed - argument-shape resolution before the existing - `ParticipantActionAdmissionRequest` path. Delivery is implemented for the - published closed contract and fixtures, compiled-runtime projection, - trusted snapshot/history anchor resolution, admission-time freshness - validation, context-envelope agreement, proposal binding, runtime admission - routing, schema publication, and adversarial ordering/bypass checks. Evidence - is `participant-decision-surface-v1`, - `resolve_participant_episode_readiness_anchor()`, - `resolve_participant_behavior_projection_anchor()`, - `project_participant_decision_surface()`, - `bind_participant_decision_surface_selection()`, and - `implementations/python/tests/test_sem_220_participant_decision_surface.py`. - Human proxy, script, LLM-agent, and RL-agent fixtures retain identical stable - action and selection-meaning refs while disclosing apparatus differences. - This mapping does not claim that presentation proves eligibility, selection, - admission, execution, result, outcome, historical exposure from a final - snapshot, backend support, UI behavior, prompt semantics, or complete - SEM-211 precondition evaluation. The lineage ledger and source audit remain - unchanged because the implementation adds no normative external derivation - or compatibility claim. -- SEM-226 specializes that delivered decision-surface projection without + command, or backend-native parameter language. Published v1 retains its + historical behavior-history-index meaning and remains available through + `ParticipantDecisionSurfaceModel` and + `project_participant_decision_surface()`. Issue #909 does not relabel its + `observation_order`. +- `participant-decision-surface-v2` separates the participant choice + coordinate `decision_epoch` from the exact derivation `state_cut`. Epoch zero + is derived from authoritative `episode_running` state while the new + episode's behavior history is empty. Later epochs are derived from terminal + participant observations, but retain their complete sequence prefix or + causal frontier rather than collapsing that cut into the epoch number. RAES + maps the three portable selection forms to the participant-only view; maps + governed action meaning to compiled action-contract addresses; maps + applicability to explicit SEM-211 eligibility; and keeps derivation, + projection/exposure policy, evidence, provenance, and memory scope in a + separate assurance plane. +- Projection, disclosure, delivery, selection, admission, attempt, result, and + outcome are distinct v2 occurrences. An actionable selection binds the + canonical participant-view digest and authoritative delivery ref. Admission + re-resolves the exact derivation anchor and delivery record against current + authority before existing argument-shape, apparatus, SEM-211, and backend + checks. Reset and restart create a new episode and epoch zero, invalidating + prior surfaces; they do not claim that a persistent participant forgot + already delivered information. Evidence is the v2 contract and schema, + `project_participant_decision_surface_v2()`, + `deliver_participant_decision_surface_v2()`, + `bind_participant_decision_surface_selection_v2()`, and the v2 SEM-220 + contract/runtime tests. +- SEM-226 specializes v1 and v2 decision-surface projection without adopting another visibility taxonomy, policy language, or participant I/O - envelope. RAES maps the participant/episode/audience/order coordinates to - `ParticipantDecisionSurfaceExposureBindingModel`; maps `V_p,o` to the - existing `participant_observation_effective_relation()` selector; maps the - revisioned SEM-230 `Effective(rho,o)` coordinate to an authoritative - `ParticipantExposurePolicyRevision` resolver; maps selected apparatus intent - to a separately resolved `ParticipantExposurePolicyModel` identity, version, - digest, disclosed and withheld refs; maps item authorization to stable - `ParticipantExposureAuthorizationRecord` refs, immutable exposure-policy - version/digest, and effective intervals instead of projection-owned gate - booleans; and maps source/result identity, - markings, transformation, redaction, declassification, evidence, - provenance, and limitations to one closed binding per serialized context, - action, and affordance ref. Realized delivery remains an optional - `ParticipantDecisionSurfaceExposureRealizationModel` occurrence binding - whose stable ref resolves independently and binds the exact delivered item - and delivery-time authorization record. That record and the occurrence must - agree with the policy effective at delivery order and with the participant, - episode, action instance, observation boundary, history order, observation - identity, evidence, and provenance. Observation lookup uses that semantic - identity rather than treating delivery order as a sequence index. It carries - its own delivery basis and limitations. Delivery is - implemented in `project_participant_decision_surface()` with deny-first - item selection, effective-order policy checks, exact item coverage, - participant/audience isolation, marking and provenance inheritance, and - non-retroactive concealment/revocation behavior. Evidence is the strengthened - `participant-decision-surface-v1` schema and fixtures, - `implementations/python/tests/test_sem_226_participant_exposure.py`, and - the retained SEM-220 projection tests. This mapping does not claim that a - selected policy, manifest capability, surface entry, disclosure decision, - HTTP response, log, or audit record proves delivery; does not make API-408 - retrieval participant-safe; and does not claim erasure, a general crossing - API, runtime persistence, backend enforcement, or universal - noninterference/bisimulation. The lineage ledger and source audit remain - unchanged because the implementation changes no normative external - derivation or compatibility claim. + envelope. V1 retains the published order-indexed binding and delivery + realization. V2 binds every exposed item to participant, episode, audience, + independent decision epoch, exact state-cut ref, exact projection-policy + decision, apparatus, exposure policy, markings, provenance, evidence, and + limitations. Policy and item authorization resolve at that exact cut; a + maximum scalar order cannot stand in for a causal frontier. Delivery is + independently authorized and resolved. +- Issue #909 adds formal lineage for the resulting backend and security + obligations. Abadi-Lamport refinement mappings and Lynch-Vaandrager + simulations ground the directional concrete-to-abstract obligation. + Lynch-Tuttle I/O automata and Alur et al. alternating refinement ground + explicit input/output ownership and availability: projected trace inclusion + alone permits a backend to refuse a required participant input. Bisimulation + remains optional and projection-relative, not the default conformance + relation. Clarkson-Schneider hyperproperties and Bohannon et al. reactive + noninterference ground adaptive-strategy quantification. The exact mappings, + divergences, source identities, and nonclaims are recorded in the lineage + ledger and source audit. Bounded tests may falsify the named finite models; + they do not prove universal refinement, bisimulation, or noninterference. - API-409 adopts the existing participant-runtime, ACT-617 mixed-control, and SEM-220 decision-surface authorities without introducing a generic external message or policy language. RAES maps one proposal, approval, denial, diff --git a/docs/explain/sdl/precedents.md b/docs/explain/sdl/precedents.md index a920d9435..0bb85de1c 100644 --- a/docs/explain/sdl/precedents.md +++ b/docs/explain/sdl/precedents.md @@ -108,6 +108,9 @@ design without becoming the RAES runtime API or authoring syntax. | Cyber-specific action/observation discipline | [CybORG](https://arxiv.org/abs/2108.09118), [CyberBattleSim](https://www.microsoft.com/en-us/research/project/cyberbattlesim/), [CyGIL](https://arxiv.org/abs/2304.01244) | Semantics | Action/effect/observation semantics must disclose simulation, emulation, and realization assumptions instead of assuming transfer across fidelity modes | | Adversary behavior under uncertainty | [CALDERA planning and acting](https://www.mitre.org/sites/default/files/2021-11/prs-18-0944-1-automated-adversary-emulation-planning-acting.pdf), [MITRE ATT&CK design](https://www.mitre.org/news-insights/publication/mitre-attck-design-and-philosophy) | Semantics | Cyber actions may change foothold, knowledge, detection surface, and downstream outcomes; technique labels do not replace action contracts | | Causality and temporal ordering | [Lamport ordering](https://systems.cs.columbia.edu/ds2-class/papers/lamport-time.pdf), [Halpern-Pearl structural causality](https://arxiv.org/abs/cs/0011012), HLA time management | Semantics | Attribution edges require explicit ordering and evidence support; timestamp adjacency is not a causal claim | +| Directional backend refinement | [Abadi-Lamport refinement mappings](https://doi.org/10.1016/0304-3975(91)90224-P), [Lynch-Vaandrager simulations](https://doi.org/10.1006/inco.1995.1134) | Semantics | A concrete backend must preserve the abstract participant semantics in the declared direction; finite conformance runs are not a universal simulation proof | +| Participant input/output availability | [Lynch-Tuttle I/O automata](https://groups.csail.mit.edu/tds/papers/Lynch/CWI89.html), [Alur et al. alternating refinement](https://doi.org/10.1007/BFb0055622) | Semantics | Participant/environment inputs and participant-facing outputs retain declared ownership and availability obligations; trace inclusion alone is insufficient | +| Reactive information flow | [Clarkson-Schneider hyperproperties](https://doi.org/10.3233/JCS-2009-0393), [Bohannon et al. reactive noninterference](https://doi.org/10.1145/1653662.1653673) | Semantics | Information-flow claims quantify over adaptive participant strategies, exact-cut policies, and explicit memory scope rather than comparing only fixed open-loop traces | | Checkable scenario semantics | [VSDL](https://arxiv.org/abs/2001.06681), [CRACK](https://iris.imtlucca.it/handle/20.500.11771/15672), [CyRIS](https://www.jaist.ac.jp/~razvan/publications/cyris_facilitating_training.pdf) | Semantics | Participant semantics inherit the requirement for executable contracts and conformance tests, while staying separate from topology/deployment generation | | Agent benchmark task structure | [Cybench](https://arxiv.org/abs/2408.08926), [AutoPenBench](https://arxiv.org/abs/2410.03225) | Semantics | Task descriptions, starter files, evaluators, subtasks, gold steps, and milestones are treated as participant-view and outcome-interpretation inputs rather than hidden harness details | | Integrated adversarial evaluation | [CAIBench](https://arxiv.org/abs/2510.24317) | Semantics | Offensive, defensive, privacy, and cyber-physical capabilities require role-neutral multi-participant semantics and outcome layers richer than final score | diff --git a/docs/research/lineage/source-audit-2026-07-12.md b/docs/research/lineage/source-audit-2026-07-12.md index bdea13d88..28d5959ed 100644 --- a/docs/research/lineage/source-audit-2026-07-12.md +++ b/docs/research/lineage/source-audit-2026-07-12.md @@ -130,14 +130,60 @@ ACES adapts the noninterference policy obligation and explicit declassification dimensions, interpreted-system local state, and labelled transition/hiding discipline into the ACES-native `policy-noninterference` relation. The governed mapping is participant-, audience-, policy-revision-, -scheduler/environment-, and order-relative and composes existing ACES world, -view, local-history, archival-evidence, control, authority, marking, and -provenance objects. The new content is limited to coordinates needed to bind -those prior definitions to existing ACES carriers. It does not copy publication -syntax or code and does not claim wire compatibility, universal proof, -production enforcement, or backend realization. No copied-code notice or -third-party distribution obligation is introduced by these publication -citations. +exact-cut policy-decision-, participant-memory-, scheduler/environment-, and +order-relative and composes existing ACES world, view, local-history, +archival-evidence, control, authority, marking, and provenance objects. Issue +#909 extends the source set and mapping below for reactive strategies and +backend I/O refinement. It does not copy publication syntax or code and does +not claim wire compatibility, universal proof, production enforcement, or +backend realization. No copied-code notice or third-party distribution +obligation is introduced by these publication citations. + +## Issue 909 Refinement, I/O, And Reactive Information-Flow Sources + +On 2026-07-26, issue #909 extended the participant-decision lineage audit to +the formal obligations created by decision epoch zero, exact state cuts, +delivery, backend realization, adaptive participants, and cross-episode +memory. The following primary publication identities were checked: + +- Martín Abadi and Leslie Lamport, *The Existence of Refinement Mappings*, + Theoretical Computer Science 82(2), 1991, + . +- Nancy A. Lynch and Frits W. Vaandrager, *Forward and Backward Simulations, + Part I: Untimed Systems*, Information and Computation 121(2), 1995, + . +- Nancy A. Lynch and Mark R. Tuttle, *An Introduction to Input/Output + Automata*, CWI Quarterly 2(3), 1989, also MIT/LCS/TM-373, + . +- Rajeev Alur, Thomas A. Henzinger, Orna Kupferman, and Moshe Y. Vardi, + *Alternating Refinement Relations*, CONCUR 1998, + . +- Michael R. Clarkson and Fred B. Schneider, *Hyperproperties*, Journal of + Computer Security 18(6), 2010, + . +- Aaron Bohannon, Benjamin C. Pierce, Vilhelm Sjöberg, Stephanie Weirich, and + Steve Zdancewic, *Reactive Noninterference*, CCS 2009, + . + +RAES adapts the refinement and I/O sources into a directional backend +obligation. Concrete participant behavior must remain admitted by the abstract +RAES semantics, but trace inclusion is not enough: participant and environment +inputs, participant-facing outputs, ownership, availability, fairness, exact +delivery, and state-cut treatment remain explicit obligations. RAES does not +adopt an I/O-automaton wire format and does not make bisimulation the default +backend-conformance relation. + +The information-flow sources refine the SEM-230 obligation from an open-loop +trace comparison to a hyperproperty over bounded or universally quantified +run supports under the same adaptive low-participant strategies. The claim +must bind exact-cut policy decisions and an explicit participant-memory scope. +Episode reset alone is not evidence that a human, agent process, external +controller, or shared memory forgot previously delivered information. + +These are semantic adaptations only. The issue copies no publication code or +syntax, introduces no source-compatible schema, and claims neither universal +refinement nor universal reactive noninterference from the bounded executable +counterexamples. ## DSL-437 Participant And Simulation Sources diff --git a/implementations/python/packages/raes_conformance/conformance/report.py b/implementations/python/packages/raes_conformance/conformance/report.py index 7a810e73e..4bdb75211 100644 --- a/implementations/python/packages/raes_conformance/conformance/report.py +++ b/implementations/python/packages/raes_conformance/conformance/report.py @@ -123,7 +123,7 @@ def _bounded_conformance_claim( evidence_refs = [f"conformance-case:{case.contract_name}:{case.name}" for case in cases] binding = BehavioralClaimBindingModel( taxonomy_id="aces-behavioral-relations", - taxonomy_revision="rev2", + taxonomy_revision="rev3", relation_id="bounded-probe-success", subject=f"Backend conformance for profile {profile}", left_carrier_ref=left_carrier_ref, diff --git a/implementations/python/packages/raes_contracts/behavioral_relations.py b/implementations/python/packages/raes_contracts/behavioral_relations.py index 6c4026710..957acd6a9 100644 --- a/implementations/python/packages/raes_contracts/behavioral_relations.py +++ b/implementations/python/packages/raes_contracts/behavioral_relations.py @@ -20,7 +20,7 @@ class ImmutablePublicationLocatorModel(ContractModel): - kind: Literal["doi", "isbn"] + kind: Literal["doi", "isbn", "report"] value: NonEmptyString diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index 425379792..31b937c36 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -1,7 +1,5 @@ """Schema-first external contract models for RAES artifact boundaries.""" -from __future__ import annotations - from ..versions import ( ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION, ATLAS_TACTICS_SOURCE_SCHEMA_VERSION, @@ -187,15 +185,12 @@ ParticipantDecisionSurfaceSelectionModel, validate_participant_decision_surface_context, ) -from .participant_decision_surface_anchor import ( - ParticipantDecisionSurfaceBehaviorAnchorModel, - ParticipantDecisionSurfaceEpisodeReadinessAnchorModel, - ParticipantDecisionSurfaceProjectionAnchorModel, -) from .participant_decision_surface_exposure import ( ParticipantDecisionSurfaceExposureBindingModel, ParticipantDecisionSurfaceExposureRealizationModel, ) +from .participant_decision_surface_exposure_v2 import * +from .participant_decision_surface_v2 import * from .participant_envelopes import ( EventClassificationModel, ParticipantJointActionAccessSetModel, @@ -428,15 +423,25 @@ "validate_participant_control_occurrence_context", "validate_participant_crossing_occurrence_context", "ParticipantDecisionSurfaceActionEntryModel", - "ParticipantDecisionSurfaceBehaviorAnchorModel", "ParticipantDecisionSurfaceCandidateSetFormModel", "ParticipantDecisionSurfaceConstrainedFormModel", - "ParticipantDecisionSurfaceEpisodeReadinessAnchorModel", "ParticipantDecisionSurfaceExposureBindingModel", "ParticipantDecisionSurfaceExposureRealizationModel", "ParticipantDecisionSurfaceModel", + "ParticipantDecisionSurfaceAssuranceV2Model", + "ParticipantDecisionSurfaceBehaviorAnchorV2Model", + "ParticipantDecisionSurfaceCausalCutModel", + "ParticipantDecisionSurfaceDeliveryV2Model", + "ParticipantDecisionSurfaceDerivationAnchorV2Model", + "ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model", + "ParticipantDecisionSurfaceExposureBindingV2Model", "ParticipantDecisionSurfaceOpenEndedFormModel", - "ParticipantDecisionSurfaceProjectionAnchorModel", "ParticipantDecisionSurfaceSelectionModel", + "ParticipantDecisionSurfaceSelectionV2Model", + "ParticipantDecisionSurfaceSequenceCutModel", + "ParticipantDecisionSurfaceStateCutModel", + "ParticipantDecisionSurfaceStateCutOrderModel", + "ParticipantDecisionSurfaceV2Model", + "ParticipantDecisionSurfaceViewV2Model", "validate_participant_decision_surface_context", "ParticipantEpisodeHistoryEventModel", "ParticipantEpisodeStateModel", "ParticipantExposurePolicyModel", "ParticipantFeatureSupportLevel", "ParticipantFeatureSupportModel", "ParticipantHistoryViewBehaviorEventModel", diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index 724a0e248..b1b55d107 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -38,6 +38,7 @@ from .participant_control import ParticipantControlOccurrenceModel from .participant_crossing import ParticipantCrossingOccurrenceModel from .participant_decision_surface import ParticipantDecisionSurfaceModel +from .participant_decision_surface_v2 import ParticipantDecisionSurfaceV2Model from .participant_envelopes import ( ParticipantJointActionRecordModel, ParticipantLifecycleEventModel, @@ -199,6 +200,7 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "participant-context-view-v1": ParticipantContextViewModel.model_json_schema(), "runtime-fact-binding-plane-v1": RuntimeFactBindingPlaneModel.model_json_schema(), "participant-decision-surface-v1": ParticipantDecisionSurfaceModel.model_json_schema(), + "participant-decision-surface-v2": ParticipantDecisionSurfaceV2Model.model_json_schema(), "participant-configuration-result-v1": ParticipantConfigurationResultModel.model_json_schema(), "operation-receipt-v1": OperationReceiptModel.model_json_schema(), "operation-status-v1": OperationStatusModel.model_json_schema(), diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py index 805a80720..3d7b3818a 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py @@ -11,7 +11,6 @@ from .base import ContractModel, NonEmptyString from .participant_context import ParticipantContextViewModel -from .participant_decision_surface_anchor import ParticipantDecisionSurfaceProjectionAnchorModel from .participant_decision_surface_exposure import ( ParticipantDecisionSurfaceExposureBindingModel, ) @@ -354,7 +353,6 @@ class ParticipantDecisionSurfaceModel(ContractModel): marking_definition_refs: list[NonEmptyString] = Field(default_factory=list) redaction_policy_ref: NonEmptyString | None = None semantic_limitations: list[NonEmptyString] = Field(min_length=1) - projection_anchor: ParticipantDecisionSurfaceProjectionAnchorModel | None = None @model_validator(mode="after") def _validate_surface_relations(self) -> ParticipantDecisionSurfaceModel: @@ -371,29 +369,8 @@ def _validate_surface_relations(self) -> ParticipantDecisionSurfaceModel: _validate_surface_form_relations(self.form, entries_by_id, entries_by_address) _validate_surface_affordances(self.affordance_refs, self.action_entries) _validate_surface_exposure_bindings(self) - self._validate_projection_anchor() return self - def _validate_projection_anchor(self) -> None: - anchor = self.projection_anchor - if anchor is None: - return - mismatched = [ - name - for name, anchor_value, surface_value in ( - ("participant_address", anchor.participant_address, self.participant_address), - ("episode_id", anchor.episode_id, self.episode_id), - ("decision_surface_order", anchor.decision_surface_order, self.observation_order), - ) - if anchor_value != surface_value - ] - if mismatched: - raise ValueError("projection anchor disagrees with the decision surface on: " + ", ".join(mismatched)) - if not set(anchor.evidence_refs).issubset(self.evidence_refs): - raise ValueError("projection anchor evidence_refs must be carried by the decision surface") - if not set(anchor.provenance_refs).issubset(self.provenance_refs): - raise ValueError("projection anchor provenance_refs must be carried by the decision surface") - @classmethod def __get_pydantic_json_schema__( cls, @@ -427,14 +404,6 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_surface_relations", inputs=[{"contract_id": "participant-decision-surface-v1", "instance_path": "#/exposure_bindings"}], ) - _add_aces_invariant( - json_schema, - "decision-surface-projection-anchor-agreement", - "A typed projection anchor, when present, must agree with the surface participant, episode, anchor-local " - "decision-surface order, evidence, and provenance without merging lifecycle and behavior histories.", - validator="raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_projection_anchor", - inputs=[{"contract_id": "participant-decision-surface-v1", "instance_path": "#/projection_anchor"}], - ) return json_schema diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py deleted file mode 100644 index dd9a6d57b..000000000 --- a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_anchor.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Typed event anchors for SEM-220 participant decision surfaces.""" - -from __future__ import annotations - -from typing import Annotated, Literal - -from pydantic import Field, StrictInt, model_validator - -from .base import ContractModel, NonEmptyString - - -def _require_unique(values: list[str], field_name: str) -> None: - if len(values) != len(set(values)): - raise ValueError(f"{field_name} must not contain duplicates") - - -class _ParticipantDecisionSurfaceProjectionAnchorBase(ContractModel): - """Coordinates shared by every trusted decision-surface event anchor.""" - - participant_address: NonEmptyString - episode_id: NonEmptyString - decision_surface_order: StrictInt = Field(ge=0) - event_ref: NonEmptyString - anchor_order: StrictInt = Field(ge=0) - evidence_refs: list[NonEmptyString] = Field(min_length=1) - provenance_refs: list[NonEmptyString] = Field(min_length=1) - - @model_validator(mode="after") - def _validate_anchor_refs(self) -> _ParticipantDecisionSurfaceProjectionAnchorBase: - _require_unique(self.evidence_refs, "evidence_refs") - _require_unique(self.provenance_refs, "provenance_refs") - if self.event_ref not in self.provenance_refs: - raise ValueError("projection anchor event_ref must be carried by provenance_refs") - return self - - -class ParticipantDecisionSurfaceEpisodeReadinessAnchorModel(_ParticipantDecisionSurfaceProjectionAnchorBase): - """RUN-311 ``episode_running`` anchor for one episode's initial surface.""" - - anchor_kind: Literal["episode_readiness"] - event_type: Literal["episode_running"] - episode_sequence_number: StrictInt = Field(ge=0) - - -class ParticipantDecisionSurfaceBehaviorAnchorModel(_ParticipantDecisionSurfaceProjectionAnchorBase): - """One terminal observation and exact prefix anchoring a later surface.""" - - anchor_kind: Literal["behavior_event"] - event_type: Literal["observation_emitted"] - action_instance_id: NonEmptyString - history_prefix_length: StrictInt = Field(ge=1) - - @model_validator(mode="after") - def _validate_history_prefix(self) -> ParticipantDecisionSurfaceBehaviorAnchorModel: - if self.history_prefix_length != self.anchor_order + 1: - raise ValueError("behavior projection anchor history_prefix_length must equal anchor_order + 1") - return self - - -ParticipantDecisionSurfaceProjectionAnchorModel = Annotated[ - ParticipantDecisionSurfaceEpisodeReadinessAnchorModel | ParticipantDecisionSurfaceBehaviorAnchorModel, - Field(discriminator="anchor_kind"), -] 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 new file mode 100644 index 000000000..a0ad4e952 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py @@ -0,0 +1,106 @@ +"""State-cut-bound SEM-226 exposure decisions for decision-surface v2.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import Field, StrictInt, model_validator + +from .base import ContractModel, NonEmptyString +from .participant_decision_surface_exposure import ParticipantExposureOperation +from .participant_manifests import DigestString + + +def _require_unique(values: list[str], field_name: str) -> None: + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must not contain duplicates") + + +class ParticipantDecisionSurfaceExposureBindingV2Model(ContractModel): + """One exact-cut authorization for an item admitted to a projected view. + + ``decision_epoch`` orders participant choices. ``decision_cut_ref`` and + ``projection_policy_decision_ref`` identify the state and policy decision + that authorized this item. None of those coordinates is a delivery fact. + """ + + item_ref: NonEmptyString + authorization_record_ref: NonEmptyString + source_ref: NonEmptyString + source_layer_ref: NonEmptyString + participant_address: NonEmptyString + episode_id: NonEmptyString + audience_scope_ref: NonEmptyString + decision_epoch: StrictInt = Field(ge=0) + decision_cut_ref: NonEmptyString + visibility_basis_ref: NonEmptyString + projection_policy_ref: NonEmptyString + projection_policy_revision: NonEmptyString + projection_policy_decision_ref: NonEmptyString + exposure_policy_ref: NonEmptyString + exposure_policy_version: NonEmptyString + exposure_policy_digest: DigestString + operation: ParticipantExposureOperation + operation_basis_ref: NonEmptyString + actor_ref: NonEmptyString + controller_ref: NonEmptyString + authority_basis_ref: NonEmptyString + source_marking_definition_refs: list[NonEmptyString] = Field(default_factory=list) + result_marking_definition_refs: list[NonEmptyString] = Field(default_factory=list) + source_provenance_refs: list[NonEmptyString] = Field(min_length=1) + result_provenance_refs: list[NonEmptyString] = Field(min_length=1) + declassification_basis_ref: NonEmptyString | None = None + redaction_policy_ref: NonEmptyString | None = None + transformation_rule_ref: NonEmptyString | None = None + evidence_refs: list[NonEmptyString] = Field(min_length=1) + provenance_refs: list[NonEmptyString] = Field(min_length=1) + loss_and_limitations: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_exposure_basis(self) -> ParticipantDecisionSurfaceExposureBindingV2Model: + for field_name in ( + "source_marking_definition_refs", + "result_marking_definition_refs", + "source_provenance_refs", + "result_provenance_refs", + "evidence_refs", + "provenance_refs", + "loss_and_limitations", + ): + _require_unique(getattr(self, field_name), field_name) + if self.source_ref != self.item_ref and self.transformation_rule_ref is None: + raise ValueError("derived exposure items require transformation_rule_ref") + if self.operation in {"masking", "redaction", "transformation"} and self.transformation_rule_ref is None: + raise ValueError(f"{self.operation} exposure operations require transformation_rule_ref") + if self.operation == "redaction" and self.redaction_policy_ref is None: + raise ValueError("redaction exposure operations require redaction_policy_ref") + if self.operation == "declassification" and self.declassification_basis_ref is None: + raise ValueError("declassification exposure operations require declassification_basis_ref") + if self.declassification_basis_ref is None and not set(self.source_marking_definition_refs).issubset( + self.result_marking_definition_refs + ): + raise ValueError( + "derived exposure results must inherit source markings unless declassification is explicit" + ) + if self.declassification_basis_ref is None and not set(self.source_provenance_refs).issubset( + self.result_provenance_refs + ): + raise ValueError( + "derived exposure results must inherit source provenance unless declassification is explicit" + ) + if not {*self.source_provenance_refs, *self.result_provenance_refs}.issubset(self.provenance_refs): + raise ValueError("source and result provenance refs must be carried by provenance_refs") + 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 new file mode 100644 index 000000000..b0f628329 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py @@ -0,0 +1,482 @@ +"""Portable participant decision-surface v2 contracts. + +Version 2 separates the participant-visible decision view from its trusted +derivation and from delivery. A decision epoch is a participant-choice +coordinate; a state cut is the causal/history coordinate used to derive it. +""" + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import Field, GetJsonSchemaHandler, StrictInt, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from .base import ContractModel, NonEmptyString +from .participant_decision_surface import ( + ParticipantDecisionSurfaceActionEntryModel, + ParticipantDecisionSurfaceFormModel, + _require_unique, + _surface_entry_indexes, + _validate_surface_affordances, + _validate_surface_form_relations, +) +from .participant_decision_surface_exposure_v2 import ( + ParticipantDecisionSurfaceExposureBindingV2Model, + ParticipantDecisionSurfaceStateCutOrderModel, +) +from .participant_manifests import DigestString +from .participant_runtime import ParticipantRuntimeDeliveryBasis +from .schema_invariants import _add_aces_invariant + + +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 + decision_epoch: StrictInt = Field(ge=0) + event_ref: NonEmptyString + state_cut: ParticipantDecisionSurfaceStateCutModel + evidence_refs: list[NonEmptyString] = Field(min_length=1) + provenance_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_anchor(self) -> _ParticipantDecisionSurfaceAnchorV2Base: + _require_unique(self.evidence_refs, "evidence_refs") + _require_unique(self.provenance_refs, "provenance_refs") + if self.event_ref not in self.provenance_refs: + raise ValueError("derivation anchor event_ref must be carried by provenance_refs") + if isinstance(self.state_cut, ParticipantDecisionSurfaceSequenceCutModel): + if self.event_ref != self.state_cut.anchor_event_ref: + raise ValueError("event_ref must equal the sequence state cut anchor_event_ref") + elif self.event_ref not in self.state_cut.frontier_event_refs: + raise ValueError("event_ref must belong to the causal state cut frontier") + return self + + +class ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model(_ParticipantDecisionSurfaceAnchorV2Base): + """The initial decision epoch derived from a trusted ``episode_running`` cut.""" + + anchor_kind: Literal["episode_readiness"] + event_type: Literal["episode_running"] + episode_sequence_number: StrictInt = Field(ge=0) + + @model_validator(mode="after") + def _validate_initial_epoch(self) -> ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model: + if self.decision_epoch != 0: + raise ValueError("episode-readiness anchors require decision_epoch zero") + if ( + isinstance(self.state_cut, ParticipantDecisionSurfaceSequenceCutModel) + and self.state_cut.history_domain != "participant_episode_lifecycle" + ): + raise ValueError("episode-readiness sequence cuts must use participant_episode_lifecycle") + return self + + +class ParticipantDecisionSurfaceBehaviorAnchorV2Model(_ParticipantDecisionSurfaceAnchorV2Base): + """A later decision epoch derived from a terminal participant observation.""" + + anchor_kind: Literal["behavior_event"] + event_type: Literal["observation_emitted"] + action_instance_id: NonEmptyString + + @model_validator(mode="after") + def _validate_behavior_epoch(self) -> ParticipantDecisionSurfaceBehaviorAnchorV2Model: + if self.decision_epoch < 1: + raise ValueError("behavior anchors require decision_epoch greater than zero") + if ( + isinstance(self.state_cut, ParticipantDecisionSurfaceSequenceCutModel) + and self.state_cut.history_domain != "participant_behavior_history" + ): + raise ValueError("behavior sequence cuts must use participant_behavior_history") + return self + + +ParticipantDecisionSurfaceDerivationAnchorV2Model = Annotated[ + ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model | ParticipantDecisionSurfaceBehaviorAnchorV2Model, + Field(discriminator="anchor_kind"), +] + + +class ParticipantDecisionSurfaceViewV2Model(ContractModel): + """The complete payload made available to one participant for one choice.""" + + surface_id: NonEmptyString + participant_address: NonEmptyString + episode_id: NonEmptyString + decision_epoch: StrictInt = Field(ge=0) + information_state_ref: NonEmptyString + context_view_ref: NonEmptyString + decision_control_mode: NonEmptyString + visible_context_refs: list[NonEmptyString] = Field(min_length=1) + action_entries: list[ParticipantDecisionSurfaceActionEntryModel] = Field(min_length=1) + affordance_refs: list[NonEmptyString] = Field(default_factory=list) + form: ParticipantDecisionSurfaceFormModel + marking_definition_refs: list[NonEmptyString] = Field(default_factory=list) + redaction_policy_ref: NonEmptyString | None = None + semantic_limitations: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_view(self) -> ParticipantDecisionSurfaceViewV2Model: + for field_name in ( + "visible_context_refs", + "affordance_refs", + "marking_definition_refs", + "semantic_limitations", + ): + _require_unique(getattr(self, field_name), field_name) + entries_by_id, entries_by_address = _surface_entry_indexes(self.action_entries) + _validate_surface_form_relations(self.form, entries_by_id, entries_by_address) + _validate_surface_affordances(self.affordance_refs, self.action_entries) + return self + + +class ParticipantDecisionSurfaceAssuranceV2Model(ContractModel): + """Trusted derivation, policy, provenance, and evidence for one view.""" + + participant_address: NonEmptyString + episode_id: NonEmptyString + decision_epoch: StrictInt = Field(ge=0) + behavior_specification_address: NonEmptyString + observation_boundary_address: NonEmptyString + implementation_selection_ref: NonEmptyString + audience_scope_ref: NonEmptyString + projection_policy_ref: NonEmptyString + projection_policy_revision: NonEmptyString + projection_policy_decision_ref: NonEmptyString + exposure_policy_ref: NonEmptyString + visibility_projection_ref: NonEmptyString + participant_memory_scope: Literal["episode_local_reset", "persistent_across_episodes"] + memory_reset_authority_ref: NonEmptyString | None = None + participant_view_digest: DigestString + derivation_anchor: ParticipantDecisionSurfaceDerivationAnchorV2Model + exposure_bindings: list[ParticipantDecisionSurfaceExposureBindingV2Model] = Field(min_length=1) + evidence_refs: list[NonEmptyString] = Field(min_length=1) + provenance_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_assurance(self) -> ParticipantDecisionSurfaceAssuranceV2Model: + _require_unique(self.evidence_refs, "evidence_refs") + _require_unique(self.provenance_refs, "provenance_refs") + if self.participant_memory_scope == "episode_local_reset": + if self.memory_reset_authority_ref is None: + raise ValueError("episode_local_reset memory scope requires memory_reset_authority_ref") + elif self.memory_reset_authority_ref is not None: + raise ValueError("persistent_across_episodes memory scope must not claim a reset authority") + anchor = self.derivation_anchor + comparisons = ( + ("participant_address", anchor.participant_address, self.participant_address), + ("episode_id", anchor.episode_id, self.episode_id), + ("decision_epoch", anchor.decision_epoch, self.decision_epoch), + ) + mismatched = [name for name, anchor_value, assurance_value in comparisons if anchor_value != assurance_value] + if mismatched: + raise ValueError("derivation anchor disagrees with assurance on: " + ", ".join(mismatched)) + if not set(anchor.evidence_refs).issubset(self.evidence_refs): + raise ValueError("derivation anchor evidence_refs must be carried by assurance") + if not set(anchor.provenance_refs).issubset(self.provenance_refs): + raise ValueError("derivation anchor provenance_refs must be carried by assurance") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"participant_memory_scope": {"const": "episode_local_reset"}}, + "required": ["participant_memory_scope"], + }, + "then": { + "required": ["memory_reset_authority_ref"], + "properties": {"memory_reset_authority_ref": {"type": "string", "minLength": 1}}, + }, + }, + { + "if": { + "properties": {"participant_memory_scope": {"const": "persistent_across_episodes"}}, + "required": ["participant_memory_scope"], + }, + "then": {"properties": {"memory_reset_authority_ref": {"type": "null"}}}, + }, + ] + ) + return json_schema + + +class ParticipantDecisionSurfaceDeliveryV2Model(ContractModel): + """Evidence that the exact participant view became available to its subject.""" + + delivery_ref: NonEmptyString + surface_id: NonEmptyString + participant_address: NonEmptyString + episode_id: NonEmptyString + decision_epoch: StrictInt = Field(ge=0) + participant_view_digest: DigestString + delivery_basis: ParticipantRuntimeDeliveryBasis + delivery_cut_ref: NonEmptyString + delivery_authorization_ref: NonEmptyString + delivery_policy_decision_ref: NonEmptyString + observation_ref: NonEmptyString + evidence_refs: list[NonEmptyString] = Field(min_length=1) + provenance_refs: list[NonEmptyString] = Field(min_length=1) + limitations: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_delivery_refs(self) -> ParticipantDecisionSurfaceDeliveryV2Model: + for field_name in ("evidence_refs", "provenance_refs", "limitations"): + _require_unique(getattr(self, field_name), field_name) + if self.delivery_basis in {"unknown", "unsupported"}: + raise ValueError("delivered decision surfaces require an affirmative delivery_basis") + return self + + +def _view_exposed_refs(view: ParticipantDecisionSurfaceViewV2Model) -> set[str]: + return { + *view.visible_context_refs, + *(entry.action_contract_address for entry in view.action_entries), + *view.affordance_refs, + } + + +class ParticipantDecisionSurfaceV2Model(ContractModel): + """A projected or delivered v2 decision surface with separated trust planes.""" + + schema_version: Literal["participant-decision-surface/v2"] + surface_state: Literal["projected", "delivered"] + participant_view: ParticipantDecisionSurfaceViewV2Model + assurance: ParticipantDecisionSurfaceAssuranceV2Model + delivery: ParticipantDecisionSurfaceDeliveryV2Model | None = None + + @model_validator(mode="after") + def _validate_surface(self) -> ParticipantDecisionSurfaceV2Model: + view = self.participant_view + assurance = self.assurance + coordinate_comparisons = ( + ("participant_address", assurance.participant_address, view.participant_address), + ("episode_id", assurance.episode_id, view.episode_id), + ("decision_epoch", assurance.decision_epoch, view.decision_epoch), + ) + mismatched = [ + name for name, assurance_value, view_value in coordinate_comparisons if assurance_value != view_value + ] + if mismatched: + raise ValueError("assurance disagrees with the participant view on: " + ", ".join(mismatched)) + + from ..satisfiability import canonical_contract_digest + + if assurance.participant_view_digest != canonical_contract_digest(view): + raise ValueError("assurance participant_view_digest must match the canonical participant view") + + bindings = {binding.item_ref: binding for binding in assurance.exposure_bindings} + _require_unique([binding.item_ref for binding in assurance.exposure_bindings], "exposure_bindings.item_ref") + expected = _view_exposed_refs(view) + if bindings.keys() != expected: + missing = sorted(expected - bindings.keys()) + extra = sorted(bindings.keys() - expected) + details = [] + if missing: + details.append("missing " + ", ".join(missing)) + if extra: + details.append("unexpected " + ", ".join(extra)) + raise ValueError("exposure_bindings must exactly cover participant view refs: " + "; ".join(details)) + for binding in assurance.exposure_bindings: + comparisons = ( + ("participant_address", binding.participant_address, view.participant_address), + ("episode_id", binding.episode_id, view.episode_id), + ("decision_epoch", binding.decision_epoch, view.decision_epoch), + ("decision_cut_ref", binding.decision_cut_ref, assurance.derivation_anchor.state_cut.cut_ref), + ("audience_scope_ref", binding.audience_scope_ref, assurance.audience_scope_ref), + ("projection_policy_ref", binding.projection_policy_ref, assurance.projection_policy_ref), + ( + "projection_policy_revision", + binding.projection_policy_revision, + assurance.projection_policy_revision, + ), + ( + "projection_policy_decision_ref", + binding.projection_policy_decision_ref, + assurance.projection_policy_decision_ref, + ), + ("exposure_policy_ref", binding.exposure_policy_ref, assurance.exposure_policy_ref), + ) + binding_mismatches = [ + name for name, binding_value, expected_value in comparisons if binding_value != expected_value + ] + if binding_mismatches: + raise ValueError( + f"exposure binding {binding.item_ref!r} disagrees with the surface on: " + + ", ".join(binding_mismatches) + ) + if not set(binding.evidence_refs).issubset(assurance.evidence_refs): + raise ValueError(f"exposure binding {binding.item_ref!r} evidence must be carried by assurance") + if not set(binding.provenance_refs).issubset(assurance.provenance_refs): + raise ValueError(f"exposure binding {binding.item_ref!r} provenance must be carried by assurance") + + if self.surface_state == "projected" and self.delivery is not None: + raise ValueError("projected surfaces must not carry delivery") + if self.surface_state == "delivered" and self.delivery is None: + raise ValueError("delivered surfaces require delivery") + if self.delivery is not None: + delivery_comparisons = ( + ("surface_id", self.delivery.surface_id, view.surface_id), + ("participant_address", self.delivery.participant_address, view.participant_address), + ("episode_id", self.delivery.episode_id, view.episode_id), + ("decision_epoch", self.delivery.decision_epoch, view.decision_epoch), + ( + "participant_view_digest", + self.delivery.participant_view_digest, + assurance.participant_view_digest, + ), + ) + delivery_mismatches = [ + name + for name, delivery_value, expected_value in delivery_comparisons + if delivery_value != expected_value + ] + if delivery_mismatches: + raise ValueError("delivery disagrees with the participant view on: " + ", ".join(delivery_mismatches)) + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + _add_aces_invariant( + json_schema, + "decision-surface-v2-plane-separation", + "The participant view contains only participant-available choice material; derivation, policy, evidence, " + "provenance, and delivery remain in separate assurance and delivery planes.", + validator="raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface", + inputs=[{"contract_id": "participant-decision-surface-v2", "instance_path": "#"}], + ) + _add_aces_invariant( + json_schema, + "decision-surface-v2-exact-cut-policy", + "Each exposed item is bound to the derivation state cut and exact policy decision, independently of the " + "participant decision epoch.", + validator="raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface", + inputs=[{"contract_id": "participant-decision-surface-v2", "instance_path": "#/assurance"}], + ) + _add_aces_invariant( + json_schema, + "decision-surface-v2-delivery-before-selection", + "A surface is actionable only in delivered state, with delivery bound to the canonical participant view.", + validator="raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface", + inputs=[{"contract_id": "participant-decision-surface-v2", "instance_path": "#/delivery"}], + ) + _add_aces_invariant( + json_schema, + "decision-surface-v2-explicit-memory-scope", + "Assurance declares whether participant-visible memory persists across episodes; an episode-local claim " + "requires an authoritative reset of every participant-visible memory channel.", + validator=("raes_contracts.contracts.ParticipantDecisionSurfaceAssuranceV2Model._validate_assurance"), + inputs=[{"contract_id": "participant-decision-surface-v2", "instance_path": "#/assurance"}], + ) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"surface_state": {"const": "delivered"}}, + "required": ["surface_state"], + }, + "then": { + "required": ["delivery"], + "properties": {"delivery": {"not": {"type": "null"}}}, + }, + }, + { + "if": { + "properties": {"surface_state": {"const": "projected"}}, + "required": ["surface_state"], + }, + "then": {"properties": {"delivery": {"type": "null"}}}, + }, + ] + ) + return json_schema + + +class ParticipantDecisionSurfaceSelectionV2Model(ContractModel): + """A proposal selected from the exact delivered v2 participant view.""" + + surface_id: NonEmptyString + decision_epoch: StrictInt = Field(ge=0) + participant_view_digest: DigestString + delivery_ref: NonEmptyString + action_contract_address: NonEmptyString + argument_shape_ref: NonEmptyString + proposal_ref: NonEmptyString + arguments: dict[ + NonEmptyString, + str | int | float | bool | list[str | int | float | bool], + ] = Field(default_factory=dict) + + +__all__ = ( + "ParticipantDecisionSurfaceAssuranceV2Model", + "ParticipantDecisionSurfaceBehaviorAnchorV2Model", + "ParticipantDecisionSurfaceCausalCutModel", + "ParticipantDecisionSurfaceDeliveryV2Model", + "ParticipantDecisionSurfaceDerivationAnchorV2Model", + "ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model", + "ParticipantDecisionSurfaceSelectionV2Model", + "ParticipantDecisionSurfaceSequenceCutModel", + "ParticipantDecisionSurfaceStateCutModel", + "ParticipantDecisionSurfaceV2Model", + "ParticipantDecisionSurfaceViewV2Model", +) diff --git a/implementations/python/packages/raes_contracts/manifest_authority.py b/implementations/python/packages/raes_contracts/manifest_authority.py index ac265c7d8..4f7553159 100644 --- a/implementations/python/packages/raes_contracts/manifest_authority.py +++ b/implementations/python/packages/raes_contracts/manifest_authority.py @@ -78,6 +78,7 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2", ) diff --git a/implementations/python/packages/raes_contracts/participant_binding_v2.py b/implementations/python/packages/raes_contracts/participant_binding_v2.py new file mode 100644 index 000000000..22d2e17ae --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_binding_v2.py @@ -0,0 +1,153 @@ +"""Exact-cut participant decision-surface v2 binding DTOs.""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from typing import Protocol, cast + +from .contracts import ( + ParticipantDecisionSurfaceCandidateSetFormModel, + ParticipantDecisionSurfaceConstrainedFormModel, + ParticipantDecisionSurfaceSelectionV2Model, + ParticipantDecisionSurfaceV2Model, + ParticipantImplementationSelectionModel, +) +from .participant_action_arguments import ParticipantValidatedActionSelection +from .participant_binding import ( + ParticipantActionAdmissionRequest, + ParticipantDecisionSurfaceArgumentShapeResolver, + participant_action_admission_request_violations, +) +from .participant_decision_surface_delivery import ( + ParticipantDecisionSurfaceDeliveryResolverV2, + validate_participant_decision_surface_v2_delivery, +) + + +class ParticipantDecisionSurfaceApparatusResolverV2(Protocol): + """Resolve the run selection at the exact cut used to derive a v2 surface.""" + + def __call__( + self, + *, + implementation_selection_ref: str, + exposure_policy_ref: str, + decision_cut_ref: str, + ) -> ParticipantImplementationSelectionModel | None: ... + + +@dataclass(frozen=True) +class ParticipantDecisionSurfaceBindingResolversV2: + """Governed dependencies for a delivered v2 surface selection.""" + + argument_shape: ParticipantDecisionSurfaceArgumentShapeResolver + apparatus: ParticipantDecisionSurfaceApparatusResolverV2 + delivery: ParticipantDecisionSurfaceDeliveryResolverV2 + + +def bind_participant_decision_surface_selection_v2( + *, + surface: ParticipantDecisionSurfaceV2Model, + selection: ParticipantDecisionSurfaceSelectionV2Model, + admission_request: ParticipantActionAdmissionRequest, + argument_shape_resolver: ParticipantDecisionSurfaceArgumentShapeResolver, + apparatus_resolver: ParticipantDecisionSurfaceApparatusResolverV2, + delivery_resolver: ParticipantDecisionSurfaceDeliveryResolverV2, +) -> ParticipantActionAdmissionRequest: + """Validate a selection from an exact, currently delivered v2 view.""" + + validate_participant_decision_surface_v2_delivery(surface, delivery_resolver) + view = surface.participant_view + assurance = surface.assurance + delivery = surface.delivery + assert delivery is not None + if selection.surface_id != view.surface_id: + raise ValueError("selection surface_id must match the participant decision surface") + if selection.decision_epoch != view.decision_epoch: + raise ValueError("selection decision_epoch must match the participant decision surface") + if selection.participant_view_digest != assurance.participant_view_digest: + raise ValueError("selection participant_view_digest must match the canonical participant view") + if selection.delivery_ref != delivery.delivery_ref: + raise ValueError("selection delivery_ref must match the authoritative surface delivery") + entries = {entry.action_contract_address: entry for entry in view.action_entries} + entry = entries.get(selection.action_contract_address) + if entry is None: + raise ValueError("selection action_contract_address is not carried by the participant decision surface") + if entry.eligibility != "eligible": + raise ValueError("participant decision surface selection is not eligible") + if entry.support != "supported": + raise ValueError("participant decision surface selection is not supported") + if entry.selection_shape_ref != selection.argument_shape_ref: + raise ValueError("selection argument_shape_ref must match the participant decision surface action entry") + if isinstance(view.form, ParticipantDecisionSurfaceCandidateSetFormModel): + if entry.entry_id not in view.form.candidate_entry_ids: + raise ValueError("selection action is not a member of the participant candidate-action set") + elif isinstance(view.form, ParticipantDecisionSurfaceConstrainedFormModel): + if entry.entry_id != view.form.action_entry_id or selection.argument_shape_ref != view.form.argument_shape_ref: + raise ValueError("selection does not match the constrained-form action and argument shape") + else: + if selection.action_contract_address not in view.form.allowed_action_contract_addresses: + raise ValueError("open-ended proposal does not bind to an allowed governed action contract") + if selection.argument_shape_ref != view.form.argument_shape_ref: + raise ValueError("open-ended proposal does not bind to the governed argument shape") + if admission_request.participant_address != view.participant_address: + raise ValueError("admission request participant_address must match the participant decision surface") + if admission_request.action_contract_address != selection.action_contract_address: + raise ValueError("admission request action_contract_address must match the validated selection") + if admission_request.observation_boundary_address != assurance.observation_boundary_address: + raise ValueError("admission request observation_boundary_address must match the participant decision surface") + if admission_request.implementation_selection.selected_decision_surface_mode != view.decision_control_mode: + raise ValueError("admission request decision-control mode must match the participant decision surface") + if ( + "participant-decision-surface-v2" + not in admission_request.implementation_selection.participant_contract_versions + ): + raise ValueError("participant implementation selection must declare participant-decision-surface-v2 support") + try: + resolved_selection = apparatus_resolver( + implementation_selection_ref=assurance.implementation_selection_ref, + exposure_policy_ref=assurance.exposure_policy_ref, + decision_cut_ref=assurance.derivation_anchor.state_cut.cut_ref, + ) + except Exception as exc: + raise ValueError("participant decision surface exact-cut apparatus resolution failed") from exc + if resolved_selection is None: + raise ValueError("participant decision surface apparatus refs did not resolve at the derivation cut") + if resolved_selection.model_dump(mode="json") != admission_request.implementation_selection.model_dump(mode="json"): + raise ValueError("admission request implementation selection and exposure policy must match the surface refs") + try: + validated_selection = argument_shape_resolver( + action_contract_address=selection.action_contract_address, + argument_shape_ref=selection.argument_shape_ref, + proposal_ref=selection.proposal_ref, + proposed_arguments=selection.arguments, + ) + except Exception as exc: + raise ValueError("participant decision surface argument-shape resolution failed") from exc + if not isinstance(validated_selection, ParticipantValidatedActionSelection): + raise ValueError("participant decision surface proposal failed governed argument-shape validation") + if ( + validated_selection.action_contract_address, + validated_selection.argument_shape_ref, + validated_selection.proposal_ref, + ) != ( + selection.action_contract_address, + selection.argument_shape_ref, + selection.proposal_ref, + ): + raise ValueError("validated participant action selection must match the governed proposal coordinates") + bound_request = cast( + ParticipantActionAdmissionRequest, + replace(admission_request, validated_selection=validated_selection), + ) + violations = participant_action_admission_request_violations(bound_request) + if violations: + raise ValueError(violations[0]) + return bound_request + + +__all__ = ( + "ParticipantDecisionSurfaceApparatusResolverV2", + "ParticipantDecisionSurfaceBindingResolversV2", + "bind_participant_decision_surface_selection_v2", +) diff --git a/implementations/python/packages/raes_contracts/participant_decision_surface_delivery.py b/implementations/python/packages/raes_contracts/participant_decision_surface_delivery.py new file mode 100644 index 000000000..ad1f3f244 --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_decision_surface_delivery.py @@ -0,0 +1,67 @@ +"""Trusted delivery realization and re-resolution for decision-surface v2.""" + +from __future__ import annotations + +from typing import Protocol + +from .contracts import ( + ParticipantDecisionSurfaceDeliveryV2Model, + ParticipantDecisionSurfaceV2Model, +) + + +class ParticipantDecisionSurfaceDeliveryResolverV2(Protocol): + """Resolve an authoritative delivery occurrence by stable reference.""" + + def __call__( + self, + *, + delivery_ref: str, + ) -> ParticipantDecisionSurfaceDeliveryV2Model | None: ... + + +def deliver_participant_decision_surface_v2( + surface: ParticipantDecisionSurfaceV2Model, + *, + delivery_ref: str, + resolver: ParticipantDecisionSurfaceDeliveryResolverV2, +) -> ParticipantDecisionSurfaceV2Model: + """Bind an authoritative delivery occurrence to an exact projected view.""" + + if surface.surface_state != "projected" or surface.delivery is not None: + raise ValueError("only a projected decision surface can transition to delivered") + try: + delivery = resolver(delivery_ref=delivery_ref) + except Exception as exc: + raise ValueError("participant decision-surface delivery resolution failed") from exc + if delivery is None or delivery.delivery_ref != delivery_ref: + raise ValueError("participant decision-surface delivery_ref did not resolve") + payload = surface.model_dump(mode="json") + payload["surface_state"] = "delivered" + payload["delivery"] = delivery.model_dump(mode="json") + return ParticipantDecisionSurfaceV2Model.model_validate(payload) + + +def validate_participant_decision_surface_v2_delivery( + surface: ParticipantDecisionSurfaceV2Model, + resolver: ParticipantDecisionSurfaceDeliveryResolverV2, +) -> None: + """Require delivered state and re-resolve the exact delivery occurrence.""" + + if surface.surface_state != "delivered" or surface.delivery is None: + raise ValueError("participant decision-surface selection requires delivered state") + try: + resolved = resolver(delivery_ref=surface.delivery.delivery_ref) + except Exception as exc: + raise ValueError("participant decision-surface delivery resolution failed") from exc + if resolved is None: + raise ValueError("participant decision-surface delivery_ref did not resolve") + if resolved != surface.delivery: + raise ValueError("participant decision-surface delivery evidence is stale or forged") + + +__all__ = ( + "ParticipantDecisionSurfaceDeliveryResolverV2", + "deliver_participant_decision_surface_v2", + "validate_participant_decision_surface_v2_delivery", +) diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact.py b/implementations/python/packages/raes_operations/_evidence_run_artifact.py index f617b7e02..f1ed6604b 100644 --- a/implementations/python/packages/raes_operations/_evidence_run_artifact.py +++ b/implementations/python/packages/raes_operations/_evidence_run_artifact.py @@ -356,7 +356,7 @@ def _terminal_observation_section(snapshot: TerminalSnapshot) -> dict[str, Any]: "form": "participant-projected-history", "taxonomy": { "taxonomy_id": "aces-behavioral-relations", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "non_claimed_relation_ids": [ "participant-projected-history-equivalence", "epistemic-indistinguishability", diff --git a/implementations/python/packages/raes_processor/models/__init__.py b/implementations/python/packages/raes_processor/models/__init__.py index 4af082846..58ad2a1ae 100644 --- a/implementations/python/packages/raes_processor/models/__init__.py +++ b/implementations/python/packages/raes_processor/models/__init__.py @@ -132,10 +132,14 @@ ParticipantDecisionSurfaceProjectionInput, project_participant_decision_surface, ) -from .decision_surface_anchor import ( - resolve_participant_behavior_projection_anchor, - resolve_participant_episode_readiness_anchor, - validate_participant_decision_surface_projection_anchor, +from .decision_surface_anchor_v2 import ( + resolve_participant_behavior_projection_anchor_v2, + resolve_participant_episode_readiness_anchor_v2, + validate_participant_decision_surface_v2_anchor, +) +from .decision_surface_v2 import ( + ParticipantDecisionSurfaceProjectionInputV2, + project_participant_decision_surface_v2, ) from .history_event import ( ParticipantBehaviorHistoryEvent, @@ -155,6 +159,11 @@ ParticipantExposureRealizationAssessment, ParticipantExposureResolvers, ) +from .participant_exposure_authority_v2 import ( + ParticipantExposureAuthorizationRecordV2, + ParticipantExposurePolicyDecisionV2, + ParticipantExposureResolversV2, +) from .resources import ( AccountPlacement, AssertionRuntime, @@ -293,8 +302,13 @@ "ParticipantPhaseRealization", "ParticipantRuntimeLifecyclePhase", "project_participant_decision_surface", - "resolve_participant_behavior_projection_anchor", - "resolve_participant_episode_readiness_anchor", + "project_participant_decision_surface_v2", + "resolve_participant_behavior_projection_anchor_v2", + "resolve_participant_episode_readiness_anchor_v2", + "ParticipantDecisionSurfaceProjectionInputV2", + "ParticipantExposureAuthorizationRecordV2", + "ParticipantExposurePolicyDecisionV2", + "ParticipantExposureResolversV2", "ParticipantTemporalRuntimeContext", "ParticipantTemporalState", "ParticipantTemporalStateTransition", @@ -310,7 +324,7 @@ "RuntimeModel", "RuntimeSnapshot", "RuntimeSnapshotEnvelope", - "validate_participant_decision_surface_projection_anchor", + "validate_participant_decision_surface_v2_anchor", "RuntimeTemplate", "ScriptRuntime", "Severity", diff --git a/implementations/python/packages/raes_processor/models/decision_surface.py b/implementations/python/packages/raes_processor/models/decision_surface.py index dc5d95f2c..3b434ecfc 100644 --- a/implementations/python/packages/raes_processor/models/decision_surface.py +++ b/implementations/python/packages/raes_processor/models/decision_surface.py @@ -5,11 +5,7 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass -from raes_contracts.contracts import ( - ParticipantDecisionSurfaceModel, - ParticipantDecisionSurfaceProjectionAnchorModel, -) -from raes_contracts.runtime_state import RuntimeSnapshot +from raes_contracts.contracts import ParticipantDecisionSurfaceModel from .behavior_anchor_checks import participant_observation_effective_relation from .behavior_anchor_index import _participant_behavior_history_anchor_indexes @@ -18,7 +14,6 @@ ParticipantBehaviorSpecificationRuntime, ParticipantObservationBoundaryRuntime, ) -from .decision_surface_anchor import _validate_resolved_projection_anchor from .history_event import ParticipantBehaviorHistoryEvent from .participant_exposure import project_participant_exposure_bindings from .participant_exposure_authority import ( @@ -72,7 +67,6 @@ class ParticipantDecisionSurfaceProjectionInput: marking_definition_refs: tuple[str, ...] redaction_policy_ref: str | None semantic_limitations: tuple[str, ...] - projection_anchor: ParticipantDecisionSurfaceProjectionAnchorModel | None = None def _surface_action_refs(form: Mapping[str, object]) -> tuple[str, tuple[str, ...]]: @@ -193,20 +187,9 @@ def _validate_context_visibility( def _validate_projection_history( - runtime_model: RuntimeModel, history_events: Sequence[ParticipantBehaviorHistoryEvent], projection: ParticipantDecisionSurfaceProjectionInput, - runtime_snapshot: RuntimeSnapshot | None, -) -> int: - if projection.projection_anchor is not None: - if runtime_snapshot is None: - raise ValueError("anchored participant decision surfaces require the current trusted RuntimeSnapshot") - return _validate_resolved_projection_anchor( - runtime_model, - runtime_snapshot, - history_events, - projection, - ) +) -> None: if not history_events: raise ValueError("participant decision surfaces require time-indexed history; a final snapshot is insufficient") if projection.observation_order < 0 or projection.observation_order >= len(history_events): @@ -216,7 +199,6 @@ def _validate_projection_history( for event in history_events ): raise ValueError("participant decision surface history must contain one participant and episode") - return projection.observation_order def _resolve_projection_scope( @@ -238,14 +220,12 @@ def _resolve_projection_scope( def _projection_visibility_relation( history_events: Sequence[ParticipantBehaviorHistoryEvent], - *, - history_order: int, projection: ParticipantDecisionSurfaceProjectionInput, boundary: ParticipantObservationBoundaryRuntime, ) -> Mapping[str, str]: action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(history_events) relation, _ = participant_observation_effective_relation( - observation_index=history_order, + observation_index=projection.observation_order, boundary_address=projection.observation_boundary_address, boundary=boundary, action_attempts=action_attempts, @@ -365,37 +345,21 @@ def _surface_payload( "marking_definition_refs": list(projection.marking_definition_refs), "redaction_policy_ref": projection.redaction_policy_ref, "semantic_limitations": list(projection.semantic_limitations), - **( - {"projection_anchor": projection.projection_anchor.model_dump(mode="json")} - if projection.projection_anchor is not None - else {} - ), } def project_participant_decision_surface( runtime_model: RuntimeModel, *, - runtime_snapshot: RuntimeSnapshot | None = None, history_events: Sequence[ParticipantBehaviorHistoryEvent], projection: ParticipantDecisionSurfaceProjectionInput, exposure_resolvers: ParticipantExposureResolvers, ) -> ParticipantDecisionSurfaceModel: """Derive one surface from compiled meaning and one scoped history prefix.""" - history_order = _validate_projection_history( - runtime_model, - history_events, - projection, - runtime_snapshot, - ) + _validate_projection_history(history_events, projection) behavior, boundary = _resolve_projection_scope(runtime_model, projection) - relation = _projection_visibility_relation( - history_events, - history_order=history_order, - projection=projection, - boundary=boundary, - ) + relation = _projection_visibility_relation(history_events, projection, boundary) _validate_context_visibility( relation, refs=projection.visible_context_refs, diff --git a/implementations/python/packages/raes_processor/models/decision_surface_anchor.py b/implementations/python/packages/raes_processor/models/decision_surface_anchor.py deleted file mode 100644 index 988754f74..000000000 --- a/implementations/python/packages/raes_processor/models/decision_surface_anchor.py +++ /dev/null @@ -1,306 +0,0 @@ -"""Trusted lifecycle and behavior anchors for decision-surface projection.""" - -from __future__ import annotations - -import hashlib -import json -from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING - -from raes_contracts.contracts import ( - ParticipantDecisionSurfaceBehaviorAnchorModel, - ParticipantDecisionSurfaceEpisodeReadinessAnchorModel, - ParticipantDecisionSurfaceModel, - ParticipantDecisionSurfaceProjectionAnchorModel, -) -from raes_contracts.participant_behavior import ParticipantBehaviorHistoryEventType -from raes_contracts.participant_episode import ( - ParticipantEpisodeExecutionState, - ParticipantEpisodeHistoryEvent, - ParticipantEpisodeHistoryEventType, - ParticipantEpisodeStatus, - iter_participant_episode_snapshot_violations, -) -from raes_contracts.runtime_state import RuntimeSnapshot - -from .behavior_history_violations import iter_participant_behavior_history_violations -from .history_event import ParticipantBehaviorHistoryEvent -from .runtime_model import RuntimeModel - -if TYPE_CHECKING: - from .decision_surface import ParticipantDecisionSurfaceProjectionInput - - -def _stable_projection_event_ref(event_domain: str, payload: Mapping[str, object]) -> str: - try: - canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") - except (TypeError, ValueError) as exc: - raise ValueError("projection anchor events must have canonically serializable payloads") from exc - return f"participant-{event_domain}-event:sha256:{hashlib.sha256(canonical).hexdigest()}" - - -def _participant_episode_snapshot_context( - runtime_snapshot: RuntimeSnapshot, - participant_address: str, -) -> tuple[ParticipantEpisodeExecutionState, tuple[ParticipantEpisodeHistoryEvent, ...]]: - if not isinstance(runtime_snapshot, RuntimeSnapshot): - raise TypeError("runtime_snapshot must be a current trusted RuntimeSnapshot") - state_payload = runtime_snapshot.participant_episode_results.get(participant_address) - history_payloads = runtime_snapshot.participant_episode_history.get(participant_address) - if state_payload is None or history_payloads is None: - raise ValueError("projection anchor participant does not have current RUN-311 state and history") - violations = tuple( - iter_participant_episode_snapshot_violations( - {participant_address: state_payload}, - {participant_address: history_payloads}, - ) - ) - if violations: - raise ValueError(f"projection anchor RUN-311 snapshot is invalid: {violations[0][1]}") - state = ParticipantEpisodeExecutionState.from_payload(state_payload) - history = tuple(ParticipantEpisodeHistoryEvent.from_payload(payload) for payload in history_payloads) - if not history or history[0].event_type != ParticipantEpisodeHistoryEventType.EPISODE_INITIALIZED: - raise ValueError( - "projection anchors require the complete participant lifecycle history from episode_initialized" - ) - return state, history - - -def _current_episode_behavior_events( - runtime_snapshot: RuntimeSnapshot, - *, - participant_address: str, - episode_id: str, -) -> tuple[ParticipantBehaviorHistoryEvent, ...]: - payloads = runtime_snapshot.participant_behavior_history.get(participant_address, []) - events = tuple(ParticipantBehaviorHistoryEvent.from_payload(payload) for payload in payloads) - return tuple(event for event in events if event.episode_id == episode_id) - - -def resolve_participant_episode_readiness_anchor( - runtime_snapshot: RuntimeSnapshot, - *, - participant_address: str, - decision_surface_order: int, - evidence_refs: Sequence[str], - provenance_refs: Sequence[str], -) -> ParticipantDecisionSurfaceEpisodeReadinessAnchorModel: - """Resolve the current RUN-311 ``episode_running`` event as a trusted anchor.""" - - if decision_surface_order != 0: - raise ValueError("episode-readiness projection is always decision_surface_order zero") - state, history = _participant_episode_snapshot_context(runtime_snapshot, participant_address) - head = history[-1] - if ( - state.status != ParticipantEpisodeStatus.RUNNING - or head.event_type != ParticipantEpisodeHistoryEventType.EPISODE_RUNNING - ): - raise ValueError("episode-readiness projection requires the current lifecycle head to be episode_running") - if head.participant_address != state.participant_address or head.episode_id != state.episode_id: - raise ValueError("episode-readiness projection state and lifecycle head must identify the same episode") - if _current_episode_behavior_events( - runtime_snapshot, - participant_address=participant_address, - episode_id=state.episode_id, - ): - raise ValueError("episode-readiness projection requires empty current-episode behavior history") - event_ref = _stable_projection_event_ref("episode", head.to_payload()) - return ParticipantDecisionSurfaceEpisodeReadinessAnchorModel( - anchor_kind="episode_readiness", - participant_address=state.participant_address, - episode_id=state.episode_id, - decision_surface_order=decision_surface_order, - event_ref=event_ref, - anchor_order=len(history) - 1, - event_type=head.event_type.value, - episode_sequence_number=state.sequence_number, - evidence_refs=list(evidence_refs), - provenance_refs=list(dict.fromkeys((*provenance_refs, event_ref))), - ) - - -def _resolved_behavior_decision_surface_order( - events: Sequence[ParticipantBehaviorHistoryEvent], - behavior_history_order: int, -) -> int: - event = events[behavior_history_order] - if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - raise ValueError("behavior decision surfaces must be anchored by a terminal observation_emitted event") - return sum( - candidate.event_type == ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED - for candidate in events[: behavior_history_order + 1] - ) - - -def resolve_participant_behavior_projection_anchor( - runtime_snapshot: RuntimeSnapshot, - *, - runtime_model: RuntimeModel, - participant_address: str, - episode_id: str, - decision_surface_order: int, - behavior_history_order: int, - evidence_refs: Sequence[str], - provenance_refs: Sequence[str], -) -> ParticipantDecisionSurfaceBehaviorAnchorModel: - """Resolve one exact event in the current participant/episode behavior prefix.""" - - state, episode_history = _participant_episode_snapshot_context(runtime_snapshot, participant_address) - if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != episode_id: - raise ValueError("behavior projection anchor must identify the current running participant episode") - events = _current_episode_behavior_events( - runtime_snapshot, - participant_address=participant_address, - episode_id=episode_id, - ) - if behavior_history_order < 0 or behavior_history_order >= len(events): - raise ValueError("behavior_history_order must identify an event in the current episode behavior history") - if behavior_history_order != len(events) - 1: - raise ValueError("behavior projection anchor must identify the exact current behavior-history prefix head") - payloads = [event.to_payload() for event in events] - violations = tuple( - iter_participant_behavior_history_violations( - payloads, - action_contracts=runtime_model.action_contracts, - observation_boundaries=runtime_model.observation_boundaries, - participant_episode_history=[event.to_payload() for event in episode_history], - expected_participant_address=participant_address, - ) - ) - if violations: - raise ValueError(f"projection anchor behavior history is invalid: {violations[0][1]}") - event = events[behavior_history_order] - resolved_decision_surface_order = _resolved_behavior_decision_surface_order(events, behavior_history_order) - if decision_surface_order != resolved_decision_surface_order: - raise ValueError( - "decision_surface_order must equal the number of completed observation_emitted events in the episode" - ) - event_ref = _stable_projection_event_ref("behavior", event.to_payload()) - return ParticipantDecisionSurfaceBehaviorAnchorModel( - anchor_kind="behavior_event", - participant_address=participant_address, - episode_id=episode_id, - decision_surface_order=decision_surface_order, - event_ref=event_ref, - anchor_order=behavior_history_order, - event_type=event.event_type.value, - action_instance_id=event.action_instance_id, - history_prefix_length=len(events), - evidence_refs=list(evidence_refs), - provenance_refs=list(dict.fromkeys((*provenance_refs, event_ref))), - ) - - -def _validate_projection_anchor_refs( - anchor: ParticipantDecisionSurfaceProjectionAnchorModel, - projection: ParticipantDecisionSurfaceProjectionInput, -) -> None: - mismatched = [ - name - for name, anchor_value, projection_value in ( - ("participant_address", anchor.participant_address, projection.participant_address), - ("episode_id", anchor.episode_id, projection.episode_id), - ("decision_surface_order", anchor.decision_surface_order, projection.observation_order), - ) - if anchor_value != projection_value - ] - if mismatched: - raise ValueError("projection anchor disagrees with projection input on: " + ", ".join(mismatched)) - if not set(anchor.evidence_refs).issubset(projection.evidence_refs): - raise ValueError("projection anchor evidence_refs must be carried by projection evidence_refs") - if not set(anchor.provenance_refs).issubset(projection.provenance_refs): - raise ValueError("projection anchor provenance_refs must be carried by projection provenance_refs") - - -def _validate_resolved_projection_anchor( - runtime_model: RuntimeModel, - runtime_snapshot: RuntimeSnapshot, - history_events: Sequence[ParticipantBehaviorHistoryEvent], - projection: ParticipantDecisionSurfaceProjectionInput, -) -> int: - anchor = projection.projection_anchor - if anchor is None: - raise ValueError("projection_anchor is required for trusted anchor validation") - _validate_projection_anchor_refs(anchor, projection) - if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorModel): - resolved = resolve_participant_episode_readiness_anchor( - runtime_snapshot, - participant_address=anchor.participant_address, - decision_surface_order=anchor.decision_surface_order, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - if resolved != anchor: - raise ValueError("episode-readiness projection anchor does not match the current trusted RuntimeSnapshot") - if history_events: - raise ValueError("episode-readiness projection requires empty current-episode behavior history") - return 0 - resolved = resolve_participant_behavior_projection_anchor( - runtime_snapshot, - runtime_model=runtime_model, - participant_address=anchor.participant_address, - episode_id=anchor.episode_id, - decision_surface_order=anchor.decision_surface_order, - behavior_history_order=anchor.anchor_order, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - if resolved != anchor: - raise ValueError("behavior projection anchor does not match the current trusted RuntimeSnapshot") - current_prefix = _current_episode_behavior_events( - runtime_snapshot, - participant_address=anchor.participant_address, - episode_id=anchor.episode_id, - ) - if tuple(history_events) != current_prefix: - raise ValueError("behavior projection requires the exact current behavior-history prefix") - return anchor.anchor_order - - -def validate_participant_decision_surface_projection_anchor( - runtime_snapshot: RuntimeSnapshot, - surface: ParticipantDecisionSurfaceModel, -) -> None: - """Reject an anchored surface that is no longer current at admission.""" - - anchor = surface.projection_anchor - if anchor is None: - return - if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorModel): - resolved = resolve_participant_episode_readiness_anchor( - runtime_snapshot, - participant_address=anchor.participant_address, - decision_surface_order=anchor.decision_surface_order, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - else: - state, _ = _participant_episode_snapshot_context(runtime_snapshot, anchor.participant_address) - if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != anchor.episode_id: - raise ValueError("behavior projection anchor is outside the current running episode") - events = _current_episode_behavior_events( - runtime_snapshot, - participant_address=anchor.participant_address, - episode_id=anchor.episode_id, - ) - if anchor.anchor_order != len(events) - 1: - raise ValueError("behavior projection anchor is not the current behavior-history prefix head") - event = events[anchor.anchor_order] - resolved_decision_surface_order = _resolved_behavior_decision_surface_order(events, anchor.anchor_order) - if anchor.decision_surface_order != resolved_decision_surface_order: - raise ValueError("behavior projection anchor has a stale or forged decision_surface_order") - resolved = ParticipantDecisionSurfaceBehaviorAnchorModel( - anchor_kind="behavior_event", - participant_address=anchor.participant_address, - episode_id=anchor.episode_id, - decision_surface_order=anchor.decision_surface_order, - event_ref=_stable_projection_event_ref("behavior", event.to_payload()), - anchor_order=anchor.anchor_order, - event_type=event.event_type.value, - action_instance_id=event.action_instance_id, - history_prefix_length=len(events), - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - if resolved != anchor: - raise ValueError("participant decision surface projection anchor is stale or does not resolve") diff --git a/implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py b/implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py new file mode 100644 index 000000000..7f3176422 --- /dev/null +++ b/implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py @@ -0,0 +1,274 @@ +"""Trusted lifecycle/behavior state cuts for participant decision-surface v2.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence + +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceBehaviorAnchorV2Model, + ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model, + ParticipantDecisionSurfaceSequenceCutModel, + ParticipantDecisionSurfaceV2Model, +) +from raes_contracts.participant_behavior import ParticipantBehaviorHistoryEventType +from raes_contracts.participant_episode import ( + ParticipantEpisodeExecutionState, + ParticipantEpisodeHistoryEvent, + ParticipantEpisodeHistoryEventType, + ParticipantEpisodeStatus, + iter_participant_episode_snapshot_violations, +) +from raes_contracts.runtime_state import RuntimeSnapshot + +from .behavior_history_violations import iter_participant_behavior_history_violations +from .history_event import ParticipantBehaviorHistoryEvent +from .runtime_model import RuntimeModel + + +def _stable_projection_event_ref(event_domain: str, payload: Mapping[str, object]) -> str: + try: + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + except (TypeError, ValueError) as exc: + raise ValueError("decision-surface anchor events must have canonically serializable payloads") from exc + return f"participant-{event_domain}-event:sha256:{hashlib.sha256(canonical).hexdigest()}" + + +def _participant_episode_snapshot_context( + runtime_snapshot: RuntimeSnapshot, + participant_address: str, +) -> tuple[ParticipantEpisodeExecutionState, tuple[ParticipantEpisodeHistoryEvent, ...]]: + if not isinstance(runtime_snapshot, RuntimeSnapshot): + raise TypeError("runtime_snapshot must be a current trusted RuntimeSnapshot") + state_payload = runtime_snapshot.participant_episode_results.get(participant_address) + history_payloads = runtime_snapshot.participant_episode_history.get(participant_address) + if state_payload is None or history_payloads is None: + raise ValueError("derivation anchor participant does not have current RUN-311 state and history") + violations = tuple( + iter_participant_episode_snapshot_violations( + {participant_address: state_payload}, + {participant_address: history_payloads}, + ) + ) + if violations: + raise ValueError(f"derivation anchor RUN-311 snapshot is invalid: {violations[0][1]}") + state = ParticipantEpisodeExecutionState.from_payload(state_payload) + history = tuple(ParticipantEpisodeHistoryEvent.from_payload(payload) for payload in history_payloads) + if not history or history[0].event_type != ParticipantEpisodeHistoryEventType.EPISODE_INITIALIZED: + raise ValueError("derivation anchors require the complete lifecycle history from episode_initialized") + return state, history + + +def _current_episode_behavior_events( + runtime_snapshot: RuntimeSnapshot, + *, + participant_address: str, + episode_id: str, +) -> tuple[ParticipantBehaviorHistoryEvent, ...]: + payloads = runtime_snapshot.participant_behavior_history.get(participant_address, []) + events = tuple(ParticipantBehaviorHistoryEvent.from_payload(payload) for payload in payloads) + return tuple(event for event in events if event.episode_id == episode_id) + + +def _stable_state_cut_ref(payload: dict[str, object]) -> str: + return _stable_projection_event_ref("state-cut", payload) + + +def _sequence_cut( + *, + history_domain: str, + order_model: str, + event_refs: Sequence[str], +) -> ParticipantDecisionSurfaceSequenceCutModel: + if not event_refs: + raise ValueError("decision-surface state cuts require a non-empty event prefix") + payload: dict[str, object] = { + "cut_kind": "sequence_prefix", + "history_domain": history_domain, + "order_model": order_model, + "anchor_event_ref": event_refs[-1], + "anchor_order": len(event_refs) - 1, + "history_prefix_length": len(event_refs), + "predecessor_event_refs": list(event_refs[:-1]), + } + return ParticipantDecisionSurfaceSequenceCutModel( + cut_ref=_stable_state_cut_ref(payload), + **payload, + ) + + +def resolve_participant_episode_readiness_anchor_v2( + runtime_snapshot: RuntimeSnapshot, + *, + participant_address: str, + decision_epoch: int, + evidence_refs: Sequence[str], + provenance_refs: Sequence[str], +) -> ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model: + """Resolve initial decision epoch D0 from the current ``episode_running`` cut.""" + + if decision_epoch != 0: + raise ValueError("episode-readiness projection requires decision_epoch zero") + state, history = _participant_episode_snapshot_context(runtime_snapshot, participant_address) + head = history[-1] + if ( + state.status != ParticipantEpisodeStatus.RUNNING + or head.event_type != ParticipantEpisodeHistoryEventType.EPISODE_RUNNING + ): + raise ValueError("episode-readiness projection requires the current lifecycle head to be episode_running") + if head.participant_address != state.participant_address or head.episode_id != state.episode_id: + raise ValueError("episode-readiness state and lifecycle head must identify the same episode") + if _current_episode_behavior_events( + runtime_snapshot, + participant_address=participant_address, + episode_id=state.episode_id, + ): + raise ValueError("episode-readiness projection requires empty current-episode behavior history") + event_refs = tuple(_stable_projection_event_ref("episode", event.to_payload()) for event in history) + state_cut = _sequence_cut( + history_domain="participant_episode_lifecycle", + order_model="control_plane_order", + event_refs=event_refs, + ) + return ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model( + anchor_kind="episode_readiness", + participant_address=state.participant_address, + episode_id=state.episode_id, + decision_epoch=decision_epoch, + event_ref=event_refs[-1], + state_cut=state_cut, + event_type=head.event_type.value, + episode_sequence_number=state.sequence_number, + evidence_refs=list(evidence_refs), + provenance_refs=list(dict.fromkeys((*provenance_refs, event_refs[-1]))), + ) + + +def _behavior_decision_epoch(events: Sequence[ParticipantBehaviorHistoryEvent]) -> int: + return sum(event.event_type == ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED for event in events) + + +def resolve_participant_behavior_projection_anchor_v2( + runtime_snapshot: RuntimeSnapshot, + *, + runtime_model: RuntimeModel, + participant_address: str, + episode_id: str, + decision_epoch: int, + behavior_history_order: int, + evidence_refs: Sequence[str], + provenance_refs: Sequence[str], +) -> ParticipantDecisionSurfaceBehaviorAnchorV2Model: + """Resolve a later decision epoch from the exact current behavior prefix.""" + + state, episode_history = _participant_episode_snapshot_context(runtime_snapshot, participant_address) + if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != episode_id: + raise ValueError("behavior projection anchor must identify the current running participant episode") + events = _current_episode_behavior_events( + runtime_snapshot, + participant_address=participant_address, + episode_id=episode_id, + ) + if behavior_history_order < 0 or behavior_history_order >= len(events): + raise ValueError("behavior_history_order must identify an event in the current episode behavior history") + if behavior_history_order != len(events) - 1: + raise ValueError("behavior projection anchor must identify the exact current behavior-history prefix head") + violations = tuple( + iter_participant_behavior_history_violations( + [event.to_payload() for event in events], + action_contracts=runtime_model.action_contracts, + observation_boundaries=runtime_model.observation_boundaries, + participant_episode_history=[event.to_payload() for event in episode_history], + expected_participant_address=participant_address, + ) + ) + if violations: + raise ValueError(f"projection anchor behavior history is invalid: {violations[0][1]}") + event = events[-1] + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + raise ValueError("behavior decision surfaces require a terminal observation_emitted event") + resolved_epoch = _behavior_decision_epoch(events) + if decision_epoch != resolved_epoch: + raise ValueError("decision_epoch must equal the number of completed participant observations") + event_refs = tuple(_stable_projection_event_ref("behavior", item.to_payload()) for item in events) + state_cut = _sequence_cut( + history_domain="participant_behavior_history", + order_model="behavior_history_order", + event_refs=event_refs, + ) + return ParticipantDecisionSurfaceBehaviorAnchorV2Model( + anchor_kind="behavior_event", + participant_address=participant_address, + episode_id=episode_id, + decision_epoch=decision_epoch, + event_ref=event_refs[-1], + state_cut=state_cut, + event_type=event.event_type.value, + action_instance_id=event.action_instance_id, + evidence_refs=list(evidence_refs), + provenance_refs=list(dict.fromkeys((*provenance_refs, event_refs[-1]))), + ) + + +def validate_participant_decision_surface_v2_anchor( + runtime_snapshot: RuntimeSnapshot, + surface: ParticipantDecisionSurfaceV2Model, + runtime_model: RuntimeModel | None = None, +) -> None: + """Re-resolve the exact v2 derivation anchor against current trusted state.""" + + anchor = surface.assurance.derivation_anchor + if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model): + resolved = resolve_participant_episode_readiness_anchor_v2( + runtime_snapshot, + participant_address=anchor.participant_address, + decision_epoch=anchor.decision_epoch, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + else: + state_cut = anchor.state_cut + if not isinstance(state_cut, ParticipantDecisionSurfaceSequenceCutModel): + raise ValueError("the reference runtime cannot re-resolve a causal-frontier behavior anchor") + if runtime_model is not None: + resolved = resolve_participant_behavior_projection_anchor_v2( + runtime_snapshot, + runtime_model=runtime_model, + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + decision_epoch=anchor.decision_epoch, + behavior_history_order=state_cut.anchor_order, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + else: + state, _ = _participant_episode_snapshot_context(runtime_snapshot, anchor.participant_address) + events = _current_episode_behavior_events( + runtime_snapshot, + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + ) + if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != anchor.episode_id: + raise ValueError("behavior derivation anchor is outside the current running episode") + if not events or events[-1].event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + raise ValueError("behavior derivation anchor is not at a terminal participant observation") + event_refs = tuple(_stable_projection_event_ref("behavior", item.to_payload()) for item in events) + resolved = ParticipantDecisionSurfaceBehaviorAnchorV2Model( + anchor_kind="behavior_event", + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + decision_epoch=_behavior_decision_epoch(events), + event_ref=event_refs[-1], + state_cut=_sequence_cut( + history_domain="participant_behavior_history", + order_model="behavior_history_order", + event_refs=event_refs, + ), + event_type=events[-1].event_type.value, + action_instance_id=events[-1].action_instance_id, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + if resolved != anchor: + raise ValueError("participant decision-surface v2 derivation anchor is stale or does not resolve") diff --git a/implementations/python/packages/raes_processor/models/decision_surface_v2.py b/implementations/python/packages/raes_processor/models/decision_surface_v2.py new file mode 100644 index 000000000..1e526c0db --- /dev/null +++ b/implementations/python/packages/raes_processor/models/decision_surface_v2.py @@ -0,0 +1,323 @@ +"""State-cut-derived SEM-220 participant decision-surface v2 projection.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass + +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceDerivationAnchorV2Model, + ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model, + ParticipantDecisionSurfaceSequenceCutModel, + ParticipantDecisionSurfaceV2Model, + ParticipantDecisionSurfaceViewV2Model, +) +from raes_contracts.runtime_state import RuntimeSnapshot +from raes_contracts.satisfiability import canonical_contract_digest + +from .behavior_anchor_checks import participant_observation_effective_relation +from .behavior_anchor_index import _participant_behavior_history_anchor_indexes +from .behavior_ref_checks import _participant_behavior_initial_view_relation +from .behavior_resources import ( + _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS, + ParticipantBehaviorSpecificationRuntime, + ParticipantObservationBoundaryRuntime, +) +from .decision_surface import ( + ParticipantDecisionSurfaceActionAssessment, + _action_affordance_addresses, + _surface_action_assessments, +) +from .decision_surface_anchor_v2 import ( + resolve_participant_behavior_projection_anchor_v2, + resolve_participant_episode_readiness_anchor_v2, +) +from .history_event import ParticipantBehaviorHistoryEvent +from .participant_exposure import ParticipantExposureAssessment +from .participant_exposure_authority_v2 import ParticipantExposureResolversV2 +from .participant_exposure_v2 import project_participant_exposure_bindings_v2 +from .runtime_model import RuntimeModel + + +@dataclass(frozen=True) +class ParticipantDecisionSurfaceProjectionInputV2: + """Governed inputs for one participant view at one decision epoch.""" + + surface_id: str + participant_address: str + episode_id: str + decision_epoch: int + information_state_ref: str + behavior_specification_address: str + observation_boundary_address: str + context_view_ref: str + implementation_selection_ref: str + decision_control_mode: str + audience_scope_ref: str + projection_policy_ref: str + projection_policy_revision: str + projection_policy_decision_ref: str + exposure_policy_ref: str + visibility_projection_ref: str + participant_memory_scope: str + memory_reset_authority_ref: str | None + visible_context_refs: tuple[str, ...] + action_assessments: Mapping[str, ParticipantDecisionSurfaceActionAssessment] + exposure_assessments: Mapping[str, ParticipantExposureAssessment] + form: Mapping[str, object] + evidence_refs: tuple[str, ...] + provenance_refs: tuple[str, ...] + marking_definition_refs: tuple[str, ...] + redaction_policy_ref: str | None + semantic_limitations: tuple[str, ...] + derivation_anchor: ParticipantDecisionSurfaceDerivationAnchorV2Model + + @property + def decision_cut_ref(self) -> str: + return self.derivation_anchor.state_cut.cut_ref + + +def _resolve_projection_scope( + runtime_model: RuntimeModel, + projection: ParticipantDecisionSurfaceProjectionInputV2, +) -> tuple[ParticipantBehaviorSpecificationRuntime, ParticipantObservationBoundaryRuntime]: + behavior = runtime_model.behavior_specifications.get(projection.behavior_specification_address) + if behavior is None: + raise ValueError("behavior_specification_address does not resolve in the compiled runtime model") + if projection.participant_address not in behavior.participant_addresses: + raise ValueError("participant_address is outside the compiled behavior specification") + if projection.observation_boundary_address not in behavior.observation_boundary_addresses: + raise ValueError("observation_boundary_address is outside the compiled behavior specification") + boundary = runtime_model.observation_boundaries.get(projection.observation_boundary_address) + if boundary is None: + raise ValueError("observation_boundary_address does not resolve in the compiled runtime model") + return behavior, boundary + + +def _validate_and_resolve_anchor( + runtime_model: RuntimeModel, + runtime_snapshot: RuntimeSnapshot, + history_events: Sequence[ParticipantBehaviorHistoryEvent], + projection: ParticipantDecisionSurfaceProjectionInputV2, +) -> int | None: + anchor = projection.derivation_anchor + coordinates = ( + ("participant_address", anchor.participant_address, projection.participant_address), + ("episode_id", anchor.episode_id, projection.episode_id), + ("decision_epoch", anchor.decision_epoch, projection.decision_epoch), + ) + mismatched = [name for name, anchor_value, projected_value in coordinates if anchor_value != projected_value] + if mismatched: + raise ValueError("derivation anchor disagrees with projection input on: " + ", ".join(mismatched)) + if not set(anchor.evidence_refs).issubset(projection.evidence_refs): + raise ValueError("derivation anchor evidence must be carried by assurance") + if not set(anchor.provenance_refs).issubset(projection.provenance_refs): + raise ValueError("derivation anchor provenance must be carried by assurance") + if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model): + resolved = resolve_participant_episode_readiness_anchor_v2( + runtime_snapshot, + participant_address=projection.participant_address, + decision_epoch=projection.decision_epoch, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + if history_events: + raise ValueError("initial decision epoch requires empty current-episode behavior history") + if resolved != anchor: + raise ValueError("episode-readiness anchor does not match the current trusted snapshot") + return None + if not isinstance(anchor.state_cut, ParticipantDecisionSurfaceSequenceCutModel): + raise ValueError("the reference projector cannot resolve a causal-frontier behavior anchor") + resolved = resolve_participant_behavior_projection_anchor_v2( + runtime_snapshot, + runtime_model=runtime_model, + participant_address=projection.participant_address, + episode_id=projection.episode_id, + decision_epoch=projection.decision_epoch, + behavior_history_order=anchor.state_cut.anchor_order, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + if resolved != anchor: + raise ValueError("behavior derivation anchor does not match the current trusted snapshot") + current = tuple( + ParticipantBehaviorHistoryEvent.from_payload(payload) + for payload in runtime_snapshot.participant_behavior_history.get(projection.participant_address, []) + if payload.get("episode_id") == projection.episode_id + ) + if tuple(history_events) != current: + raise ValueError("behavior projection requires the exact current behavior-history prefix") + return anchor.state_cut.anchor_order + + +def _visibility_relation( + history_events: Sequence[ParticipantBehaviorHistoryEvent], + *, + history_order: int | None, + boundary_address: str, + boundary: ParticipantObservationBoundaryRuntime, +) -> Mapping[str, str]: + if history_order is None: + return _participant_behavior_initial_view_relation(boundary) + action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(history_events) + relation, _ = participant_observation_effective_relation( + observation_index=history_order, + boundary_address=boundary_address, + boundary=boundary, + action_attempts=action_attempts, + state_transitions=state_transitions, + observations=observations, + ) + return relation + + +def _validate_visible_refs( + relation: Mapping[str, str], + refs: Sequence[str], + *, + state_cut_ref: str, +) -> None: + hidden = sorted(ref for ref in refs if relation.get(ref) not in _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS) + if hidden: + raise ValueError(f"refs are not participant-visible at state cut {state_cut_ref!r}: " + ", ".join(hidden)) + + +def _project_action( + runtime_model: RuntimeModel, + behavior: ParticipantBehaviorSpecificationRuntime, + relation: Mapping[str, str], + assessment: ParticipantDecisionSurfaceActionAssessment, + *, + state_cut_ref: str, +) -> tuple[dict[str, object], tuple[str, ...]]: + action_address = assessment.action_contract_address + if action_address not in behavior.action_contract_addresses: + raise ValueError(f"action {action_address!r} is outside the compiled behavior specification") + action_contract = runtime_model.action_contracts.get(action_address) + if action_contract is None: + raise ValueError(f"action {action_address!r} does not resolve in the compiled runtime model") + if not action_contract.argument_shape_ref or assessment.selection_shape_ref != action_contract.argument_shape_ref: + raise ValueError(f"action {action_address!r} selection shape does not match its compiled argument shape") + affordances = _action_affordance_addresses( + runtime_model, + behavior_affordance_addresses=behavior.tool_affordance_addresses, + action_address=action_address, + ) + _validate_visible_refs(relation, (action_address, *affordances), state_cut_ref=state_cut_ref) + dispositions = {relation[ref] for ref in (action_address, *affordances)} + if len(dispositions) != 1: + raise ValueError(f"action {action_address!r} has conflicting view dispositions at the state cut") + visibility = next(iter(dispositions)) + return ( + { + "entry_id": assessment.entry_id, + "action_contract_address": action_address, + "presentation_basis_ref": assessment.presentation_basis_ref, + "visibility": visibility, + "eligibility": assessment.eligibility, + "eligibility_reason_refs": list(assessment.eligibility_reason_refs), + "constraint_refs": list(assessment.constraint_refs), + "selection_shape_ref": action_contract.argument_shape_ref, + "support": assessment.support, + "support_refs": list(assessment.support_refs), + "affordance_refs": list(affordances), + "realization_refs": list(assessment.realization_refs), + }, + affordances, + ) + + +def project_participant_decision_surface_v2( + runtime_model: RuntimeModel, + runtime_snapshot: RuntimeSnapshot, + *, + history_events: Sequence[ParticipantBehaviorHistoryEvent], + projection: ParticipantDecisionSurfaceProjectionInputV2, + exposure_resolvers: ParticipantExposureResolversV2, +) -> ParticipantDecisionSurfaceV2Model: + """Derive a projected v2 surface from an exact trusted state cut.""" + + behavior, boundary = _resolve_projection_scope(runtime_model, projection) + history_order = _validate_and_resolve_anchor( + runtime_model, + runtime_snapshot, + history_events, + projection, + ) + relation = _visibility_relation( + history_events, + history_order=history_order, + boundary_address=projection.observation_boundary_address, + boundary=boundary, + ) + _validate_visible_refs( + relation, + projection.visible_context_refs, + state_cut_ref=projection.decision_cut_ref, + ) + entries: list[dict[str, object]] = [] + affordances: list[str] = [] + for assessment in _surface_action_assessments(projection): + entry, entry_affordances = _project_action( + runtime_model, + behavior, + relation, + assessment, + state_cut_ref=projection.decision_cut_ref, + ) + entries.append(entry) + affordances.extend(entry_affordances) + surface_affordances = list(dict.fromkeys(affordances)) + exposure_bindings, _ = project_participant_exposure_bindings_v2( + relation, + projection, + entries, + surface_affordances, + exposure_resolvers, + ) + participant_view_payload = { + "surface_id": projection.surface_id, + "participant_address": projection.participant_address, + "episode_id": projection.episode_id, + "decision_epoch": projection.decision_epoch, + "information_state_ref": projection.information_state_ref, + "context_view_ref": projection.context_view_ref, + "decision_control_mode": projection.decision_control_mode, + "visible_context_refs": list(projection.visible_context_refs), + "action_entries": entries, + "affordance_refs": surface_affordances, + "form": dict(projection.form), + "marking_definition_refs": list(projection.marking_definition_refs), + "redaction_policy_ref": projection.redaction_policy_ref, + "semantic_limitations": list(projection.semantic_limitations), + } + view_model = ParticipantDecisionSurfaceViewV2Model.model_validate(participant_view_payload) + assurance_payload = { + "participant_address": projection.participant_address, + "episode_id": projection.episode_id, + "decision_epoch": projection.decision_epoch, + "behavior_specification_address": projection.behavior_specification_address, + "observation_boundary_address": projection.observation_boundary_address, + "implementation_selection_ref": projection.implementation_selection_ref, + "audience_scope_ref": projection.audience_scope_ref, + "projection_policy_ref": projection.projection_policy_ref, + "projection_policy_revision": projection.projection_policy_revision, + "projection_policy_decision_ref": projection.projection_policy_decision_ref, + "exposure_policy_ref": projection.exposure_policy_ref, + "visibility_projection_ref": projection.visibility_projection_ref, + "participant_memory_scope": projection.participant_memory_scope, + "memory_reset_authority_ref": projection.memory_reset_authority_ref, + "participant_view_digest": canonical_contract_digest(view_model), + "derivation_anchor": projection.derivation_anchor.model_dump(mode="json"), + "exposure_bindings": exposure_bindings, + "evidence_refs": list(projection.evidence_refs), + "provenance_refs": list(projection.provenance_refs), + } + return ParticipantDecisionSurfaceV2Model.model_validate( + { + "schema_version": "participant-decision-surface/v2", + "surface_state": "projected", + "participant_view": participant_view_payload, + "assurance": assurance_payload, + } + ) diff --git a/implementations/python/packages/raes_processor/models/participant_exposure_authority_v2.py b/implementations/python/packages/raes_processor/models/participant_exposure_authority_v2.py new file mode 100644 index 000000000..77b6ca809 --- /dev/null +++ b/implementations/python/packages/raes_processor/models/participant_exposure_authority_v2.py @@ -0,0 +1,129 @@ +"""Exact-state-cut authority records and resolvers for SEM-226 v2 exposure.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Protocol + +from raes_contracts.contracts import ParticipantImplementationSelectionModel + +from .participant_exposure_authority import ParticipantExposureAssessment + + +@dataclass(frozen=True) +class ParticipantExposurePolicyDecisionV2: + """The projection-policy decision effective at one exact state cut.""" + + policy_ref: str + revision: str + decision_ref: str + decision_cut_ref: str + evidence_refs: tuple[str, ...] + provenance_refs: tuple[str, ...] + limitations: tuple[str, ...] + + +@dataclass(frozen=True) +class ParticipantExposureAuthorizationRecordV2: + """An item authorization evaluated at one exact state cut.""" + + authorization_record_ref: str + item_ref: str + source_ref: str + source_layer_ref: str + participant_address: str + episode_id: str + audience_scope_ref: str + decision_epoch: int + decision_cut_ref: str + implementation_selection_ref: str + projection_policy_ref: str + projection_policy_revision: str + projection_policy_decision_ref: str + exposure_policy_ref: str + exposure_policy_version: str + exposure_policy_digest: str + visibility_basis_ref: str + operation: str + operation_basis_ref: str + actor_ref: str + controller_ref: str + authority_basis_ref: str + backend_support_ref: str + source_marking_definition_refs: tuple[str, ...] + result_marking_definition_refs: tuple[str, ...] + source_provenance_refs: tuple[str, ...] + result_provenance_refs: tuple[str, ...] + evidence_refs: tuple[str, ...] + provenance_refs: tuple[str, ...] + loss_and_limitations: tuple[str, ...] + declassification_basis_ref: str | None = None + redaction_policy_ref: str | None = None + transformation_rule_ref: str | None = None + + +class ParticipantExposureApparatusResolverV2(Protocol): + """Resolve the selected apparatus at an exact state cut.""" + + def __call__( + self, + *, + implementation_selection_ref: str, + exposure_policy_ref: str, + decision_cut_ref: str, + ) -> ParticipantImplementationSelectionModel | None: ... + + +class ParticipantExposureProjectionPolicyResolverV2(Protocol): + """Resolve the projection-policy decision at an exact state cut.""" + + def __call__( + self, + *, + projection_policy_ref: str, + participant_address: str, + audience_scope_ref: str, + decision_cut_ref: str, + ) -> ParticipantExposurePolicyDecisionV2 | None: ... + + +class ParticipantExposureAuthorizationResolverV2(Protocol): + """Resolve an exact-cut item authorization by stable record reference.""" + + def __call__( + self, + *, + authorization_record_ref: str, + item_ref: str, + decision_cut_ref: str, + ) -> ParticipantExposureAuthorizationRecordV2 | None: ... + + +@dataclass(frozen=True) +class ParticipantExposureResolversV2: + apparatus: ParticipantExposureApparatusResolverV2 + projection_policy: ParticipantExposureProjectionPolicyResolverV2 + authorization: ParticipantExposureAuthorizationResolverV2 + + +class ParticipantExposureProjectionV2(Protocol): + """Exact-cut coordinates consumed by the v2 exposure selector.""" + + participant_address: str + episode_id: str + decision_epoch: int + decision_cut_ref: str + implementation_selection_ref: str + decision_control_mode: str + audience_scope_ref: str + projection_policy_ref: str + projection_policy_revision: str + projection_policy_decision_ref: str + exposure_policy_ref: str + visible_context_refs: tuple[str, ...] + exposure_assessments: Mapping[str, ParticipantExposureAssessment] + evidence_refs: tuple[str, ...] + provenance_refs: tuple[str, ...] + marking_definition_refs: tuple[str, ...] + redaction_policy_ref: str | None diff --git a/implementations/python/packages/raes_processor/models/participant_exposure_v2.py b/implementations/python/packages/raes_processor/models/participant_exposure_v2.py new file mode 100644 index 000000000..afc52d588 --- /dev/null +++ b/implementations/python/packages/raes_processor/models/participant_exposure_v2.py @@ -0,0 +1,278 @@ +"""Deny-first, exact-state-cut SEM-226 participant exposure selection.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from raes_contracts.contracts import ParticipantExposurePolicyModel + +from .behavior_resources import _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS +from .participant_exposure_authority_v2 import ( + ParticipantExposureAuthorizationRecordV2, + ParticipantExposurePolicyDecisionV2, + ParticipantExposureProjectionV2, + ParticipantExposureResolversV2, +) +from .participant_exposure_policy import _policy_permits_item + + +def _serialized_surface_refs( + projection: ParticipantExposureProjectionV2, + entries: list[dict[str, object]], + surface_affordances: list[str], +) -> set[str]: + return { + *projection.visible_context_refs, + *(str(entry["action_contract_address"]) for entry in entries), + *surface_affordances, + } + + +def _resolve_policy_decision( + projection: ParticipantExposureProjectionV2, + resolvers: ParticipantExposureResolversV2, +) -> ParticipantExposurePolicyDecisionV2: + try: + decision = resolvers.projection_policy( + projection_policy_ref=projection.projection_policy_ref, + participant_address=projection.participant_address, + audience_scope_ref=projection.audience_scope_ref, + decision_cut_ref=projection.decision_cut_ref, + ) + except Exception as exc: + raise ValueError("participant exposure exact-cut projection-policy resolution failed") from exc + if decision is None: + raise ValueError("participant exposure requires an authoritative policy decision at the exact state cut") + expected = { + "policy_ref": projection.projection_policy_ref, + "revision": projection.projection_policy_revision, + "decision_ref": projection.projection_policy_decision_ref, + "decision_cut_ref": projection.decision_cut_ref, + } + mismatches = sorted(name for name, value in expected.items() if getattr(decision, name) != value) + if mismatches: + raise ValueError("projection policy decision has mismatched exact-cut coordinates: " + ", ".join(mismatches)) + for field_name in ("evidence_refs", "provenance_refs", "limitations"): + values = getattr(decision, field_name) + if not values or len(values) != len(set(values)) or any(not value for value in values): + raise ValueError(f"projection policy decision requires unique non-empty {field_name}") + if not set(decision.evidence_refs).issubset(projection.evidence_refs): + raise ValueError("projection policy decision evidence must be carried by assurance") + if not set(decision.provenance_refs).issubset(projection.provenance_refs): + raise ValueError("projection policy decision provenance must be carried by assurance") + return decision + + +def _selected_exposure_policy( + projection: ParticipantExposureProjectionV2, + resolvers: ParticipantExposureResolversV2, +) -> ParticipantExposurePolicyModel: + try: + selection = resolvers.apparatus( + implementation_selection_ref=projection.implementation_selection_ref, + exposure_policy_ref=projection.exposure_policy_ref, + decision_cut_ref=projection.decision_cut_ref, + ) + except Exception as exc: + raise ValueError("participant exposure exact-cut apparatus resolution failed") from exc + if selection is None: + raise ValueError("participant exposure apparatus refs did not resolve at the exact state cut") + if selection.participant_address != projection.participant_address: + raise ValueError("implementation selection participant_address must match the exposure projection") + if selection.selected_decision_surface_mode != projection.decision_control_mode: + raise ValueError("implementation selection decision-surface mode must match the exposure projection") + policy = selection.exposure_policy + if policy.policy_id != projection.exposure_policy_ref: + raise ValueError("selected exposure policy identity must match exposure_policy_ref") + if policy.policy_version is None or policy.policy_digest is None: + raise ValueError("selected exposure policy requires an explicit version and digest") + return policy + + +def _resolve_authorization( + projection: ParticipantExposureProjectionV2, + *, + item_ref: str, + authorization_record_ref: str, + resolvers: ParticipantExposureResolversV2, +) -> ParticipantExposureAuthorizationRecordV2: + try: + authorization = resolvers.authorization( + authorization_record_ref=authorization_record_ref, + item_ref=item_ref, + decision_cut_ref=projection.decision_cut_ref, + ) + except Exception as exc: + raise ValueError("participant exposure exact-cut authorization resolution failed") from exc + if authorization is None: + raise ValueError(f"exposure item {item_ref!r} has no authorization at the exact state cut") + if authorization.authorization_record_ref != authorization_record_ref or authorization.item_ref != item_ref: + raise ValueError("exposure authorization resolver returned a different record or item") + return authorization + + +def _validate_authorization( + authorization: ParticipantExposureAuthorizationRecordV2, + projection: ParticipantExposureProjectionV2, + policy: ParticipantExposurePolicyModel, + relation: Mapping[str, str], +) -> None: + exact_coordinates = { + "participant_address": projection.participant_address, + "episode_id": projection.episode_id, + "audience_scope_ref": projection.audience_scope_ref, + "decision_epoch": projection.decision_epoch, + "decision_cut_ref": projection.decision_cut_ref, + "implementation_selection_ref": projection.implementation_selection_ref, + "projection_policy_ref": projection.projection_policy_ref, + "projection_policy_revision": projection.projection_policy_revision, + "projection_policy_decision_ref": projection.projection_policy_decision_ref, + "exposure_policy_ref": projection.exposure_policy_ref, + "exposure_policy_version": policy.policy_version, + "exposure_policy_digest": policy.policy_digest, + } + mismatches = sorted( + name for name, expected in exact_coordinates.items() if getattr(authorization, name) != expected + ) + if mismatches: + raise ValueError( + f"exposure authorization {authorization.item_ref!r} has mismatched exact-cut coordinates: " + + ", ".join(mismatches) + ) + if relation.get(authorization.item_ref) not in _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS: + raise ValueError(f"exposure item {authorization.item_ref!r} is not participant-visible at the state cut") + if not _policy_permits_item(policy, authorization.item_ref): + raise ValueError(f"selected exposure policy does not permit item {authorization.item_ref!r}") + required_strings = ( + authorization.source_ref, + authorization.source_layer_ref, + authorization.visibility_basis_ref, + authorization.operation_basis_ref, + authorization.actor_ref, + authorization.controller_ref, + authorization.authority_basis_ref, + authorization.backend_support_ref, + ) + if any(not value for value in required_strings): + raise ValueError("participant exposure authorization requires non-empty authority refs") + for field_name in ( + "source_marking_definition_refs", + "result_marking_definition_refs", + "source_provenance_refs", + "result_provenance_refs", + "evidence_refs", + "provenance_refs", + "loss_and_limitations", + ): + values = getattr(authorization, field_name) + if field_name in {"evidence_refs", "provenance_refs", "loss_and_limitations"} and not values: + raise ValueError(f"exposure authorization {authorization.item_ref!r} requires {field_name}") + if len(values) != len(set(values)) or any(not value for value in values): + raise ValueError(f"exposure authorization {authorization.item_ref!r} has invalid {field_name}") + if authorization.source_ref != authorization.item_ref and authorization.transformation_rule_ref is None: + raise ValueError("derived exposure items require a transformation rule") + if authorization.operation in {"masking", "redaction", "transformation"} and ( + authorization.transformation_rule_ref is None + ): + raise ValueError(f"{authorization.operation} exposure operations require a transformation rule") + if authorization.operation == "redaction" and authorization.redaction_policy_ref is None: + raise ValueError("redaction exposure operations require a redaction policy") + if authorization.operation == "declassification" and authorization.declassification_basis_ref is None: + raise ValueError("declassification exposure operations require a declassification basis") + if authorization.declassification_basis_ref is None and not set( + authorization.source_marking_definition_refs + ).issubset(authorization.result_marking_definition_refs): + raise ValueError("derived exposure results must inherit source markings unless declassification is explicit") + if authorization.declassification_basis_ref is None and not set(authorization.source_provenance_refs).issubset( + authorization.result_provenance_refs + ): + raise ValueError("derived exposure results must inherit source provenance unless declassification is explicit") + if not {*authorization.source_provenance_refs, *authorization.result_provenance_refs}.issubset( + authorization.provenance_refs + ): + raise ValueError("source and result exposure provenance must be carried by provenance_refs") + carried = ( + ("evidence", authorization.evidence_refs, projection.evidence_refs), + ("provenance", authorization.provenance_refs, projection.provenance_refs), + ("result markings", authorization.result_marking_definition_refs, projection.marking_definition_refs), + ) + for label, refs, carrier in carried: + if not set(refs).issubset(carrier): + raise ValueError(f"exposure authorization {authorization.item_ref!r} {label} must be carried by assurance") + if ( + authorization.redaction_policy_ref is not None + and authorization.redaction_policy_ref != projection.redaction_policy_ref + ): + raise ValueError("exposure authorization redaction policy must match the participant view") + + +def _binding_payload( + authorization: ParticipantExposureAuthorizationRecordV2, + projection: ParticipantExposureProjectionV2, + policy: ParticipantExposurePolicyModel, +) -> dict[str, object]: + return { + "item_ref": authorization.item_ref, + "authorization_record_ref": authorization.authorization_record_ref, + "source_ref": authorization.source_ref, + "source_layer_ref": authorization.source_layer_ref, + "participant_address": projection.participant_address, + "episode_id": projection.episode_id, + "audience_scope_ref": authorization.audience_scope_ref, + "decision_epoch": projection.decision_epoch, + "decision_cut_ref": projection.decision_cut_ref, + "visibility_basis_ref": authorization.visibility_basis_ref, + "projection_policy_ref": projection.projection_policy_ref, + "projection_policy_revision": projection.projection_policy_revision, + "projection_policy_decision_ref": projection.projection_policy_decision_ref, + "exposure_policy_ref": projection.exposure_policy_ref, + "exposure_policy_version": policy.policy_version, + "exposure_policy_digest": policy.policy_digest, + "operation": authorization.operation, + "operation_basis_ref": authorization.operation_basis_ref, + "actor_ref": authorization.actor_ref, + "controller_ref": authorization.controller_ref, + "authority_basis_ref": authorization.authority_basis_ref, + "source_marking_definition_refs": list(authorization.source_marking_definition_refs), + "result_marking_definition_refs": list(authorization.result_marking_definition_refs), + "source_provenance_refs": list(authorization.source_provenance_refs), + "result_provenance_refs": list(authorization.result_provenance_refs), + "declassification_basis_ref": authorization.declassification_basis_ref, + "redaction_policy_ref": authorization.redaction_policy_ref, + "transformation_rule_ref": authorization.transformation_rule_ref, + "evidence_refs": list(authorization.evidence_refs), + "provenance_refs": list(authorization.provenance_refs), + "loss_and_limitations": list(authorization.loss_and_limitations), + } + + +def project_participant_exposure_bindings_v2( + relation: Mapping[str, str], + projection: ParticipantExposureProjectionV2, + entries: list[dict[str, object]], + surface_affordances: list[str], + resolvers: ParticipantExposureResolversV2, +) -> tuple[list[dict[str, object]], ParticipantExposurePolicyDecisionV2]: + """Resolve every visible item against policy and authority at one exact cut.""" + + policy_decision = _resolve_policy_decision(projection, resolvers) + policy = _selected_exposure_policy(projection, resolvers) + expected = _serialized_surface_refs(projection, entries, surface_affordances) + if set(projection.exposure_assessments) != expected: + raise ValueError("exposure_assessments must exactly cover every serialized participant-view ref") + bindings = [] + for item_ref in sorted(expected): + assessment = projection.exposure_assessments[item_ref] + if assessment.item_ref != item_ref: + raise ValueError(f"exposure assessment key {item_ref!r} must match its item_ref") + if assessment.realization is not None: + raise ValueError("v2 item exposure authorization must not be used as a surface delivery occurrence") + authorization = _resolve_authorization( + projection, + item_ref=item_ref, + authorization_record_ref=assessment.authorization_record_ref, + resolvers=resolvers, + ) + _validate_authorization(authorization, projection, policy, relation) + bindings.append(_binding_payload(authorization, projection, policy)) + return bindings, policy_decision diff --git a/implementations/python/packages/raes_runtime/participant_control.py b/implementations/python/packages/raes_runtime/participant_control.py index 4f6337007..633cdb895 100644 --- a/implementations/python/packages/raes_runtime/participant_control.py +++ b/implementations/python/packages/raes_runtime/participant_control.py @@ -2,7 +2,10 @@ from __future__ import annotations -from raes_contracts.contracts import ParticipantDecisionSurfaceModel, ParticipantDecisionSurfaceSelectionModel +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceModel, + ParticipantDecisionSurfaceSelectionModel, +) from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_binding import ( ParticipantActionAdmissionRequest, @@ -18,12 +21,14 @@ ) from raes_contracts.planning import RuntimeDomain from raes_contracts.runtime_state import OperationReceipt -from raes_processor.models import ( - ParticipantBehaviorRuntime, - validate_participant_decision_surface_projection_anchor, -) +from raes_processor.models import ParticipantBehaviorRuntime from .control_plane_execution import execute_participant_action +from .participant_control_diagnostics import ( + _NO_PARTICIPANT_RUNTIME_MESSAGE, + _participant_binding_address, + _participant_binding_diagnostic, +) from .participant_control_intents import ( ParticipantApprovalControlIntent, ParticipantCancellationControlIntent, @@ -36,23 +41,7 @@ ParticipantProposalControlIntent, ) from .participant_control_mediation import record_participant_control - -_NO_PARTICIPANT_RUNTIME_MESSAGE = "Target does not provide a participant runtime." -_PARTICIPANT_BINDING_REJECTED = "runtime.participant-binding.rejected" - - -def _participant_binding_address(participant_behavior: object) -> str: - address = getattr(participant_behavior, "address", None) - return address if isinstance(address, str) and address else "runtime.control-plane.participant-binding" - - -def _participant_binding_diagnostic(address: str, message: str) -> Diagnostic: - return Diagnostic( - code=_PARTICIPANT_BINDING_REJECTED, - domain="runtime", - address=address, - message=message, - ) +from .participant_decision_surface_control_v2 import ParticipantDecisionSurfaceV2ControlMixin def _participant_binding_diagnostics( @@ -217,7 +206,7 @@ def _participant_binding_request_diagnostics( return diagnostics -class ParticipantControlMixin: +class ParticipantControlMixin(ParticipantDecisionSurfaceV2ControlMixin): """Participant runtime methods for the shared runtime control plane.""" def record_participant_control( @@ -415,9 +404,6 @@ def admit_participant_decision_surface_selection( request_fingerprint=request_fingerprint, ) try: - if surface.projection_anchor is None: - raise ValueError("participant decision surface admission requires a current projection_anchor") - validate_participant_decision_surface_projection_anchor(self._snapshot, surface) request = bind_participant_decision_surface_selection( surface=surface, selection=selection, diff --git a/implementations/python/packages/raes_runtime/participant_control_diagnostics.py b/implementations/python/packages/raes_runtime/participant_control_diagnostics.py new file mode 100644 index 000000000..c0593c388 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_control_diagnostics.py @@ -0,0 +1,22 @@ +"""Shared participant control-plane diagnostic construction.""" + +from __future__ import annotations + +from raes_contracts.diagnostics import Diagnostic + +_NO_PARTICIPANT_RUNTIME_MESSAGE = "Target does not provide a participant runtime." +_PARTICIPANT_BINDING_REJECTED = "runtime.participant-binding.rejected" + + +def _participant_binding_address(participant_behavior: object) -> str: + address = getattr(participant_behavior, "address", None) + return address if isinstance(address, str) and address else "runtime.control-plane.participant-binding" + + +def _participant_binding_diagnostic(address: str, message: str) -> Diagnostic: + return Diagnostic( + code=_PARTICIPANT_BINDING_REJECTED, + domain="runtime", + address=address, + message=message, + ) diff --git a/implementations/python/packages/raes_runtime/participant_decision_surface_control_v2.py b/implementations/python/packages/raes_runtime/participant_decision_surface_control_v2.py new file mode 100644 index 000000000..ab7efb3de --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_decision_surface_control_v2.py @@ -0,0 +1,78 @@ +"""Runtime admission for exact-cut participant decision surfaces.""" + +from __future__ import annotations + +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceSelectionV2Model, + ParticipantDecisionSurfaceV2Model, +) +from raes_contracts.participant_binding import ParticipantActionAdmissionRequest +from raes_contracts.participant_binding_v2 import ( + ParticipantDecisionSurfaceBindingResolversV2, + bind_participant_decision_surface_selection_v2, +) +from raes_contracts.planning import RuntimeDomain +from raes_contracts.runtime_state import OperationReceipt +from raes_processor.models import ( + ParticipantBehaviorRuntime, + validate_participant_decision_surface_v2_anchor, +) + +from .participant_control_diagnostics import ( + _NO_PARTICIPANT_RUNTIME_MESSAGE, + _participant_binding_address, + _participant_binding_diagnostic, +) + + +class ParticipantDecisionSurfaceV2ControlMixin: + """Exact-cut decision-surface operations mixed into the control plane.""" + + def admit_participant_decision_surface_selection_v2( + self, + participant_behavior: ParticipantBehaviorRuntime, + *, + surface: ParticipantDecisionSurfaceV2Model, + selection: ParticipantDecisionSurfaceSelectionV2Model, + admission_request: ParticipantActionAdmissionRequest, + resolvers: ParticipantDecisionSurfaceBindingResolversV2, + idempotency_key: str = "", + request_fingerprint: str = "", + ) -> OperationReceipt: + """Re-resolve v2 state and delivery before ordinary action admission.""" + + if self._target.participant_runtime is None: + return self._reject_submission( + domain=RuntimeDomain.PARTICIPANT, + message=_NO_PARTICIPANT_RUNTIME_MESSAGE, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) + try: + validate_participant_decision_surface_v2_anchor(self._snapshot, surface) + request = bind_participant_decision_surface_selection_v2( + surface=surface, + selection=selection, + admission_request=admission_request, + argument_shape_resolver=resolvers.argument_shape, + apparatus_resolver=resolvers.apparatus, + delivery_resolver=resolvers.delivery, + ) + except (TypeError, ValueError) as exc: + return self._reject_diagnostics( + domain=RuntimeDomain.PARTICIPANT, + diagnostics=[ + _participant_binding_diagnostic(_participant_binding_address(participant_behavior), str(exc)) + ], + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) + return self.admit_participant_action( + participant_behavior, + request, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) + + +__all__ = ("ParticipantDecisionSurfaceV2ControlMixin",) diff --git a/implementations/python/tests/sem230_information_flow_model.py b/implementations/python/tests/sem230_information_flow_model.py index ec4ff5cf3..3024bec49 100644 --- a/implementations/python/tests/sem230_information_flow_model.py +++ b/implementations/python/tests/sem230_information_flow_model.py @@ -6,6 +6,7 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from enum import Enum @@ -45,11 +46,17 @@ class Decision(str, Enum): WITHHELD = "withheld" +class ParticipantMemoryScope(str, Enum): + EPISODE_LOCAL_RESET = "episode_local_reset" + PERSISTENT_ACROSS_EPISODES = "persistent_across_episodes" + + @dataclass(frozen=True) -class PolicyRevision: +class ProjectionPolicyDecision: policy_id: str revision: str - effective_order: int + decision_ref: str + decision_cut_ref: str visible_low_refs: frozenset[str] permitted_declassifications: frozenset[str] @@ -64,6 +71,8 @@ class Crossing: source_ref: str value: str policy_revision: str + policy_decision_ref: str + decision_cut_ref: str authorized: bool admitted: bool visible: bool @@ -73,22 +82,30 @@ class Crossing: transformation_valid: bool -def _effective_policy( - order: int, - policies: tuple[PolicyRevision, ...], -) -> PolicyRevision | None: - eligible = [policy for policy in policies if policy.effective_order <= order] - return max(eligible, key=lambda policy: policy.effective_order, default=None) +def _exact_policy_decision( + crossing: Crossing, + policy_decisions: tuple[ProjectionPolicyDecision, ...], +) -> ProjectionPolicyDecision | None: + matches = [ + decision + for decision in policy_decisions + if decision.decision_cut_ref == crossing.decision_cut_ref + and decision.decision_ref == crossing.policy_decision_ref + and decision.revision == crossing.policy_revision + ] + if len(matches) > 1: + raise ValueError("policy authority returned multiple decisions for one exact state cut") + return matches[0] if matches else None def decide_crossing( crossing: Crossing, - policies: tuple[PolicyRevision, ...], + policy_decisions: tuple[ProjectionPolicyDecision, ...], ) -> Decision: """Apply the bounded model's independent, deny-first crossing gates.""" - policy = _effective_policy(crossing.order, policies) - if policy is None or crossing.policy_revision != policy.revision: + policy = _exact_policy_decision(crossing, policy_decisions) + if policy is None: return Decision.WITHHELD if crossing.kind in {CrossingKind.CONCEALMENT, CrossingKind.REVOCATION}: return Decision.WITHHELD @@ -111,7 +128,7 @@ def decide_crossing( def project_history( crossings: tuple[Crossing, ...], - policies: tuple[PolicyRevision, ...], + policy_decisions: tuple[ProjectionPolicyDecision, ...], *, participant: str, audience: str, @@ -122,7 +139,7 @@ def project_history( for crossing in sorted(crossings, key=lambda candidate: candidate.order): if crossing.participant != participant or crossing.audience != audience: continue - if decide_crossing(crossing, policies) is Decision.DISCLOSED: + if decide_crossing(crossing, policy_decisions) is Decision.DISCLOSED: visible.append((crossing.order, crossing.source_ref, crossing.value)) return tuple(visible) @@ -131,7 +148,7 @@ def policy_noninterference_holds( *, left_runs: tuple[tuple[Crossing, ...], ...], right_runs: tuple[tuple[Crossing, ...], ...], - policies: tuple[PolicyRevision, ...], + policy_decisions: tuple[ProjectionPolicyDecision, ...], participant: str, audience: str, ) -> bool: @@ -145,7 +162,7 @@ def policy_noninterference_holds( left_support = { project_history( run, - policies, + policy_decisions, participant=participant, audience=audience, ) @@ -154,10 +171,71 @@ def policy_noninterference_holds( right_support = { project_history( run, - policies, + policy_decisions, participant=participant, audience=audience, ) for run in right_runs } return left_support == right_support + + +ProjectedHistory = tuple[tuple[int, str, str], ...] +ParticipantStrategy = Callable[[ProjectedHistory], str] + + +def participant_information_state( + current_episode_history: ProjectedHistory, + *, + prior_delivered_history: ProjectedHistory, + memory_scope: ParticipantMemoryScope, + memory_reset_authority_ref: str | None, +) -> ProjectedHistory: + """Apply the declared memory scope without equating reset with forgetting.""" + + if memory_scope is ParticipantMemoryScope.EPISODE_LOCAL_RESET: + if memory_reset_authority_ref is None: + raise ValueError("episode_local_reset requires authoritative reset of every visible memory channel") + return current_episode_history + if memory_reset_authority_ref is not None: + raise ValueError("persistent memory scope must not claim a reset authority") + return (*prior_delivered_history, *current_episode_history) + + +def reactive_policy_noninterference_holds( + *, + left_runs: tuple[tuple[Crossing, ...], ...], + right_runs: tuple[tuple[Crossing, ...], ...], + policy_decisions: tuple[ProjectionPolicyDecision, ...], + participant: str, + audience: str, + strategies: tuple[ParticipantStrategy, ...], + prior_delivered_history: ProjectedHistory = (), + memory_scope: ParticipantMemoryScope, + memory_reset_authority_ref: str | None, +) -> bool: + """Compare finite projected-history/choice supports for adaptive strategies. + + This quantifies only over the supplied finite run and strategy classes. It + is bounded falsification evidence, not a universal proof. + """ + + def support(runs: tuple[tuple[Crossing, ...], ...], strategy: ParticipantStrategy): + outcomes: set[tuple[ProjectedHistory, str]] = set() + for run in runs: + projected = project_history( + run, + policy_decisions, + participant=participant, + audience=audience, + ) + information_state = participant_information_state( + projected, + prior_delivered_history=prior_delivered_history, + memory_scope=memory_scope, + memory_reset_authority_ref=memory_reset_authority_ref, + ) + outcomes.add((information_state, strategy(information_state))) + return outcomes + + return all(support(left_runs, strategy) == support(right_runs, strategy) for strategy in strategies) diff --git a/implementations/python/tests/test_backend_conformance_cli.py b/implementations/python/tests/test_backend_conformance_cli.py index ac87d5d91..edbb2a2d3 100644 --- a/implementations/python/tests/test_backend_conformance_cli.py +++ b/implementations/python/tests/test_backend_conformance_cli.py @@ -50,7 +50,7 @@ def test_backend_conformance_cli_passes_for_provisioning_only_profile(): assert payload["passed"] is True claim = payload["claim"] assert claim["taxonomy_id"] == "aces-behavioral-relations" - assert claim["taxonomy_revision"] == "rev2" + assert claim["taxonomy_revision"] == "rev3" assert claim["relation_id"] == "bounded-probe-success" assert claim["quantifier_scope"] == "finite-cases" assert claim["evidence_scope"] == "finite" diff --git a/implementations/python/tests/test_behavioral_relation_claims.py b/implementations/python/tests/test_behavioral_relation_claims.py index 05386a090..6876ab97e 100644 --- a/implementations/python/tests/test_behavioral_relation_claims.py +++ b/implementations/python/tests/test_behavioral_relation_claims.py @@ -14,7 +14,7 @@ def _valid_binding() -> dict[str, object]: return { "taxonomy_id": "aces-behavioral-relations", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "relation_id": "bounded-probe-success", "subject": "Named backend fixture cases", "left_carrier_ref": "backend-target:stub", diff --git a/implementations/python/tests/test_behavioral_relations.py b/implementations/python/tests/test_behavioral_relations.py index 7bf229f58..bbe424330 100644 --- a/implementations/python/tests/test_behavioral_relations.py +++ b/implementations/python/tests/test_behavioral_relations.py @@ -43,6 +43,7 @@ "weak-bisimulation", "participant-projected-history-equivalence", "policy-noninterference", + "io-alternating-refinement", "epistemic-indistinguishability", "alternating-strategic-equivalence", "probabilistic-bisimulation", @@ -55,7 +56,7 @@ def _bounded_empirical_claim() -> dict[str, object]: return { "taxonomy_id": "aces-behavioral-relations", - "taxonomy_revision": "rev2", + "taxonomy_revision": "rev3", "relation_id": "empirical-adequacy", "subject": "TechVault baseline study", "left_carrier_ref": "study-techvault-baseline@1.0.0", @@ -86,6 +87,10 @@ def _trace_exists(system, actions: tuple[str, ...]) -> bool: return bool(states) +def _enabled_actions(system, state: str) -> set[str]: + return {action for action, _ in _transition_index(system)[state]} + + def _strongly_bisimilar(left, right) -> bool: left_index = _transition_index(left) right_index = _transition_index(right) @@ -177,8 +182,11 @@ def test_catalog_bibliography_claim_surfaces_and_relation_references_resolve(): "alur-henzinger-kupferman-2002", "goguen-meseguer-1982", "sabelfeld-sands-2009", + "lynch-tuttle-1989", + "clarkson-schneider-2010", + "bohannon-pierce-sjoberg-weirich-zdancewic-2009", } - assert all(source.immutable_locator.kind in {"doi", "isbn"} for source in catalog.bibliography) + assert all(source.immutable_locator.kind in {"doi", "isbn", "report"} for source in catalog.bibliography) assert all(set(relation.source_refs) <= source_ids for relation in catalog.relations.values()) assert {surface.surface_id for surface in catalog.claim_surfaces} == { "sdl-transformation", @@ -217,6 +225,55 @@ def test_hidden_action_example_distinguishes_strong_from_weak_matching(): assert example.expected_weak_matching is True +def test_decision_epoch_zero_hidden_projection_step_can_weakly_match_without_strong_bisimulation(): + abstract = ExampleTransitionSystemModel( + states=["a-ready", "a-delivered"], + initial_state="a-ready", + transitions=[ + ExampleTransitionModel( + source="a-ready", + action="deliver:decision-epoch-0", + target="a-delivered", + ) + ], + ) + concrete = ExampleTransitionSystemModel( + states=["c-ready", "c-projected", "c-delivered"], + initial_state="c-ready", + transitions=[ + ExampleTransitionModel(source="c-ready", action="tau:projection", target="c-projected"), + ExampleTransitionModel( + source="c-projected", + action="deliver:decision-epoch-0", + target="c-delivered", + ), + ], + ) + + assert not _strongly_bisimilar(abstract, concrete) + assert _weak_trace_exists(abstract, ("deliver:decision-epoch-0",), "tau:projection") + assert _weak_trace_exists(concrete, ("deliver:decision-epoch-0",), "tau:projection") + + +def test_projected_trace_inclusion_does_not_establish_required_participant_input_availability(): + abstract = ExampleTransitionSystemModel( + states=["a0", "a1"], + initial_state="a0", + transitions=[ExampleTransitionModel(source="a0", action="select:scan", target="a1")], + ) + refusing_backend = ExampleTransitionSystemModel( + states=["c0"], + initial_state="c0", + transitions=[ExampleTransitionModel(source="c0", action="tau:idle", target="c0")], + ) + observed_concrete_traces = {()} + observed_abstract_traces = {()} + + assert observed_concrete_traces <= observed_abstract_traces + assert "select:scan" in _enabled_actions(abstract, abstract.initial_state) + assert "select:scan" not in _enabled_actions(refusing_backend, refusing_backend.initial_state) + + @settings(max_examples=40) @given( visible=st.text(alphabet="abcxyz", min_size=1, max_size=5), @@ -279,7 +336,7 @@ def test_claim_binding_rejects_bounded_evidence_promoted_to_universal_claim(): with pytest.raises(ValidationError, match="universal quantification requires model-check or proof evidence"): BehavioralClaimBindingModel( taxonomy_id="aces-behavioral-relations", - taxonomy_revision="rev2", + taxonomy_revision="rev3", relation_id="trace-equivalence", subject="two finite backend runs", left_carrier_ref="backend-run:left", diff --git a/implementations/python/tests/test_participant_implementation_manifest.py b/implementations/python/tests/test_participant_implementation_manifest.py index e3f1da161..03b02d9e6 100644 --- a/implementations/python/tests/test_participant_implementation_manifest.py +++ b/implementations/python/tests/test_participant_implementation_manifest.py @@ -42,6 +42,7 @@ def _manifest_payload() -> dict[str, object]: "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2", ], "compatibility": { "participant_runtimes": ["stub-participant-runtime"], @@ -61,6 +62,7 @@ def _manifest_payload() -> dict[str, object]: "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2", ], "supported_decision_surface_modes": ["autonomous", "policy-directed"], "tool_affordance_expectations": ["shell", "http-api"], @@ -117,6 +119,7 @@ def _provenance_payload() -> dict[str, object]: "participant_contract_versions": [ "participant-episode-state-envelope-v1", "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2", ], "exposure_policy": { "policy_id": "red-agent-policy", diff --git a/implementations/python/tests/test_sem_220_participant_decision_surface.py b/implementations/python/tests/test_sem_220_participant_decision_surface.py index 264f523a7..da9b29d6e 100644 --- a/implementations/python/tests/test_sem_220_participant_decision_surface.py +++ b/implementations/python/tests/test_sem_220_participant_decision_surface.py @@ -10,11 +10,8 @@ import pytest from jsonschema import Draft202012Validator from pydantic import ValidationError -from raes_backend_stubs.stubs import create_stub_target from raes_contracts.contracts import ( ParticipantContextViewModel, - ParticipantDecisionSurfaceBehaviorAnchorModel, - ParticipantDecisionSurfaceEpisodeReadinessAnchorModel, ParticipantDecisionSurfaceModel, ParticipantDecisionSurfaceSelectionModel, ParticipantImplementationManifestModel, @@ -28,12 +25,10 @@ ParticipantValidatedActionSelection, bind_participant_decision_surface_selection, ) -from raes_contracts.runtime_state import RuntimeSnapshot from raes_processor.models import ( ParticipantActionContractRuntime, ParticipantBehaviorHistoryEvent, ParticipantBehaviorHistoryEventType, - ParticipantBehaviorRuntime, ParticipantBehaviorSpecificationRuntime, ParticipantDecisionSurfaceActionAssessment, ParticipantDecisionSurfaceProjectionInput, @@ -46,10 +41,7 @@ ParticipantToolAffordanceRuntime, RuntimeModel, project_participant_decision_surface, - resolve_participant_behavior_projection_anchor, - resolve_participant_episode_readiness_anchor, ) -from raes_runtime.control_plane import RuntimeControlPlane from raes_runtime.participant_control import ParticipantControlMixin REPO_ROOT = Path(__file__).resolve().parents[3] @@ -623,68 +615,13 @@ def test_decision_surface_schema_is_closed_discriminated_and_published() -> None schema = schema_bundle()["participant-decision-surface-v1"] assert schema["additionalProperties"] is False assert schema["properties"]["form"]["discriminator"]["propertyName"] == "surface_form" - anchor_schema = schema["properties"]["projection_anchor"]["anyOf"][0] - assert anchor_schema["discriminator"]["propertyName"] == "anchor_kind" - assert set(anchor_schema["discriminator"]["mapping"]) == { - "behavior_event", - "episode_readiness", - } assert {entry["id"] for entry in schema["x-aces-invariants"]} >= { "decision-surface-entry-reference-agreement", "decision-surface-presentation-not-lifecycle-evidence", - "decision-surface-projection-anchor-agreement", "decision-surface-sem226-item-exposure-agreement", } -def _surface_payload_with_readiness_anchor() -> dict[str, object]: - payload = _surface_payload() - event_ref = "participant-episode-event:sha256:" + "4" * 64 - payload["provenance_refs"].append(event_ref) # type: ignore[union-attr] - payload["projection_anchor"] = { - "anchor_kind": "episode_readiness", - "participant_address": PARTICIPANT, - "episode_id": EPISODE, - "decision_surface_order": 0, - "event_ref": event_ref, - "anchor_order": 1, - "event_type": "episode_running", - "episode_sequence_number": 0, - "evidence_refs": ["evidence.surface.red.order-0"], - "provenance_refs": [event_ref], - } - return payload - - -@pytest.mark.parametrize( - ("anchor_updates", "message"), - ( - ({"participant_address": "participant.behavior.blue-agent"}, "disagrees with the decision surface"), - ({"episode_id": "episode-other"}, "disagrees with the decision surface"), - ({"decision_surface_order": 1}, "disagrees with the decision surface"), - ({"evidence_refs": ["evidence.anchor.other"]}, "evidence_refs must be carried"), - ( - { - "provenance_refs": [ - "participant-episode-event:sha256:" + "4" * 64, - "provenance.anchor.other", - ] - }, - "provenance_refs must be carried", - ), - ), -) -def test_projection_anchor_must_agree_with_surface_scope_and_refs( - anchor_updates: dict[str, object], - message: str, -) -> None: - payload = _surface_payload_with_readiness_anchor() - payload["projection_anchor"].update(anchor_updates) # type: ignore[union-attr] - - with pytest.raises(ValidationError, match=message): - ParticipantDecisionSurfaceModel.model_validate(payload) - - def test_decision_surface_valid_and_invalid_fixtures_match_model_and_schema() -> None: validator = Draft202012Validator(schema_bundle()["participant-decision-surface-v1"]) valid_paths = sorted((FIXTURE_ROOT / "valid").glob("*.json")) @@ -695,8 +632,6 @@ def test_decision_surface_valid_and_invalid_fixtures_match_model_and_schema() -> payload = json.loads(path.read_text(encoding="utf-8")) validator.validate(payload) ParticipantDecisionSurfaceModel.model_validate(payload) - if path.stem == "human-candidate": - assert payload["projection_anchor"]["anchor_kind"] == "behavior_event" for path in invalid_paths: payload = json.loads(path.read_text(encoding="utf-8")) assert list(validator.iter_errors(payload)), path @@ -1007,8 +942,6 @@ def test_realization_kind_preserves_surface_semantic_refs( assert surface.action_entries[0].action_contract_address == SCAN assert surface.action_entries[0].selection_shape_ref == SCAN_SHAPE assert surface.form.selection_meaning_ref == "selection-meaning.candidate.v1" - assert surface.decision_control_mode == decision_control_mode - assert surface.implementation_selection_ref == implementation_selection_ref @pytest.mark.parametrize( @@ -1078,9 +1011,8 @@ def test_context_envelope_and_payload_must_agree(field_name: str, mismatched_val class _RecordingControl(ParticipantControlMixin): - def __init__(self, snapshot: RuntimeSnapshot | None = None) -> None: + def __init__(self) -> None: self._target = SimpleNamespace(participant_runtime=object()) - self._snapshot = snapshot or RuntimeSnapshot() self.admitted: ParticipantActionAdmissionRequest | None = None def _reject_diagnostics(self, **kwargs: object) -> str: @@ -1097,10 +1029,19 @@ def admit_participant_action( def test_open_ended_proposal_validates_before_existing_admission_path() -> None: - snapshot, surface = _anchored_surface(surface_form="open_ended_generation") - selection = _surface_selection(surface) + payload = _surface_payload(surface_form="open_ended_generation") + payload["action_entries"][0]["eligibility"] = "eligible" # type: ignore[index] + payload["action_entries"][0]["eligibility_reason_refs"] = [] # type: ignore[index] + surface = ParticipantDecisionSurfaceModel.model_validate(payload) + selection = ParticipantDecisionSurfaceSelectionModel( + surface_id=surface.surface_id, + observation_order=surface.observation_order, + action_contract_address=SCAN, + argument_shape_ref="selection-shapes.scan.v1", + proposal_ref="proposals.scan.1", + ) request = _admission_request() - control = _RecordingControl(snapshot) + control = _RecordingControl() resolver_calls: list[str] = [] apparatus_calls: list[tuple[str, str]] = [] @@ -1123,7 +1064,7 @@ def reject_shape(**kwargs: str) -> bool: ), ) assert rejected == "rejected" - assert resolver_calls == ["proposals.selection.1"] + assert resolver_calls == ["proposals.scan.1"] assert apparatus_calls == [(surface.implementation_selection_ref, surface.exposure_policy_ref)] assert control.admitted is None @@ -1213,13 +1154,22 @@ def resolve_different_coordinates(**kwargs: object) -> ParticipantValidatedActio def test_surface_apparatus_must_resolve_to_the_admission_selection() -> None: - snapshot, surface = _anchored_surface(surface_form="open_ended_generation") - selection = _surface_selection(surface) + payload = _surface_payload(surface_form="open_ended_generation") + payload["action_entries"][0]["eligibility"] = "eligible" # type: ignore[index] + payload["action_entries"][0]["eligibility_reason_refs"] = [] # type: ignore[index] + surface = ParticipantDecisionSurfaceModel.model_validate(payload) + selection = ParticipantDecisionSurfaceSelectionModel( + surface_id=surface.surface_id, + observation_order=surface.observation_order, + action_contract_address=SCAN, + argument_shape_ref="selection-shapes.scan.v1", + proposal_ref="proposals.scan.1", + ) request = _admission_request() mismatched_selection = request.implementation_selection.model_copy( update={"configuration_ref": "participant-configurations.other.v1"} ) - control = _RecordingControl(snapshot) + control = _RecordingControl() shape_calls: list[str] = [] rejected = control.admit_participant_decision_surface_selection( @@ -1260,472 +1210,3 @@ def test_presentation_cannot_be_encoded_as_selection_result_or_outcome() -> None payload["outcome"] = "succeeded" with pytest.raises(ValidationError, match="Extra inputs are not permitted"): ParticipantDecisionSurfaceModel.model_validate(payload) - - -def _compiled_participant_behavior() -> ParticipantBehaviorRuntime: - return ParticipantBehaviorRuntime( - address=PARTICIPANT, - name="red-agent", - spec={}, - participant_name="red-agent", - action_contract_addresses=(SCAN,), - observation_boundary_addresses=(BOUNDARY,), - ) - - -def _projection_with_anchor( - anchor: ParticipantDecisionSurfaceEpisodeReadinessAnchorModel | ParticipantDecisionSurfaceBehaviorAnchorModel, - *, - surface_form: str = "candidate_action_set", -) -> ParticipantDecisionSurfaceProjectionInput: - projection = _projection_input( - observation_order=anchor.decision_surface_order, - surface_form=surface_form, - ) - return replace( - projection, - observation_point=( - f"participant-episode-history:{anchor.anchor_order}" - if anchor.anchor_kind == "episode_readiness" - else f"behavior-history:{anchor.anchor_order}" - ), - projection_anchor=anchor, - evidence_refs=tuple(dict.fromkeys((*projection.evidence_refs, *anchor.evidence_refs))), - provenance_refs=tuple(dict.fromkeys((*projection.provenance_refs, *anchor.provenance_refs))), - ) - - -def _anchored_surface( - *, - surface_form: str, -) -> tuple[RuntimeSnapshot, ParticipantDecisionSurfaceModel]: - control_plane = RuntimeControlPlane(create_stub_target()) - control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - snapshot = control_plane.get_snapshot().snapshot - anchor = resolve_participant_episode_readiness_anchor( - snapshot, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=("evidence.episode-running",), - provenance_refs=("provenance.runtime-control-plane",), - ) - projection = _projection_with_anchor(anchor, surface_form=surface_form) - surface = project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=snapshot, - history_events=(), - projection=projection, - exposure_resolvers=_projection_exposure_resolvers(projection), - ) - return snapshot, surface - - -def _context_for_surface(surface: ParticipantDecisionSurfaceModel) -> ParticipantContextViewModel: - return ParticipantContextViewModel.model_validate( - { - "view_id": surface.context_view_ref, - "participant_address": surface.participant_address, - "episode_id": surface.episode_id, - "generated_at": "2026-07-26T08:00:00Z", - "source_snapshot_ref": "snapshots.run-1.initial", - "view_ref": "views.decision-surface.v1", - "meaning_ref": "semantics.decision-surface.v1", - "participant_scope": "participant_local", - "audience_scope": "participant_visible", - "observation_point": surface.observation_point, - "derived_from_refs": [ - "snapshots.run-1.initial", - surface.projection_anchor.event_ref, - ], - "source_layers": [ - { - "source_id": "episode-readiness", - "source_layer": "participant_episode_state", - "ref": "snapshots.run-1.initial", - "temporal_relation": "same_observation_point", - "observation_point": surface.observation_point, - "evidence_refs": surface.evidence_refs, - "provenance_refs": surface.provenance_refs, - } - ], - "transformation": { - "transformation_rule_ref": surface.projection_policy_ref, - "description": "Project the initial participant-local decision surface from episode readiness", - "input_source_ids": ["episode-readiness"], - "output_semantics_ref": "semantics.decision-surface.v1", - }, - "comparability": { - "comparability_class": "portable_equivalent", - "comparison_basis_ref": "comparability.decision-surface.v1", - "backend_disclosure_refs": [], - "limitations": surface.semantic_limitations, - }, - "evidence_refs": surface.evidence_refs, - "provenance_refs": surface.provenance_refs, - "semantic_limitations": surface.semantic_limitations, - "derivation_basis_ref": surface.projection_policy_ref, - "payload_ref": surface.surface_id, - "visibility_projection_ref": surface.visibility_projection_ref, - "marking_definition_refs": surface.marking_definition_refs, - "redaction_policy_ref": surface.redaction_policy_ref, - } - ) - - -def test_public_initialize_projects_initial_context_and_surface_before_first_admission() -> None: - control_plane = RuntimeControlPlane(create_stub_target()) - receipt = control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - snapshot = control_plane.get_snapshot().snapshot - - assert receipt.accepted is True - assert snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] - anchor = resolve_participant_episode_readiness_anchor( - snapshot, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=("evidence.episode-running",), - provenance_refs=("provenance.runtime-control-plane",), - ) - projection = _projection_with_anchor(anchor) - surface = project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=snapshot, - history_events=(), - projection=projection, - exposure_resolvers=_projection_exposure_resolvers(projection), - ) - context = _context_for_surface(surface) - - assert anchor.anchor_kind == "episode_readiness" - assert anchor.event_type == "episode_running" - assert anchor.decision_surface_order == 0 - assert anchor.anchor_order == 1 - assert surface.observation_order == 0 - assert surface.projection_anchor == anchor - assert surface.action_entries[0].action_contract_address == SCAN - validate_participant_decision_surface_context(surface, context) - - selection = _surface_selection(surface) - admission_request = _admission_request() - admitted = control_plane.admit_participant_decision_surface_selection( - _compiled_participant_behavior(), - surface=surface, - selection=selection, - admission_request=admission_request, - resolvers=ParticipantDecisionSurfaceBindingResolvers( - argument_shape=_resolved_selection, - apparatus=lambda **_: admission_request.implementation_selection, - ), - ) - admitted_snapshot = control_plane.get_snapshot().snapshot - - assert admitted.accepted is True - assert [event["event_type"] for event in admitted_snapshot.participant_behavior_history[PARTICIPANT]] == [ - "action_attempted", - "state_transition_recorded", - "observation_emitted", - ] - replayed = control_plane.admit_participant_decision_surface_selection( - _compiled_participant_behavior(), - surface=surface, - selection=selection, - admission_request=admission_request, - resolvers=ParticipantDecisionSurfaceBindingResolvers( - argument_shape=_resolved_selection, - apparatus=lambda **_: admission_request.implementation_selection, - ), - ) - replayed_snapshot = control_plane.get_snapshot().snapshot - assert replayed.accepted is False - assert len(replayed_snapshot.participant_behavior_history[PARTICIPANT]) == 3 - - -def test_control_plane_rejects_an_unanchored_surface_without_recording_behavior() -> None: - control_plane = RuntimeControlPlane(create_stub_target()) - control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - surface = _eligible_surface() - request = _admission_request() - - receipt = control_plane.admit_participant_decision_surface_selection( - _compiled_participant_behavior(), - surface=surface, - selection=_surface_selection(surface), - admission_request=request, - resolvers=ParticipantDecisionSurfaceBindingResolvers( - argument_shape=_resolved_selection, - apparatus=lambda **_: request.implementation_selection, - ), - ) - snapshot = control_plane.get_snapshot().snapshot - - assert receipt.accepted is False - assert snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] - - -def test_readiness_anchor_requires_current_complete_running_snapshot() -> None: - control_plane = RuntimeControlPlane(create_stub_target()) - control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - snapshot = control_plane.get_snapshot().snapshot - anchor = resolve_participant_episode_readiness_anchor( - snapshot, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=("evidence.episode-running",), - provenance_refs=("provenance.runtime-control-plane",), - ) - projection = _projection_with_anchor(anchor) - - with pytest.raises(ValueError, match="current trusted RuntimeSnapshot"): - project_participant_decision_surface( - _runtime_model(), - history_events=(), - projection=projection, - exposure_resolvers=_projection_exposure_resolvers(projection), - ) - with pytest.raises(ValueError, match="empty current-episode behavior history"): - project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=snapshot, - history_events=_history(), - projection=projection, - exposure_resolvers=_projection_exposure_resolvers(projection), - ) - with pytest.raises(ValueError, match="evidence_refs"): - project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=snapshot, - history_events=(), - projection=replace(projection, evidence_refs=("evidence.surface.other",)), - exposure_resolvers=_projection_exposure_resolvers(projection), - ) - with pytest.raises(ValueError, match="decision_surface_order zero"): - resolve_participant_episode_readiness_anchor( - snapshot, - participant_address=PARTICIPANT, - decision_surface_order=1, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - - -def test_reset_invalidates_old_surface_and_creates_new_episode_order_zero_anchor() -> None: - control_plane = RuntimeControlPlane(create_stub_target()) - control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - first_snapshot = control_plane.get_snapshot().snapshot - first_anchor = resolve_participant_episode_readiness_anchor( - first_snapshot, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=("evidence.episode-running",), - provenance_refs=("provenance.runtime-control-plane",), - ) - first_projection = _projection_with_anchor(first_anchor) - first_surface = project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=first_snapshot, - history_events=(), - projection=first_projection, - exposure_resolvers=_projection_exposure_resolvers(first_projection), - ) - - control_plane.reset_participant_episode(PARTICIPANT, episode_id="episode-2") - reset_snapshot = control_plane.get_snapshot().snapshot - stale_request = _admission_request() - stale = control_plane.admit_participant_decision_surface_selection( - _compiled_participant_behavior(), - surface=first_surface, - selection=_surface_selection(first_surface), - admission_request=stale_request, - resolvers=ParticipantDecisionSurfaceBindingResolvers( - argument_shape=_resolved_selection, - apparatus=lambda **_: stale_request.implementation_selection, - ), - ) - reset_anchor = resolve_participant_episode_readiness_anchor( - reset_snapshot, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=("evidence.episode-running.reset",), - provenance_refs=("provenance.runtime-control-plane",), - ) - - assert stale.accepted is False - assert reset_anchor.episode_id == "episode-2" - assert reset_anchor.episode_sequence_number == 1 - assert reset_anchor.decision_surface_order == 0 - assert reset_snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] - - -def test_restart_creates_a_new_episode_order_zero_anchor() -> None: - control_plane = RuntimeControlPlane(create_stub_target()) - control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - control_plane.terminate_participant_episode(PARTICIPANT) - control_plane.restart_participant_episode(PARTICIPANT, episode_id="episode-restarted") - snapshot = control_plane.get_snapshot().snapshot - - anchor = resolve_participant_episode_readiness_anchor( - snapshot, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=("evidence.episode-running.restart",), - provenance_refs=("provenance.runtime-control-plane",), - ) - - assert anchor.episode_id == "episode-restarted" - assert anchor.episode_sequence_number == 1 - assert anchor.decision_surface_order == 0 - assert snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] - - -def test_readiness_anchor_rejects_incomplete_forged_and_cross_scope_inputs() -> None: - control_plane = RuntimeControlPlane(create_stub_target()) - control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - snapshot = control_plane.get_snapshot().snapshot - anchor = resolve_participant_episode_readiness_anchor( - snapshot, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=("evidence.episode-running",), - provenance_refs=("provenance.runtime-control-plane",), - ) - - incomplete = replace( - snapshot, - participant_episode_history={ - PARTICIPANT: snapshot.participant_episode_history[PARTICIPANT][-1:], - }, - ) - with pytest.raises(ValueError, match="complete participant lifecycle history"): - resolve_participant_episode_readiness_anchor( - incomplete, - participant_address=PARTICIPANT, - decision_surface_order=0, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - - forged_ref = "participant-episode-event:sha256:" + "0" * 64 - forged = anchor.model_copy( - update={ - "event_ref": forged_ref, - "provenance_refs": [*anchor.provenance_refs, forged_ref], - } - ) - forged_projection = _projection_with_anchor(forged) - with pytest.raises(ValueError, match="current trusted RuntimeSnapshot"): - project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=snapshot, - history_events=(), - projection=forged_projection, - exposure_resolvers=_projection_exposure_resolvers(forged_projection), - ) - - cross_episode = anchor.model_copy(update={"episode_id": "episode-other"}) - with pytest.raises(ValueError, match="episode_id"): - project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=snapshot, - history_events=(), - projection=_projection_with_anchor(cross_episode), - exposure_resolvers=_projection_exposure_resolvers(_projection_with_anchor(cross_episode)), - ) - - cross_participant = anchor.model_copy(update={"participant_address": "participant.behavior.blue-agent"}) - with pytest.raises(ValueError, match="participant_address"): - project_participant_decision_surface( - _runtime_model(), - runtime_snapshot=snapshot, - history_events=(), - projection=_projection_with_anchor(cross_participant), - exposure_resolvers=_projection_exposure_resolvers(_projection_with_anchor(cross_participant)), - ) - - -def test_behavior_anchor_resolves_exact_current_episode_history_prefix() -> None: - control_plane = RuntimeControlPlane(create_stub_target()) - control_plane.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) - control_plane.admit_participant_action(_compiled_participant_behavior(), _admission_request()) - snapshot = control_plane.get_snapshot().snapshot - runtime_model = _runtime_model() - boundary = runtime_model.observation_boundaries[BOUNDARY] - runtime_model = replace( - runtime_model, - observation_boundaries={ - BOUNDARY: replace( - boundary, - view_transitions=(), - view_relation_timeline=(boundary.view_relation_timeline[0],), - ) - }, - ) - history = tuple( - ParticipantBehaviorHistoryEvent.from_payload(payload) - for payload in snapshot.participant_behavior_history[PARTICIPANT] - ) - anchor = resolve_participant_behavior_projection_anchor( - snapshot, - runtime_model=runtime_model, - participant_address=PARTICIPANT, - episode_id=EPISODE, - decision_surface_order=1, - behavior_history_order=2, - evidence_refs=("evidence.scan-result",), - provenance_refs=("provenance.runtime-control-plane",), - ) - - assert anchor.anchor_kind == "behavior_event" - assert anchor.decision_surface_order == 1 - assert anchor.anchor_order == 2 - assert anchor.history_prefix_length == 3 - assert anchor.event_type == "observation_emitted" - - projection = _projection_with_anchor(anchor) - surface = project_participant_decision_surface( - runtime_model, - runtime_snapshot=snapshot, - history_events=history, - projection=projection, - exposure_resolvers=_projection_exposure_resolvers(projection), - ) - assert surface.observation_order == 1 - assert surface.projection_anchor.anchor_order == 2 - with pytest.raises(ValueError, match="number of completed observation_emitted"): - resolve_participant_behavior_projection_anchor( - snapshot, - runtime_model=runtime_model, - participant_address=PARTICIPANT, - episode_id=EPISODE, - decision_surface_order=2, - behavior_history_order=2, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - forged_anchor = anchor.model_copy(update={"decision_surface_order": 2}) - forged_surface = surface.model_copy( - update={ - "observation_order": 2, - "projection_anchor": forged_anchor, - } - ) - request = _admission_request() - rejected = control_plane.admit_participant_decision_surface_selection( - _compiled_participant_behavior(), - surface=forged_surface, - selection=_surface_selection(surface).model_copy(update={"observation_order": 2}), - admission_request=request, - resolvers=ParticipantDecisionSurfaceBindingResolvers( - argument_shape=_resolved_selection, - apparatus=lambda **_: request.implementation_selection, - ), - ) - assert rejected.accepted is False - assert len(control_plane.get_snapshot().snapshot.participant_behavior_history[PARTICIPANT]) == 3 - with pytest.raises(ValueError, match="exact current behavior-history prefix"): - project_participant_decision_surface( - runtime_model, - runtime_snapshot=snapshot, - history_events=history[:-1], - projection=projection, - exposure_resolvers=_projection_exposure_resolvers(projection), - ) diff --git a/implementations/python/tests/test_sem_220_participant_decision_surface_v2.py b/implementations/python/tests/test_sem_220_participant_decision_surface_v2.py new file mode 100644 index 000000000..c5b685499 --- /dev/null +++ b/implementations/python/tests/test_sem_220_participant_decision_surface_v2.py @@ -0,0 +1,378 @@ +from __future__ import annotations + +import copy +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from pydantic import ValidationError +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceAssuranceV2Model, + ParticipantDecisionSurfaceBehaviorAnchorV2Model, + ParticipantDecisionSurfaceCausalCutModel, + ParticipantDecisionSurfaceDeliveryV2Model, + ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model, + ParticipantDecisionSurfaceExposureBindingV2Model, + ParticipantDecisionSurfaceSelectionV2Model, + ParticipantDecisionSurfaceSequenceCutModel, + ParticipantDecisionSurfaceV2Model, + ParticipantDecisionSurfaceViewV2Model, + schema_bundle, +) +from raes_contracts.satisfiability import canonical_contract_digest + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURE_ROOT = REPO_ROOT / "contracts" / "fixtures" / "control-plane" / "participant-decision-surface-v2" + + +def _sequence_cut() -> ParticipantDecisionSurfaceSequenceCutModel: + return ParticipantDecisionSurfaceSequenceCutModel( + cut_kind="sequence_prefix", + cut_ref="participant-state-cuts.red.episode-1.initial", + history_domain="participant_episode_lifecycle", + order_model="control_plane_order", + anchor_event_ref="participant-episode-event:sha256:" + "a" * 64, + anchor_order=1, + history_prefix_length=2, + predecessor_event_refs=["participant-episode-event:sha256:" + "b" * 64], + ) + + +def _anchor() -> ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model: + return ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model( + anchor_kind="episode_readiness", + participant_address="participants.red", + episode_id="episode-1", + decision_epoch=0, + event_ref="participant-episode-event:sha256:" + "a" * 64, + state_cut=_sequence_cut(), + event_type="episode_running", + episode_sequence_number=0, + evidence_refs=["evidence.episode-running"], + provenance_refs=[ + "provenance.episode-running", + "participant-episode-event:sha256:" + "a" * 64, + ], + ) + + +def _view() -> ParticipantDecisionSurfaceViewV2Model: + return ParticipantDecisionSurfaceViewV2Model( + surface_id="decision-surfaces.red.episode-1.epoch-0", + participant_address="participants.red", + episode_id="episode-1", + decision_epoch=0, + information_state_ref="information-states.red.episode-1.epoch-0", + context_view_ref="context-views.red.episode-1.epoch-0", + decision_control_mode="agent", + visible_context_refs=["context.network-segment"], + action_entries=[ + { + "entry_id": "scan", + "action_contract_address": "actions.scan", + "presentation_basis_ref": "presentation.scan", + "visibility": "observable", + "eligibility": "eligible", + "eligibility_reason_refs": [], + "constraint_refs": ["constraints.scan"], + "selection_shape_ref": "argument-shapes.scan", + "support": "supported", + "support_refs": ["support.scan"], + "affordance_refs": ["affordances.scanner"], + "realization_refs": ["realizations.scan"], + } + ], + affordance_refs=["affordances.scanner"], + form={ + "surface_form": "candidate_action_set", + "selection_meaning_ref": "selection-meaning.candidate.v1", + "candidate_entry_ids": ["scan"], + }, + marking_definition_refs=["markings.participant"], + redaction_policy_ref="redaction.participant", + semantic_limitations=["limitations.bounded-surface"], + ) + + +def _binding( + item_ref: str = "actions.scan", + *, + suffix: str = "scan", +) -> ParticipantDecisionSurfaceExposureBindingV2Model: + return ParticipantDecisionSurfaceExposureBindingV2Model( + item_ref=item_ref, + authorization_record_ref=f"authorizations.{suffix}", + source_ref=item_ref, + source_layer_ref="source-layer.scenario", + participant_address="participants.red", + episode_id="episode-1", + audience_scope_ref="audiences.red", + decision_epoch=0, + decision_cut_ref="participant-state-cuts.red.episode-1.initial", + visibility_basis_ref="view-rules.scan", + projection_policy_ref="projection-policies.red", + projection_policy_revision="revision-1", + projection_policy_decision_ref="policy-decisions.red.initial", + exposure_policy_ref="exposure-policies.red", + exposure_policy_version="1", + exposure_policy_digest="sha256:" + "c" * 64, + operation="projection", + operation_basis_ref="operations.project", + actor_ref="actors.runtime", + controller_ref="controllers.runtime", + authority_basis_ref="authority.participant", + source_marking_definition_refs=["markings.participant"], + result_marking_definition_refs=["markings.participant"], + source_provenance_refs=[f"provenance.{suffix}"], + result_provenance_refs=[f"provenance.{suffix}"], + evidence_refs=[f"evidence.authorization.{suffix}"], + provenance_refs=[f"provenance.{suffix}"], + loss_and_limitations=["limitations.authorization"], + ) + + +def _assurance(view: ParticipantDecisionSurfaceViewV2Model) -> ParticipantDecisionSurfaceAssuranceV2Model: + return ParticipantDecisionSurfaceAssuranceV2Model( + participant_address=view.participant_address, + episode_id=view.episode_id, + decision_epoch=view.decision_epoch, + behavior_specification_address="behavior-specifications.red", + observation_boundary_address="observation-boundaries.red", + implementation_selection_ref="implementation-selections.red", + audience_scope_ref="audiences.red", + projection_policy_ref="projection-policies.red", + projection_policy_revision="revision-1", + projection_policy_decision_ref="policy-decisions.red.initial", + exposure_policy_ref="exposure-policies.red", + visibility_projection_ref="visibility-projections.red.initial", + participant_memory_scope="persistent_across_episodes", + memory_reset_authority_ref=None, + participant_view_digest=canonical_contract_digest(view), + derivation_anchor=_anchor(), + exposure_bindings=[ + _binding("context.network-segment", suffix="context.network-segment"), + _binding(), + _binding("affordances.scanner", suffix="affordances.scanner"), + ], + evidence_refs=[ + "evidence.episode-running", + "evidence.authorization.context.network-segment", + "evidence.authorization.scan", + "evidence.authorization.affordances.scanner", + ], + provenance_refs=[ + "provenance.episode-running", + "participant-episode-event:sha256:" + "a" * 64, + "provenance.context.network-segment", + "provenance.scan", + "provenance.affordances.scanner", + ], + ) + + +def _projected_surface() -> ParticipantDecisionSurfaceV2Model: + view = _view() + return ParticipantDecisionSurfaceV2Model( + schema_version="participant-decision-surface/v2", + surface_state="projected", + participant_view=view, + assurance=_assurance(view), + ) + + +def _delivery(surface: ParticipantDecisionSurfaceV2Model) -> ParticipantDecisionSurfaceDeliveryV2Model: + view = surface.participant_view + return ParticipantDecisionSurfaceDeliveryV2Model( + delivery_ref="decision-surface-deliveries.red.episode-1.epoch-0", + surface_id=view.surface_id, + participant_address=view.participant_address, + episode_id=view.episode_id, + decision_epoch=view.decision_epoch, + participant_view_digest=surface.assurance.participant_view_digest, + delivery_basis="emission_is_delivery", + delivery_cut_ref=surface.assurance.derivation_anchor.state_cut.cut_ref, + delivery_authorization_ref="delivery-authorizations.red.episode-1.epoch-0", + delivery_policy_decision_ref="delivery-policy-decisions.red.episode-1.epoch-0", + observation_ref="participant-observations.red.episode-1.epoch-0", + evidence_refs=["evidence.surface-delivery"], + provenance_refs=["provenance.surface-delivery"], + limitations=["limitations.synchronous-delivery"], + ) + + +def test_v2_separates_participant_view_from_derivation_and_evidence() -> None: + surface = _projected_surface() + + participant_payload = surface.participant_view.model_dump(mode="json") + assurance_payload = surface.assurance.model_dump(mode="json") + + assert participant_payload["decision_epoch"] == 0 + assert "derivation_anchor" not in participant_payload + assert "evidence_refs" not in participant_payload + assert "provenance_refs" not in participant_payload + assert assurance_payload["derivation_anchor"]["state_cut"]["anchor_order"] == 1 + assert assurance_payload["participant_view_digest"] == canonical_contract_digest(surface.participant_view) + + +def test_v2_rejects_a_sequence_cut_whose_prefix_does_not_end_at_its_anchor() -> None: + payload = _sequence_cut().model_dump(mode="json") + payload["history_prefix_length"] = 1 + + with pytest.raises(ValidationError, match="history_prefix_length must equal anchor_order \\+ 1"): + ParticipantDecisionSurfaceSequenceCutModel.model_validate(payload) + + +def test_v2_causal_cut_preserves_a_frontier_without_inventing_a_scalar_order() -> None: + frontier = [ + "participant-behavior-event:sha256:" + "a" * 64, + "participant-behavior-event:sha256:" + "b" * 64, + ] + cut = ParticipantDecisionSurfaceCausalCutModel( + cut_kind="causal_frontier", + cut_ref="participant-state-cuts.red.concurrent", + history_domain="participant_behavior_history", + order_model="causal_partial_order", + frontier_event_refs=frontier, + predecessor_closure_ref="participant-state-cut-closures.red.concurrent", + ) + anchor = ParticipantDecisionSurfaceBehaviorAnchorV2Model( + anchor_kind="behavior_event", + participant_address="participants.red", + episode_id="episode-1", + decision_epoch=1, + event_ref=frontier[0], + state_cut=cut, + event_type="observation_emitted", + action_instance_id="scan-1", + evidence_refs=["evidence.concurrent"], + provenance_refs=[frontier[0]], + ) + + payload = anchor.model_dump(mode="json") + assert payload["state_cut"]["frontier_event_refs"] == frontier + assert "anchor_order" not in payload["state_cut"] + + +def test_v2_rejects_assurance_bound_to_a_different_decision_epoch() -> None: + surface = _projected_surface() + payload = surface.model_dump(mode="json") + payload["assurance"]["decision_epoch"] = 1 + + with pytest.raises(ValidationError, match="decision_epoch"): + ParticipantDecisionSurfaceV2Model.model_validate(payload) + + +def test_v2_rejects_a_participant_view_digest_mismatch() -> None: + surface = _projected_surface() + payload = surface.model_dump(mode="json") + payload["assurance"]["participant_view_digest"] = "sha256:" + "d" * 64 + + with pytest.raises(ValidationError, match="participant_view_digest"): + ParticipantDecisionSurfaceV2Model.model_validate(payload) + + +def test_v2_memory_scope_does_not_treat_episode_reset_as_memory_erasure() -> None: + surface = _projected_surface() + + assert surface.assurance.participant_memory_scope == "persistent_across_episodes" + assert surface.assurance.memory_reset_authority_ref is None + + payload = surface.model_dump(mode="json") + payload["assurance"]["participant_memory_scope"] = "episode_local_reset" + with pytest.raises(ValidationError, match="requires memory_reset_authority_ref"): + ParticipantDecisionSurfaceV2Model.model_validate(payload) + + +def test_v2_episode_local_memory_scope_requires_and_carries_reset_authority() -> None: + surface = _projected_surface() + payload = surface.model_dump(mode="json") + payload["assurance"]["participant_memory_scope"] = "episode_local_reset" + payload["assurance"]["memory_reset_authority_ref"] = "memory-reset-authorities.red.episode-1" + + local = ParticipantDecisionSurfaceV2Model.model_validate(payload) + assert local.assurance.memory_reset_authority_ref == "memory-reset-authorities.red.episode-1" + + payload["assurance"]["participant_memory_scope"] = "persistent_across_episodes" + with pytest.raises(ValidationError, match="must not claim a reset authority"): + ParticipantDecisionSurfaceV2Model.model_validate(payload) + + +def test_v2_requires_delivery_only_for_the_delivered_lifecycle_state() -> None: + projected = _projected_surface() + delivered_payload = projected.model_dump(mode="json") + delivered_payload["surface_state"] = "delivered" + + with pytest.raises(ValidationError, match="delivered surfaces require delivery"): + ParticipantDecisionSurfaceV2Model.model_validate(delivered_payload) + + projected_payload = projected.model_dump(mode="json") + projected_payload["delivery"] = _delivery(projected).model_dump(mode="json") + with pytest.raises(ValidationError, match="projected surfaces must not carry delivery"): + ParticipantDecisionSurfaceV2Model.model_validate(projected_payload) + + +def test_v2_delivery_must_bind_the_exact_participant_view_digest_and_epoch() -> None: + projected = _projected_surface() + payload = projected.model_dump(mode="json") + payload["surface_state"] = "delivered" + delivery = _delivery(projected).model_dump(mode="json") + delivery["decision_epoch"] = 1 + payload["delivery"] = delivery + + with pytest.raises(ValidationError, match="delivery disagrees with the participant view"): + ParticipantDecisionSurfaceV2Model.model_validate(payload) + + +def test_v2_selection_binds_delivery_and_the_canonical_participant_view() -> None: + projected = _projected_surface() + delivered_payload = projected.model_dump(mode="json") + delivered_payload["surface_state"] = "delivered" + delivered_payload["delivery"] = _delivery(projected).model_dump(mode="json") + delivered = ParticipantDecisionSurfaceV2Model.model_validate(delivered_payload) + + selection = ParticipantDecisionSurfaceSelectionV2Model( + surface_id=delivered.participant_view.surface_id, + decision_epoch=delivered.participant_view.decision_epoch, + participant_view_digest=delivered.assurance.participant_view_digest, + delivery_ref=delivered.delivery.delivery_ref if delivered.delivery else "", + action_contract_address="actions.scan", + argument_shape_ref="argument-shapes.scan", + proposal_ref="proposals.scan.1", + arguments={}, + ) + + assert selection.delivery_ref == "decision-surface-deliveries.red.episode-1.epoch-0" + assert "observation_order" not in selection.model_dump(mode="json") + + +def test_v2_is_closed_against_legacy_or_hidden_order_fields() -> None: + payload = _projected_surface().model_dump(mode="json") + payload["observation_order"] = 0 + payload["participant_view"]["anchor_order"] = 1 + + with pytest.raises(ValidationError): + ParticipantDecisionSurfaceV2Model.model_validate(copy.deepcopy(payload)) + + +def test_v2_published_schema_and_fixtures_match_the_contract_model() -> None: + schema = schema_bundle()["participant-decision-surface-v2"] + validator = Draft202012Validator(schema) + valid_paths = sorted((FIXTURE_ROOT / "valid").glob("*.json")) + invalid_paths = sorted((FIXTURE_ROOT / "invalid").glob("*.json")) + + assert {path.stem for path in valid_paths} == {"delivered-initial", "projected-initial"} + assert {path.stem for path in invalid_paths} == { + "delivered-without-delivery", + "episode-local-without-reset-authority", + "legacy-observation-order", + } + for path in valid_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + validator.validate(payload) + ParticipantDecisionSurfaceV2Model.model_validate(payload) + for path in invalid_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + assert list(validator.iter_errors(payload)) + with pytest.raises(ValidationError): + ParticipantDecisionSurfaceV2Model.model_validate(payload) diff --git a/implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py b/implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py new file mode 100644 index 000000000..5f4e3b32a --- /dev/null +++ b/implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py @@ -0,0 +1,548 @@ +"""End-to-end exact-cut, delivery, and admission tests for SEM-220 v2.""" + +from __future__ import annotations + +from dataclasses import replace + +import pytest +from raes_backend_stubs.stubs import create_stub_target +from raes_contracts.contracts import ( + ParticipantDecisionSurfaceDeliveryV2Model, + ParticipantDecisionSurfaceSelectionV2Model, +) +from raes_contracts.participant_binding_v2 import ParticipantDecisionSurfaceBindingResolversV2 +from raes_contracts.participant_decision_surface_delivery import deliver_participant_decision_surface_v2 +from raes_processor.models import ( + ParticipantBehaviorHistoryEvent, + ParticipantBehaviorRuntime, + ParticipantDecisionSurfaceProjectionInputV2, + ParticipantExposureAssessment, + ParticipantExposureAuthorizationRecordV2, + ParticipantExposurePolicyDecisionV2, + ParticipantExposureResolversV2, + project_participant_decision_surface_v2, + resolve_participant_behavior_projection_anchor_v2, + resolve_participant_episode_readiness_anchor_v2, +) +from raes_runtime.control_plane import RuntimeControlPlane +from test_sem_220_participant_decision_surface import ( + BEHAVIOR, + BOUNDARY, + EPISODE, + PARTICIPANT, + SCAN, + SCAN_AFFORDANCE, + SCAN_ENTRY, + _admission_request, + _assessment, + _projection_implementation_selection, + _resolved_selection, + _runtime_model, +) + + +def _compiled_participant_behavior() -> ParticipantBehaviorRuntime: + return ParticipantBehaviorRuntime( + address=PARTICIPANT, + name="red-agent", + spec={}, + participant_name="red-agent", + action_contract_addresses=(SCAN,), + observation_boundary_addresses=(BOUNDARY,), + ) + + +def _authorization( + item_ref: str, + *, + episode_id: str, + decision_epoch: int, + decision_cut_ref: str, +) -> ParticipantExposureAuthorizationRecordV2: + return ParticipantExposureAuthorizationRecordV2( + authorization_record_ref=f"exposure-authorizations.v2.{item_ref}.epoch-{decision_epoch}", + item_ref=item_ref, + source_ref=item_ref, + source_layer_ref=f"source-layers.{item_ref}", + participant_address=PARTICIPANT, + episode_id=episode_id, + audience_scope_ref="audience.participant.red-agent", + decision_epoch=decision_epoch, + decision_cut_ref=decision_cut_ref, + implementation_selection_ref="participant-selections.red.agent.v2", + projection_policy_ref="projection-policy.red.v2", + projection_policy_revision="2", + projection_policy_decision_ref=f"projection-policy-decisions.red.epoch-{decision_epoch}", + exposure_policy_ref="exposure-policy.red.v2", + exposure_policy_version="2", + exposure_policy_digest="sha256:" + "4" * 64, + visibility_basis_ref=f"visibility-bases.{item_ref}", + operation="disclosure", + operation_basis_ref=f"disclosures.{item_ref}.v2", + actor_ref="actors.runtime-projector", + controller_ref="controllers.red-agent", + authority_basis_ref="authorities.red-agent.v2", + backend_support_ref="backend-support.reference.v2", + source_marking_definition_refs=("markings.participant-visible.v2",), + result_marking_definition_refs=("markings.participant-visible.v2",), + source_provenance_refs=("provenance.surface.v2",), + result_provenance_refs=("provenance.surface.v2",), + evidence_refs=("evidence.surface.v2",), + provenance_refs=("provenance.surface.v2",), + loss_and_limitations=("No known projection loss",), + ) + + +def _projection( + anchor, +) -> ParticipantDecisionSurfaceProjectionInputV2: + emitted_refs = ("context.public", SCAN, SCAN_AFFORDANCE) + return ParticipantDecisionSurfaceProjectionInputV2( + surface_id=f"decision-surfaces.red.{anchor.episode_id}.epoch-{anchor.decision_epoch}", + participant_address=PARTICIPANT, + episode_id=anchor.episode_id, + decision_epoch=anchor.decision_epoch, + information_state_ref=f"information-states.red.{anchor.episode_id}.epoch-{anchor.decision_epoch}", + behavior_specification_address=BEHAVIOR, + observation_boundary_address=BOUNDARY, + context_view_ref=f"context-views.red.{anchor.episode_id}.epoch-{anchor.decision_epoch}", + implementation_selection_ref="participant-selections.red.agent.v2", + decision_control_mode="autonomous", + audience_scope_ref="audience.participant.red-agent", + projection_policy_ref="projection-policy.red.v2", + projection_policy_revision="2", + projection_policy_decision_ref=f"projection-policy-decisions.red.epoch-{anchor.decision_epoch}", + exposure_policy_ref="exposure-policy.red.v2", + visibility_projection_ref=f"visibility-projection.red.epoch-{anchor.decision_epoch}", + participant_memory_scope="persistent_across_episodes", + memory_reset_authority_ref=None, + visible_context_refs=("context.public",), + action_assessments={SCAN: _assessment(SCAN, entry_id=SCAN_ENTRY)}, + exposure_assessments={ + item_ref: ParticipantExposureAssessment( + item_ref=item_ref, + authorization_record_ref=f"exposure-authorizations.v2.{item_ref}.epoch-{anchor.decision_epoch}", + ) + for item_ref in emitted_refs + }, + form={ + "surface_form": "candidate_action_set", + "selection_meaning_ref": "selection-meaning.candidate.v2", + "candidate_entry_ids": [SCAN_ENTRY], + }, + evidence_refs=tuple(dict.fromkeys((*anchor.evidence_refs, "evidence.policy.v2", "evidence.surface.v2"))), + provenance_refs=tuple( + dict.fromkeys((*anchor.provenance_refs, "provenance.policy.v2", "provenance.surface.v2")) + ), + marking_definition_refs=("markings.participant-visible.v2",), + redaction_policy_ref="redaction.red.v2", + semantic_limitations=("Projection is not delivery, selection, admission, execution, or outcome",), + derivation_anchor=anchor, + ) + + +def _exposure_resolvers( + projection: ParticipantDecisionSurfaceProjectionInputV2, + *, + resolved_cut_ref: str | None = None, +) -> tuple[ParticipantExposureResolversV2, object]: + emitted_refs = tuple(projection.exposure_assessments) + selection = _projection_implementation_selection( + decision_control_mode=projection.decision_control_mode, + permitted_refs=emitted_refs, + ).model_copy( + update={ + "participant_contract_versions": [ + "participant-behavior-history-event-stream-v1", + "participant-decision-surface-v2", + ], + "exposure_policy": _projection_implementation_selection( + decision_control_mode=projection.decision_control_mode, + permitted_refs=emitted_refs, + ).exposure_policy.model_copy( + update={ + "policy_id": projection.exposure_policy_ref, + "policy_version": "2", + "policy_digest": "sha256:" + "4" * 64, + } + ), + } + ) + authorizations = { + item_ref: _authorization( + item_ref, + episode_id=projection.episode_id, + decision_epoch=projection.decision_epoch, + decision_cut_ref=projection.decision_cut_ref, + ) + for item_ref in emitted_refs + } + policy_decision = ParticipantExposurePolicyDecisionV2( + policy_ref=projection.projection_policy_ref, + revision=projection.projection_policy_revision, + decision_ref=projection.projection_policy_decision_ref, + decision_cut_ref=resolved_cut_ref or projection.decision_cut_ref, + evidence_refs=("evidence.policy.v2",), + provenance_refs=("provenance.policy.v2",), + limitations=("Policy decision is scoped to the identified state cut",), + ) + return ( + ParticipantExposureResolversV2( + apparatus=lambda **_: selection, + projection_policy=lambda **_: policy_decision, + authorization=lambda *, authorization_record_ref, item_ref, decision_cut_ref: ( + authorizations.get(item_ref) + if decision_cut_ref == projection.decision_cut_ref + and authorizations.get(item_ref) is not None + and authorizations[item_ref].authorization_record_ref == authorization_record_ref + else None + ), + ), + selection, + ) + + +def _delivery(surface) -> ParticipantDecisionSurfaceDeliveryV2Model: + return ParticipantDecisionSurfaceDeliveryV2Model( + delivery_ref=f"decision-surface-deliveries.{surface.participant_view.surface_id}", + surface_id=surface.participant_view.surface_id, + participant_address=surface.participant_view.participant_address, + episode_id=surface.participant_view.episode_id, + decision_epoch=surface.participant_view.decision_epoch, + participant_view_digest=surface.assurance.participant_view_digest, + delivery_basis="emission_is_delivery", + delivery_cut_ref=surface.assurance.derivation_anchor.state_cut.cut_ref, + delivery_authorization_ref=f"delivery-authorizations.epoch-{surface.participant_view.decision_epoch}", + delivery_policy_decision_ref=f"delivery-policy-decisions.epoch-{surface.participant_view.decision_epoch}", + observation_ref=f"participant-view-observations.epoch-{surface.participant_view.decision_epoch}", + evidence_refs=("evidence.surface-delivery.v2",), + provenance_refs=("provenance.surface-delivery.v2",), + limitations=("Synchronous reference-runtime delivery",), + ) + + +def _initial_surface(): + control = RuntimeControlPlane(create_stub_target()) + control.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + snapshot = control.get_snapshot().snapshot + anchor = resolve_participant_episode_readiness_anchor_v2( + snapshot, + participant_address=PARTICIPANT, + decision_epoch=0, + evidence_refs=("evidence.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection(anchor) + resolvers, selection = _exposure_resolvers(projection) + surface = project_participant_decision_surface_v2( + _runtime_model(), + snapshot, + history_events=(), + projection=projection, + exposure_resolvers=resolvers, + ) + return control, surface, selection + + +def _selection_for(delivered) -> ParticipantDecisionSurfaceSelectionV2Model: + assert delivered.delivery is not None + return ParticipantDecisionSurfaceSelectionV2Model( + surface_id=delivered.participant_view.surface_id, + decision_epoch=delivered.participant_view.decision_epoch, + participant_view_digest=delivered.assurance.participant_view_digest, + delivery_ref=delivered.delivery.delivery_ref, + action_contract_address=SCAN, + argument_shape_ref=delivered.participant_view.action_entries[0].selection_shape_ref, + proposal_ref=f"proposals.scan.v2.{delivered.participant_view.episode_id}", + arguments={}, + ) + + +def test_initial_epoch_projects_delivers_and_admits_only_the_exact_view() -> None: + control, projected, implementation_selection = _initial_surface() + delivery = _delivery(projected) + delivered = deliver_participant_decision_surface_v2( + projected, + delivery_ref=delivery.delivery_ref, + resolver=lambda **_: delivery, + ) + selection = ParticipantDecisionSurfaceSelectionV2Model( + surface_id=delivered.participant_view.surface_id, + decision_epoch=0, + participant_view_digest=delivered.assurance.participant_view_digest, + delivery_ref=delivery.delivery_ref, + action_contract_address=SCAN, + argument_shape_ref=delivered.participant_view.action_entries[0].selection_shape_ref, + proposal_ref="proposals.scan.v2.1", + arguments={}, + ) + request = replace(_admission_request(), implementation_selection=implementation_selection) + + receipt = control.admit_participant_decision_surface_selection_v2( + _compiled_participant_behavior(), + surface=delivered, + selection=selection, + admission_request=request, + resolvers=ParticipantDecisionSurfaceBindingResolversV2( + argument_shape=_resolved_selection, + apparatus=lambda **_: implementation_selection, + delivery=lambda **_: delivery, + ), + ) + + assert receipt.accepted is True + assert delivered.participant_view.decision_epoch == 0 + assert delivered.assurance.derivation_anchor.state_cut.history_domain == "participant_episode_lifecycle" + assert len(control.get_snapshot().snapshot.participant_behavior_history[PARTICIPANT]) == 3 + + replay = control.admit_participant_decision_surface_selection_v2( + _compiled_participant_behavior(), + surface=delivered, + selection=selection, + admission_request=request, + resolvers=ParticipantDecisionSurfaceBindingResolversV2( + argument_shape=_resolved_selection, + apparatus=lambda **_: implementation_selection, + delivery=lambda **_: delivery, + ), + ) + assert replay.accepted is False + assert len(control.get_snapshot().snapshot.participant_behavior_history[PARTICIPANT]) == 3 + + +def test_v2_admission_requires_the_participant_implementation_to_declare_v2_support() -> None: + control, projected, implementation_selection = _initial_surface() + delivery = _delivery(projected) + delivered = deliver_participant_decision_surface_v2( + projected, + delivery_ref=delivery.delivery_ref, + resolver=lambda **_: delivery, + ) + undeclared = implementation_selection.model_copy( + update={"participant_contract_versions": ["participant-behavior-history-event-stream-v1"]} + ) + + rejected = control.admit_participant_decision_surface_selection_v2( + _compiled_participant_behavior(), + surface=delivered, + selection=_selection_for(delivered), + admission_request=replace(_admission_request(), implementation_selection=undeclared), + resolvers=ParticipantDecisionSurfaceBindingResolversV2( + argument_shape=_resolved_selection, + apparatus=lambda **_: undeclared, + delivery=lambda **_: delivery, + ), + ) + + assert rejected.accepted is False + assert control.get_snapshot().snapshot.participant_behavior_history.get(PARTICIPANT, []) == [] + + +@pytest.mark.parametrize("transition", ("reset", "restart")) +def test_reset_and_restart_invalidate_prior_surfaces_and_create_a_new_epoch_zero(transition: str) -> None: + control, projected, implementation_selection = _initial_surface() + delivery = _delivery(projected) + delivered = deliver_participant_decision_surface_v2( + projected, + delivery_ref=delivery.delivery_ref, + resolver=lambda **_: delivery, + ) + if transition == "restart": + control.terminate_participant_episode(PARTICIPANT) + control.restart_participant_episode(PARTICIPANT, episode_id=f"{EPISODE}-restarted") + else: + control.reset_participant_episode(PARTICIPANT, episode_id=f"{EPISODE}-reset") + + rejected = control.admit_participant_decision_surface_selection_v2( + _compiled_participant_behavior(), + surface=delivered, + selection=_selection_for(delivered), + admission_request=replace(_admission_request(), implementation_selection=implementation_selection), + resolvers=ParticipantDecisionSurfaceBindingResolversV2( + argument_shape=_resolved_selection, + apparatus=lambda **_: implementation_selection, + delivery=lambda **_: delivery, + ), + ) + snapshot = control.get_snapshot().snapshot + new_episode_id = snapshot.participant_episode_results[PARTICIPANT]["episode_id"] + anchor = resolve_participant_episode_readiness_anchor_v2( + snapshot, + participant_address=PARTICIPANT, + decision_epoch=0, + evidence_refs=(f"evidence.{transition}.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection(anchor) + exposure_resolvers, _ = _exposure_resolvers(projection) + new_surface = project_participant_decision_surface_v2( + _runtime_model(), + snapshot, + history_events=(), + projection=projection, + exposure_resolvers=exposure_resolvers, + ) + + assert rejected.accepted is False + assert new_surface.participant_view.episode_id == new_episode_id + assert new_surface.participant_view.decision_epoch == 0 + assert new_surface.assurance.participant_memory_scope == "persistent_across_episodes" + + +def test_reset_preserves_prior_behavior_evidence_while_the_new_episode_starts_at_epoch_zero() -> None: + control, _, implementation_selection = _initial_surface() + admitted = control.admit_participant_action( + _compiled_participant_behavior(), + replace(_admission_request(), implementation_selection=implementation_selection), + ) + assert admitted.accepted is True + assert len(control.get_snapshot().snapshot.participant_behavior_history[PARTICIPANT]) == 3 + + control.reset_participant_episode(PARTICIPANT, episode_id=f"{EPISODE}-memory-preserved") + snapshot = control.get_snapshot().snapshot + assert len(snapshot.participant_behavior_history[PARTICIPANT]) == 3 + + anchor = resolve_participant_episode_readiness_anchor_v2( + snapshot, + participant_address=PARTICIPANT, + decision_epoch=0, + evidence_refs=("evidence.reset.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection(anchor) + exposure_resolvers, _ = _exposure_resolvers(projection) + surface = project_participant_decision_surface_v2( + _runtime_model(), + snapshot, + history_events=(), + projection=projection, + exposure_resolvers=exposure_resolvers, + ) + + assert surface.participant_view.decision_epoch == 0 + assert surface.assurance.participant_memory_scope == "persistent_across_episodes" + assert surface.assurance.memory_reset_authority_ref is None + + +def test_projection_policy_must_be_the_decision_at_the_exact_state_cut() -> None: + control = RuntimeControlPlane(create_stub_target()) + control.initialize_participant_episode(PARTICIPANT, episode_id=EPISODE) + snapshot = control.get_snapshot().snapshot + anchor = resolve_participant_episode_readiness_anchor_v2( + snapshot, + participant_address=PARTICIPANT, + decision_epoch=0, + evidence_refs=("evidence.episode-running",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection(anchor) + resolvers, _ = _exposure_resolvers(projection, resolved_cut_ref="participant-state-cuts.stale") + + with pytest.raises(ValueError, match="decision_cut_ref"): + project_participant_decision_surface_v2( + _runtime_model(), + snapshot, + history_events=(), + projection=projection, + exposure_resolvers=resolvers, + ) + + +def test_delivery_fails_closed_when_no_delivery_time_authority_resolves() -> None: + _, projected, _ = _initial_surface() + delivery = _delivery(projected) + + with pytest.raises(ValueError, match="delivery_ref did not resolve"): + deliver_participant_decision_surface_v2( + projected, + delivery_ref=delivery.delivery_ref, + resolver=lambda **_: None, + ) + + +def test_admission_rejects_a_surface_whose_derivation_cut_became_stale() -> None: + control, projected, implementation_selection = _initial_surface() + delivery = _delivery(projected) + delivered = deliver_participant_decision_surface_v2( + projected, + delivery_ref=delivery.delivery_ref, + resolver=lambda **_: delivery, + ) + control.admit_participant_action( + _compiled_participant_behavior(), + replace(_admission_request(), implementation_selection=implementation_selection), + ) + selection = ParticipantDecisionSurfaceSelectionV2Model( + surface_id=delivered.participant_view.surface_id, + decision_epoch=0, + participant_view_digest=delivered.assurance.participant_view_digest, + delivery_ref=delivery.delivery_ref, + action_contract_address=SCAN, + argument_shape_ref=delivered.participant_view.action_entries[0].selection_shape_ref, + proposal_ref="proposals.scan.v2.stale", + arguments={}, + ) + + rejected = control.admit_participant_decision_surface_selection_v2( + _compiled_participant_behavior(), + surface=delivered, + selection=selection, + admission_request=replace(_admission_request(), implementation_selection=implementation_selection), + resolvers=ParticipantDecisionSurfaceBindingResolversV2( + argument_shape=_resolved_selection, + apparatus=lambda **_: implementation_selection, + delivery=lambda **_: delivery, + ), + ) + + assert rejected.accepted is False + assert len(control.get_snapshot().snapshot.participant_behavior_history[PARTICIPANT]) == 3 + + +def test_later_epoch_uses_behavior_cut_without_reinterpreting_decision_epoch_as_history_order() -> None: + control, _, implementation_selection = _initial_surface() + control.admit_participant_action( + _compiled_participant_behavior(), + replace(_admission_request(), implementation_selection=implementation_selection), + ) + snapshot = control.get_snapshot().snapshot + history = tuple( + ParticipantBehaviorHistoryEvent.from_payload(payload) + for payload in snapshot.participant_behavior_history[PARTICIPANT] + ) + runtime_model = _runtime_model() + boundary = runtime_model.observation_boundaries[BOUNDARY] + runtime_model = replace( + runtime_model, + observation_boundaries={ + BOUNDARY: replace( + boundary, + view_transitions=(), + view_relation_timeline=(boundary.view_relation_timeline[0],), + ) + }, + ) + anchor = resolve_participant_behavior_projection_anchor_v2( + snapshot, + runtime_model=runtime_model, + participant_address=PARTICIPANT, + episode_id=EPISODE, + decision_epoch=1, + behavior_history_order=2, + evidence_refs=("evidence.scan-result",), + provenance_refs=("provenance.runtime-control-plane",), + ) + projection = _projection(anchor) + resolvers, _ = _exposure_resolvers(projection) + + surface = project_participant_decision_surface_v2( + runtime_model, + snapshot, + history_events=history, + projection=projection, + exposure_resolvers=resolvers, + ) + + assert surface.participant_view.decision_epoch == 1 + assert surface.assurance.derivation_anchor.state_cut.anchor_order == 2 + assert surface.assurance.derivation_anchor.state_cut.history_prefix_length == 3 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 977e8fd36..fcbaf456c 100644 --- a/implementations/python/tests/test_sem_230_information_flow_control.py +++ b/implementations/python/tests/test_sem_230_information_flow_control.py @@ -4,6 +4,7 @@ from copy import deepcopy +import pytest from hypothesis import given, settings from hypothesis import strategies as st from raes_contracts.behavioral_relations import load_behavioral_relation_catalog @@ -12,17 +13,21 @@ CrossingKind, Decision, Label, - PolicyRevision, + ParticipantMemoryScope, + ProjectionPolicyDecision, decide_crossing, + participant_information_state, policy_noninterference_holds, project_history, + reactive_policy_noninterference_holds, ) from tools.check_behavioral_relation_claims import _validate_claim_text -BASE_POLICY = PolicyRevision( +BASE_POLICY = ProjectionPolicyDecision( policy_id="participant-egress", revision="rev1", - effective_order=0, + decision_ref="policy-decisions.participant-egress.cut-1", + decision_cut_ref="state-cuts.1", visible_low_refs=frozenset({"status"}), permitted_declassifications=frozenset(), ) @@ -38,6 +43,8 @@ def _crossing(**overrides: object) -> Crossing: "source_ref": "status", "value": "ready", "policy_revision": "rev1", + "policy_decision_ref": "policy-decisions.participant-egress.cut-1", + "decision_cut_ref": "state-cuts.1", "authorized": True, "admitted": True, "visible": True, @@ -53,12 +60,13 @@ def _crossing(**overrides: object) -> Crossing: def test_catalog_publishes_revisioned_policy_noninterference_claim_surface(): catalog = load_behavioral_relation_catalog() - assert catalog.taxonomy_revision == "rev2" + assert catalog.taxonomy_revision == "rev3" relation = catalog.relations["policy-noninterference"] assert relation.projection_required is True assert relation.quantification.states assert relation.quantification.traces assert relation.quantification.schedulers + assert "adaptive low participant strategies" in relation.quantification.strategies assert relation.quantification.environments assert relation.dimensions.nondeterminism.status == "supported" assert relation.dimensions.probability.status == "outside-scope" @@ -68,6 +76,8 @@ def test_catalog_publishes_revisioned_policy_noninterference_claim_surface(): assert relation.assurance.proof_status == "deliberately-unproved" assert { "fagin-halpern-moses-vardi-1995", + "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "clarkson-schneider-2010", "goguen-meseguer-1982", "milner-1980", "sabelfeld-sands-2009", @@ -106,17 +116,26 @@ def test_unauthorized_high_variation_is_purged_from_projected_histories(): assert policy_noninterference_holds( left_runs=((low, high_left),), right_runs=((low, high_right),), - policies=(BASE_POLICY,), + policy_decisions=(BASE_POLICY,), participant="alice", audience="participant:alice", ) -def test_governed_declassification_changes_low_history_only_at_effective_order(): - future_policy = PolicyRevision( +def test_governed_declassification_changes_low_history_only_at_its_exact_state_cut(): + before_policy = ProjectionPolicyDecision( + policy_id="participant-egress", + revision="rev1", + decision_ref="policy-decisions.participant-egress.cut-3", + decision_cut_ref="state-cuts.3", + visible_low_refs=frozenset({"status"}), + permitted_declassifications=frozenset(), + ) + future_policy = ProjectionPolicyDecision( policy_id="participant-egress", revision="rev2", - effective_order=4, + decision_ref="policy-decisions.participant-egress.cut-4", + decision_cut_ref="state-cuts.4", visible_low_refs=frozenset({"status"}), permitted_declassifications=frozenset({"hidden-answer"}), ) @@ -125,6 +144,8 @@ def test_governed_declassification_changes_low_history_only_at_effective_order() source_ref="hidden-answer", value="secret", policy_revision="rev1", + policy_decision_ref=before_policy.decision_ref, + decision_cut_ref=before_policy.decision_cut_ref, visible=False, declassification_authorized=True, ) @@ -133,22 +154,25 @@ def test_governed_declassification_changes_low_history_only_at_effective_order() source_ref="hidden-answer", value="released", policy_revision="rev2", + policy_decision_ref=future_policy.decision_ref, + decision_cut_ref=future_policy.decision_cut_ref, declassification_authorized=True, ) assert project_history( (before, after), - (BASE_POLICY, future_policy), + (before_policy, future_policy), participant="alice", audience="participant:alice", ) == ((4, "hidden-answer", "released"),) def test_future_policy_revision_cannot_retroactively_authorize_a_crossing(): - future_policy = PolicyRevision( + future_policy = ProjectionPolicyDecision( policy_id="participant-egress", revision="rev2", - effective_order=10, + decision_ref="policy-decisions.participant-egress.cut-10", + decision_cut_ref="state-cuts.10", visible_low_refs=frozenset({"status", "hidden-answer"}), permitted_declassifications=frozenset({"hidden-answer"}), ) @@ -156,13 +180,24 @@ def test_future_policy_revision_cannot_retroactively_authorize_a_crossing(): order=9, source_ref="hidden-answer", policy_revision="rev2", + policy_decision_ref=future_policy.decision_ref, + decision_cut_ref="state-cuts.9", declassification_authorized=True, ) assert decide_crossing(earlier, (BASE_POLICY, future_policy)) is Decision.WITHHELD -def test_observability_is_participant_audience_policy_and_order_relative(): +def test_an_equal_scalar_order_cannot_substitute_for_an_incomparable_state_cut(): + incomparable = _crossing( + decision_cut_ref="state-cuts.concurrent-right", + policy_decision_ref=BASE_POLICY.decision_ref, + ) + + assert decide_crossing(incomparable, (BASE_POLICY,)) is Decision.WITHHELD + + +def test_observability_is_participant_audience_policy_and_exact_cut_relative(): crossing = _crossing(participant="alice", audience="team:red") assert ( @@ -257,16 +292,124 @@ def test_set_based_nondeterminism_compares_all_bounded_projected_histories(): assert policy_noninterference_holds( left_runs=((low,), (low, high)), right_runs=((low,),), - policies=(BASE_POLICY,), + policy_decisions=(BASE_POLICY,), participant="alice", audience="participant:alice", ) assert not policy_noninterference_holds( left_runs=((low,), (low, extra_low)), right_runs=((low,),), - policies=(BASE_POLICY,), + policy_decisions=(BASE_POLICY,), + participant="alice", + audience="participant:alice", + ) + + +def test_adaptive_low_strategy_is_unchanged_by_undelivered_high_variation(): + low = _crossing() + high_left = _crossing(order=2, source_ref="hidden-answer", value="left-secret", visible=False) + high_right = _crossing(order=2, source_ref="hidden-answer", value="right-secret", visible=False) + + def choose(history): + return "inspect" if any(value == "left-secret" for _, _, value in history) else "continue" + + assert reactive_policy_noninterference_holds( + left_runs=((low, high_left),), + right_runs=((low, high_right),), + policy_decisions=(BASE_POLICY,), + participant="alice", + audience="participant:alice", + strategies=(choose,), + memory_scope=ParticipantMemoryScope.PERSISTENT_ACROSS_EPISODES, + memory_reset_authority_ref=None, + ) + + +def test_delivered_high_variation_can_change_an_adaptive_strategy_choice_and_refutes_the_bounded_relation(): + leaky_policy = ProjectionPolicyDecision( + policy_id="participant-egress", + revision="rev-leaky", + decision_ref="policy-decisions.participant-egress.leaky", + decision_cut_ref="state-cuts.leaky", + visible_low_refs=frozenset({"status", "hidden-answer"}), + permitted_declassifications=frozenset(), + ) + low = _crossing( + policy_revision=leaky_policy.revision, + policy_decision_ref=leaky_policy.decision_ref, + decision_cut_ref=leaky_policy.decision_cut_ref, + ) + high_left = _crossing( + order=2, + source_ref="hidden-answer", + value="left-secret", + policy_revision=leaky_policy.revision, + policy_decision_ref=leaky_policy.decision_ref, + decision_cut_ref=leaky_policy.decision_cut_ref, + ) + high_right = _crossing( + order=2, + source_ref="hidden-answer", + value="right-secret", + policy_revision=leaky_policy.revision, + policy_decision_ref=leaky_policy.decision_ref, + decision_cut_ref=leaky_policy.decision_cut_ref, + ) + + def choose(history): + return "inspect" if any(value == "left-secret" for _, _, value in history) else "continue" + + left_history = project_history( + (low, high_left), + (leaky_policy,), + participant="alice", + audience="participant:alice", + ) + right_history = project_history( + (low, high_right), + (leaky_policy,), + participant="alice", + audience="participant:alice", + ) + assert choose(left_history) == "inspect" + assert choose(right_history) == "continue" + assert not reactive_policy_noninterference_holds( + left_runs=((low, high_left),), + right_runs=((low, high_right),), + policy_decisions=(leaky_policy,), participant="alice", audience="participant:alice", + strategies=(choose,), + memory_scope=ParticipantMemoryScope.PERSISTENT_ACROSS_EPISODES, + memory_reset_authority_ref=None, + ) + + +def test_reset_does_not_erase_persistent_information_state_without_memory_reset_authority(): + prior = ((1, "status", "remembered"),) + current = ((1, "status", "new-episode"),) + + assert participant_information_state( + current, + prior_delivered_history=prior, + memory_scope=ParticipantMemoryScope.PERSISTENT_ACROSS_EPISODES, + memory_reset_authority_ref=None, + ) == (*prior, *current) + with pytest.raises(ValueError, match="authoritative reset"): + participant_information_state( + current, + prior_delivered_history=prior, + memory_scope=ParticipantMemoryScope.EPISODE_LOCAL_RESET, + memory_reset_authority_ref=None, + ) + assert ( + participant_information_state( + current, + prior_delivered_history=prior, + memory_scope=ParticipantMemoryScope.EPISODE_LOCAL_RESET, + memory_reset_authority_ref="memory-reset-authorities.alice", + ) + == current ) diff --git a/specs/formal/behavioral-relations/README.md b/specs/formal/behavioral-relations/README.md index c50065626..e052c8527 100644 --- a/specs/formal/behavioral-relations/README.md +++ b/specs/formal/behavioral-relations/README.md @@ -12,15 +12,17 @@ weaker observation from being reported as a stronger behavioral result. The machine-readable authority is `contracts/concept-authority/behavioral-relations-v1.json`, contract `behavioral-relations/v1`, taxonomy `aces-behavioral-relations`, revision -`rev2`. Relation identifiers, formal dimensions, claim-surface defaults, +`rev3`. Relation identifiers, formal dimensions, claim-surface defaults, bibliography coordinates, assurance status, and worked transition systems are -normative there. ADR-081 governs the architecture. This document is the +normative there. ADR-081 and ADR-095 govern the architecture. This document is the normative reader-facing formalization of that catalog. -Revision `rev2` adds the SEM-230 `policy-noninterference` relation and its -dedicated participant-information-flow claim surface. The JSON contract remains -`behavioral-relations/v1` because its closed shape is unchanged. Revision `rev1` -is historical taxonomy identity; current in-repository producers bind `rev2`. +Revision `rev3` makes SEM-230 `policy-noninterference` reactive over adaptive +low participant strategies and exact state cuts, and adds +`io-alternating-refinement` for actionable backend participant semantics. The +JSON contract remains `behavioral-relations/v1` because its closed shape is +unchanged. Revisions `rev1` and `rev2` are historical taxonomy identities; +current in-repository producers bind `rev3`. The taxonomy defines claim vocabulary and proof obligations. It does not add a model checker, theorem prover, stochastic simulator, game solver, scheduler, @@ -80,7 +82,8 @@ identifier rather than an artifact-local synonym. | `strong-bisimulation` | behavioral | Every labelled step is matched in both directions without hiding actions. | | `weak-bisimulation` | behavioral | Both directions match visible steps while admitting governed hidden-action closure. | | `participant-projected-history-equivalence` | behavioral | Two histories are equal after the same named participant projection. | -| `policy-noninterference` | behavioral | Unauthorized high-input variation preserves the support set of participant-visible histories under fixed low-equivalence, dynamic purge, declassification, policy, scheduler/environment, and order assumptions. | +| `policy-noninterference` | behavioral | Unauthorized high variation preserves participant-visible history support sets for every adaptive low strategy under fixed memory, low-equivalence, exact-cut policy, declassification, scheduler/environment, and order assumptions. | +| `io-alternating-refinement` | behavioral | A concrete backend preserves abstract participant inputs/outputs, ownership, and declared availability obligations against quantified environment choices. | | `epistemic-indistinguishability` | epistemic | Two worlds are indistinguishable to a named participant under an information model. | | `alternating-strategic-equivalence` | strategic | Named coalitions preserve abilities against quantified opponent choices. | | `probabilistic-bisimulation` | behavioral | Related states assign equal probability mass to related equivalence classes. | @@ -168,11 +171,15 @@ property tests. They do not establish `data-refinement`, simulation, ### Backend realization Envelope admission establishes `realization-envelope-membership`. The intended -universal runtime obligation is projection-bound `trace-inclusion`, but it is -deliberately unproved in revision `rev2`. Current conformance reports establish -only `bounded-probe-success` for their named fixture and target-probe cases. -Provisioning success, snapshots, witnesses, and negative probes do not establish -reverse inclusion, equivalence, simulation, or bisimulation. +universal soundness obligation is projection-bound `trace-inclusion`. +Actionable participant interaction also requires declared input/output +ownership and action-availability obligations, represented by +`io-alternating-refinement`; trace inclusion alone permits refusal of required +inputs. Both remain deliberately unproved in revision `rev3`. Current +conformance reports establish only `bounded-probe-success` for named fixture +and target-probe cases. Provisioning success, snapshots, witnesses, and +negative probes do not establish reverse inclusion, equivalence, simulation, +alternating refinement, or bisimulation. ### Backend comparison @@ -194,9 +201,10 @@ comparison. ### Participant information-flow policy The SEM-230 claim surface uses `policy-noninterference` only when participant, -episode scope, audience, policy-revision sequence, low-equivalence relation, -dynamic purge, permitted declassification schedule, scheduler/environment -classes, order model, and observation projection are fixed. The baseline is +episode and memory scope, audience, exact-cut policy-decision sequence, +low-equivalence relation, adaptive low-strategy class, dynamic purge, permitted +declassification schedule, scheduler/environment classes, order model, and +observation projection are fixed. The baseline is termination- and progress-insensitive, untimed, and set-based under nondeterminism. Partial-order claims compare the declared visible order relation, not one linearization. Probability measures are outside the baseline. @@ -214,7 +222,7 @@ provide structural and finite evidence only. A future strategic claim MUST use availability, opponent quantification, information sets, schedulers, objectives, and preserved abilities. A probabilistic claim MUST use `probabilistic-bisimulation` and supply the probability kernel and equivalence -classes. Neither relation is implemented or proved in revision `rev2`. +classes. Neither relation is implemented or proved in revision `rev3`. ### Independent adequacy studies @@ -248,7 +256,20 @@ state-relation obligation; matching one visible trace does not prove it. The executable versions of both examples are embedded in the revisioned catalog and checked by `implementations/python/tests/test_behavioral_relations.py`. -## Assurance Boundary For Revision 2 +### Trace inclusion is not participant-input availability + +Let an abstract decision-epoch-zero state accept participant input +`select:scan`, while a concrete backend refuses every input. The concrete +projected trace set containing only the empty trace is included in the +abstract empty-trace prefix, yet the abstract input is unavailable in the +concrete state. Trace inclusion therefore cannot establish actionable +participant realization. The executable finite counterexample also checks a +hidden concrete projection step followed by `deliver:decision-epoch-0`: the +visible trace can weakly match while strong bisimulation fails. Neither finite +case proves `io-alternating-refinement`; each can falsify an incorrect +implication used in a backend claim. + +## Assurance Boundary For Revision 3 Implemented and tested now: @@ -258,13 +279,15 @@ Implemented and tested now: - canonical artifact identity; - realization-envelope membership and subsumption; and - participant projection machinery and bounded projected-history comparisons; - and - the SEM-230 relation definition, catalog/claim-policy validation, and bounded - test-local counterexamples. + reactive-strategy counterexamples; and +- bounded decision-epoch-zero step-matching and participant-input-availability + counterexamples for `io-alternating-refinement`. Defined but deliberately unproved or only partially implemented: -- universal `trace-inclusion` for backend realization; +- universal `trace-inclusion` and `io-alternating-refinement` for backend + realization; - `trace-equivalence`, forward/backward simulation, and data refinement; - strong and weak bisimulation; and - universal `policy-noninterference`, production policy enforcement, and @@ -283,7 +306,8 @@ artifacts: ## Primary Sources The catalog records the complete title, authors, publication year and venue, -edition/version, and immutable DOI or ISBN for each source. Revision `rev2` +edition/version, and immutable DOI, ISBN, or primary publication URL for each +source. Revision `rev3` uses, among others: - Milner, *A Calculus of Communicating Systems* (1980), @@ -310,6 +334,12 @@ uses, among others: `10.1109/SP.1982.10014`; and - Sabelfeld and Sands, “Declassification: Dimensions and Principles” (2009), DOI `10.3233/JCS-2009-0352`. +- Lynch and Tuttle, “An Introduction to Input/Output Automata” (1989), *CWI + Quarterly* 2(3), 219-246; +- Clarkson and Schneider, “Hyperproperties” (2010), *Journal of Computer + Security* 18(6), 1157-1210; and +- Bohannon, Pierce, Sjöberg, Weirich, and Zdancewic, “Reactive + Noninterference” (2009), DOI `10.1145/1653662.1653673`. Bibliographic prose here is an aid. The machine-readable catalog is the revision-pinned identity surface. diff --git a/specs/formal/participant-runtime/README.md b/specs/formal/participant-runtime/README.md index acfb2dd38..84dec634e 100644 --- a/specs/formal/participant-runtime/README.md +++ b/specs/formal/participant-runtime/README.md @@ -262,6 +262,34 @@ or downgraded by the declared enforcement point. only the emitted observation, a history-consistent reconstruction, a perfect-recall history, a lossy projection, unknown, or unsupported. +`Decision epoch` +: A zero-based participant choice opportunity within one episode. It is not a + lifecycle-history index, behavior-history index, policy order, delivery + order, or backend scheduler step. + +`Decision state cut` +: The exact total-order prefix or downward-closed causal frontier from which a + participant decision view is derived. A sequence cut and a causal cut are + distinct order models; a maximum scalar index cannot resolve an incomparable + causal frontier. + +`Participant decision view` +: The participant-available context, action entries, affordances, form, and + semantic limitations for one participant, episode, and decision epoch. The + view excludes trusted derivation, policy, evidence, provenance, and delivery + material. + +`Participant decision assurance` +: The trusted derivation anchor, exact-cut projection/exposure decisions, + canonical view digest, evidence, provenance, and participant-memory scope for + one decision view. + +`Participant memory scope` +: Either `episode_local_reset`, which requires an authoritative reset of every + participant-visible memory channel, or `persistent_across_episodes`. Episode + reset/restart alone changes episode identity and restarts decision epoch; it + does not prove forgetting. + `Step signal` : A participant-visible or evaluator-visible signal produced at an action step: observation, reward, return, action mask, termination, truncation, or @@ -3860,18 +3888,35 @@ to the supported conclusion scope or explicitly record ## Refinement And Conformance Obligations -The intended universal relation is `trace-inclusion`: under the named -participant observation projection, every admitted concrete backend trace must -map to a valid abstract RAES trace. This section defines that obligation; it -does not establish it. Current executable evidence is bounded to named fixtures -and target probes, so no simulation, data-refinement, trace-equivalence, or -bisimulation claim follows. The evidence boundary for each executed check must -be carried by its conformance report. +The intended universal soundness relation includes `trace-inclusion`: under +the named participant observation projection, every admitted concrete backend +trace must map to a valid abstract RAES trace. Actionable participant +realization additionally requires `io-alternating-refinement`: participant and +environment inputs, participant-facing outputs, action ownership, availability, +fairness, exact-cut derivation, and delivery must satisfy their declared +alternating obligations. Trace inclusion alone is insufficient because a +backend that refuses every participant input can have a trivially included +trace set. + +This section defines those obligations; it does not establish them. Current +executable evidence is bounded to named fixtures, target probes, and finite +counterexamples, so no universal simulation, data-refinement, alternating +refinement, trace-equivalence, or bisimulation claim follows. Bisimulation is +optional and projection-relative when separately claimed; it is not the +default backend-conformance relation. The evidence boundary for each executed +check must be carried by its conformance report. Required preservation properties: - participant, episode, action, operation, observation, state, joint-action, and evidence identity; +- decision epoch independent of its derivation state cut, with epoch zero + grounded in authoritative `episode_running` state and empty current-episode + behavior history; +- participant-view, assurance, projection, disclosure, delivery, selection, + admission, attempt, result, and outcome separation; +- exact-cut policy and authorization resolution plus delivery-before-selection; +- declared participant-memory scope across reset and restart; - append-only history and monotonic participant sequence numbers; - lifecycle phase, phase realization, admission disposition, and operation state vocabulary; diff --git a/specs/formal/participant-semantics/README.md b/specs/formal/participant-semantics/README.md index c1cce208b..21b188031 100644 --- a/specs/formal/participant-semantics/README.md +++ b/specs/formal/participant-semantics/README.md @@ -1406,72 +1406,86 @@ values cannot enter an instantiated scenario. The executable oracle is `SEM-220` requires explicit semantics for open-ended action generation, constrained action forms, candidate-action sets, and their selection meaning. -For participant `p`, episode `e`, and observation/order point `o`, define -the decision surface as the participant-local projection: +ADR-095 revises the executable coordinate system without changing the three +selection forms. For participant `p`, episode `e`, decision epoch `k`, runtime +state `q`, participant/audience `a`, exact policy decision `r_c`, and state cut +`c`, define: ```text -D(p, e, o) = Project( +D(p, e, k) = Pi[p, a, r_c, c]( + q, behavior and action-contract refs, - V(p, o) and observation-boundary state, - participant context and audience scope, + V(p, c) and observation-boundary state, + participant context, participant-implementation selection and decision-control mode, - exposure policy, SEM-211 eligibility state, - realized affordance/support disclosures, - evidence, provenance, marking, redaction, and limitations + affordance/support disclosures, + marking, redaction, and participant-visible limitations ) ``` -The projection carries stable references and relation state. It does not copy -world truth, raw policy bodies, hidden prompts, evaluator state, credentials, -or backend-native objects into a participant-visible payload. - -Every surface has: - -- participant address, episode id, and observation/order point; -- surface form and its selection interpretation; -- behavior, action-contract, observation-boundary, context-view, - implementation-selection, and exposure-policy refs; -- visible context refs and their source/transformation/disclosure bases; -- action entries with presentation or generation basis, visibility, - eligibility, constraint, and support/realization disclosures; -- affordance refs bound to action contracts and observation effects; -- evidence/provenance, markings, redaction, limitations, and weakening; and -- the event/order/evidence anchor from which the surface was derived. - -The event/order/evidence anchor is a closed tagged value, not a mixed history. -An `episode_readiness` anchor resolves the current RUN-311 `episode_running` -event from the trusted runtime snapshot and complete participant lifecycle -history. It grounds compiled `V_p,0`, the initial context, and `D(p,e,0)` while -the new episode's behavior history remains empty. A `behavior_event` anchor -resolves one exact action-linked event and the complete participant/episode -behavior-history prefix used by the existing effective-view-relation -algorithm. - -The anchor keeps three order domains explicit: - -- RUN-311 `sequence_number` identifies the episode generation only; -- `decision_surface_order`, carried as the surface `observation_order`, numbers - `D(p,e,0)`, `D(p,e,1)`, and subsequent surfaces within that episode; and -- `anchor_order` identifies the referenced event in its tagged lifecycle or - behavior history without changing the meaning of `observation_order`. - -The readiness resolver derives decision-surface order zero. A later surface is -anchored by the exact current terminal `observation_emitted` event, and its -decision-surface order equals the number of completed observation events in the -episode. The value is resolved from runtime history rather than supplied as -caller-authored metadata. - -For a new episode, `episode_initialized`, `episode_reset`, or -`episode_restarted` precedes `episode_running`; readiness then precedes the -initial context and surface. Proposal and selection follow the surface but do -not create participant behavior. Admission creates the first -`action_attempted`; its state transition and terminal observation precede the -next behavior-anchored surface. Reset and restart create a new episode id and -restart `decision_surface_order` at zero. Projection and admission both -re-resolve an anchor against current runtime authority, so a standalone, -truncated, previous-episode, terminated, behavior-superseded, or absent runtime -admission anchor fails closed. +`k` is the zero-based participant choice opportunity. `c` is the exact total +prefix or causal frontier from which the view is derived. Lifecycle generation, +behavior-history index, policy-effective cut, derivation anchor, disclosure +decision, delivery occurrence, and participant observation are independently +typed coordinates. Equality of integer values never merges their meanings. + +`participant-decision-surface-v1` retains its historical meaning: +`observation_order` indexes the supplied time-indexed participant behavior +history. It remains valid for historical data and is not relabelled or admitted +through the v2 path. `participant-decision-surface-v2` is the actionable +contract and has no `observation_order`. + +V2 separates three trust planes: + +- `participant_view` is the complete low payload actually eligible for + participant delivery: surface/participant/episode identity, `decision_epoch`, + information-state/context refs, visible context, action entries, + affordances, form, markings, redaction, and disclosed limitations; +- `assurance` carries the exact derivation state cut, lifecycle or terminal + observation anchor, policy decision, apparatus/boundary refs, per-item + authorization, participant-memory scope and reset authority when applicable, + evidence, provenance, and canonical RFC 8785 digest of the participant view; + and +- `delivery` records the trusted occurrence by which that exact digest became + available to the participant, including delivery basis, delivery cut, + delivery authorization/policy decision, observation ref, evidence, + provenance, and limitations. + +Assurance metadata is not participant-visible merely because it accompanies +the same surface artifact. Event ids, anchor order, prefix length, policy +decision ids, authorization records, evidence topology, provenance, rejection +detail, entry ordering, refresh behavior, and surface identity can convey +information and must be included in the participant projection only when an +independent SEM-226 decision authorizes them. + +The reactive sequential baseline is: + +```text +episode_running + -> derive and authorize projected D(p,e,0) from V(p,initial-cut) + -> disclose -> deliver -> participant observes + -> participant selects/proposes from the delivered digest + -> validate -> admit -> action_attempted + -> state_transition_recorded -> terminal observation_emitted + -> derive D(p,e,1) from that exact behavior cut +``` + +Projection is not disclosure; disclosure is not delivery; delivery is not +acknowledgement or interpretation; presentation is not selection; selection is +not admission; admission is not attempt, result, or outcome. A projected +surface is valid assurance data but cannot be selected. A v2 selection binds +surface id, decision epoch, participant-view digest, and delivery ref. +Admission re-resolves both derivation and delivery before behavior can be +written. + +`episode_running` grounds epoch zero while current-episode behavior history is +empty. Later epochs equal the number of completed terminal participant +observations, but their state cuts retain the complete behavior prefix and its +own anchor order. Reset and restart create a new episode and epoch zero. They +do not erase a persistent human, agent, controller, or shared-memory +participant history unless an explicit `episode_local_reset` memory authority +resets every participant-visible channel. The three surface forms have distinct selection meaning: @@ -1516,16 +1530,17 @@ This requirement refines the existing time-indexed `V_p,t`, view-rule, view-transition, observation-boundary, context-view, and audience-view semantics. It introduces no parallel visibility taxonomy. -For item `x`, participant `p`, episode `e`, and order point `o`: +For item `x`, participant `p`, episode `e`, audience `a`, and exact state cut +`c`: ```text -Exposed(x, p, e, o) only if - x is admitted by V(p, o) +Exposed(x, p, e, a, c) only if + x is admitted by V(p, c) and its source layer and transformation are participant-facing - and its audience/role scope includes p + and its audience/role scope includes (p, a) and its marking, redaction, withholding, and loss rules are satisfied - and the selected exposure policy authorizes the disclosure class - and any visibility change has an event/order/evidence anchor at or before o + and the exact policy decision at c authorizes the disclosure class + and the item authorization is bound to c ``` The conjunction is fail closed. Backend reachability, operating scope, @@ -1548,11 +1563,14 @@ Augmentation names its source, transformation, audience, visibility basis, evidence/provenance, marking/redaction, and limitations. A generic metadata or context map is not an exposure authority. -Exposure is participant-local and time-indexed. A visibility transition -changes surfaces at or after its effective order; future disclosure cannot -justify an earlier surface. When participants have different boundaries, -roles, or transition histories, they may receive different surfaces for the -same world event without semantic inconsistency. +Exposure is participant-local and state-cut-indexed. A future or incomparable +policy decision cannot authorize an earlier cut. A decision epoch is not a +policy order. Delivery authority is resolved again at the delivery cut; a +derivation-time authorization cannot be carried forward merely because +delivery occurs in the same epoch. Unknown, stale, cross-cut, cross-policy, or +incomparable authority fails closed. When participants have different +boundaries, roles, policy cuts, or transition histories, they may receive +different surfaces for the same world event without semantic inconsistency. Realized exposure is separately evidenced. A manifest capability, selected mode, or exposure-policy ref can explain intent and apparatus support, but @@ -1570,13 +1588,13 @@ The joint model preserves meaning across stages: ambiguous selection meaning, incomplete constraints, or conflicting visibility bases; - **compilation** emits canonical participant/action/observation addresses and - the inputs required to derive `D(p,e,o)`; + the inputs required to derive `D(p,e,k)` from an exact state cut; - **planning** validates selected implementation/backend support and records declared weakening before execution; - **execution** applies existing SEM-211 admission and records behavior history, results, and visibility transitions; - **observation/retrieval** derives participant-local surfaces from the - applicable `V_p,o` snapshot rather than global or final state; and + applicable `V_p,c` state cut rather than global or final state; and - **conformance** compares authored, compiled, selected, realized, and evidenced facts and reports disagreement through existing diagnostics. @@ -1600,18 +1618,18 @@ must preserve or strengthen its rows. | SEM-219 E: constraints fail closed | affordance action refs plus unchanged SEM-211 preconditions/failure classes | semantic validation and existing planner/admission/result gates | complete action constraints remain reachable through the compiled action address | binding copies, drops, or overrides exhausted/unknown constraints | I4, I7 / #294 | | SEM-219 F: support is apparatus metadata | authored affordance IR remains separate from manifest/selection support | absence-preserving compilation plus existing apparatus validation | support can be joined later without changing authored meaning | installed content or backend support creates an affordance grant | I11, I12 / #294 | | SEM-219 G: side effects and observations are explicit | affordance observation addresses plus action effects/evidence expectations | boundary classification, compiler IR, existing result/snapshot/conformance gates | tool output remains governed by referenced observation/effect contracts | tool output lacks a view rule or leaks hidden truth | I5, I13 / #294 | -| SEM-220 A: surface has participant/episode/order identity | `ParticipantContextViewModel` envelope, typed `D(p,e,o)` payload/ref, and tagged episode-readiness or behavior-event projection anchor | trusted runtime-snapshot/history resolution, projection, admission-time freshness validation, and context-view validation | `episode_running` grounds `V_p,0` and `D(p,e,0)` without behavior; later surfaces resolve one exact behavior prefix | cumulative/global context, a standalone lifecycle event, or a stale prior-episode surface substitutes for current participant-local state | I1, I3, I15 / #295, #909 | +| SEM-220 A: decision epoch and derivation cut remain distinct | `ParticipantDecisionSurfaceV2Model`, typed readiness/behavior anchor, and sequence/causal state cut | trusted snapshot/history resolution at projection and admission | `episode_running` grounds epoch zero with empty behavior; epoch one carries behavior anchor order two after one three-occurrence action | lifecycle, behavior, policy, delivery, or decision coordinates are collapsed into one scalar | I1, I3, I15 / #295, #909 | | SEM-220 B: candidate membership is not eligibility | action-entry contract ref plus explicit SEM-211 eligibility state/reason refs | surface derivation followed by independent admission | visible candidate is marked ineligible with a typed reason | every presented candidate is implicitly executable | I4 / #295 | | SEM-220 C: open-ended proposals bind before admission | compiled `ParticipantActionContractRuntime.argument_shape_ref`, `ParticipantValidatedActionSelection`, and SEM-211 admission helper | proposal resolution, concrete argument validation/normalization, immutable carrier binding, then runtime admission | generated proposal resolves and validates before an attempt | free-form generation bypasses applicability or invents backend-local meaning | I4, I11 / #295, #303 | | SEM-220 D: constrained forms preserve mapping meaning | `ParticipantActionArgumentDefinition`, canonical compiled shape identity, and explicit default/normalization/omission/loss disclosure | closed authoring validation, compiler mapping, `resolve_participant_action_arguments()`, and conformance comparison | form values map deterministically to validated action arguments | omitted/defaulted field changes meaning without disclosure | I12, I14, I16 / #295, #303 | -| SEM-220 E: selection is separate from attempt and outcome | decision record, behavior-history attempt, action result, outcome interpretation | execution history and result/outcome validators | chosen candidate links to one admitted attempt and later result | surface appearance is recorded as selection or success | I10 / #295 | -| SEM-220 F: implementation kind does not change semantics | participant implementation manifest/selection and stable surface refs | apparatus validation and cross-run conformance | human proxy and autonomous implementation realize equivalent refs with disclosed differences | implementation type silently changes action or selection meaning | I1, I11, I12, I15 / #295 | -| SEM-226 A: exposure is scoped by `V_p,o` | compiled view-relation timeline, observation boundary, resolved policy history, and `ParticipantDecisionSurfaceExposureBindingModel` | `project_participant_decision_surface()` resolves the effective revision and evaluates the compiled relation at the exact history order | disclosed item appears only from its effective order | caller-supplied, stale, or future policy/visibility state enters an earlier surface | I2, I3 / #296 | +| SEM-220 E: delivery, selection, admission, attempt, and outcome are separate | projected/delivered lifecycle, canonical participant-view digest, delivery record, v2 selection, behavior history, action result, outcome interpretation | delivery-time authority, admission-time anchor/delivery re-resolution, then existing execution validators | exact delivered digest is selected and admitted once before attempt/result | projected, stale, reset, replayed, forged, or undelivered surface creates behavior | I10 / #295, #909 | +| SEM-220 F: implementation kind does not change semantics | participant implementation manifest/selection, explicit `participant-decision-surface-v2` support, and stable surface refs | capability declaration plus exact-cut apparatus validation and cross-run conformance | human proxy and autonomous implementation realize equivalent refs with disclosed differences | implementation type or undeclared v2 consumption silently changes action or selection meaning | I1, I11, I12, I15 / #295, #909 | +| SEM-226 A: exposure is scoped by `V_p,c` | compiled view relation, observation boundary, exact-cut policy decision, and `ParticipantDecisionSurfaceExposureBindingV2Model` | `project_participant_decision_surface_v2()` resolves policy and item authority at the derivation cut | disclosed item is authorized at the exact cut independently of decision epoch | caller-supplied, stale, future, cross-cut, or incomparable policy state enters a view | I2, I3 / #296, #909 | | SEM-226 B: source strata remain distinct | `ParticipantContextViewModel.source_layers` plus resolved authorization-record source/result, transformation, marking, and provenance refs | trusted item-authorization resolution followed by the deny-first exposure selector | archival evidence is mediated through an authorized participant-facing transformation with inherited markings and provenance | truth/adjudication/evidence payload or a self-attested transform aliases the visible context payload | I2, I3, I13 / #296 | | SEM-226 C: role/audience scope is explicit | context-view audience fields plus resolved authorization participant, episode, audience, order, apparatus, and policy coordinates | exact authorization/surface agreement and separately resolved implementation-selection checks before serialization | role-scoped context reaches only the intended participant audience | private or role-specific context appears through a synthetic selection or another participant's authorization | I2, I17 / #296 | | SEM-226 D: augmentation is governed exposure | resolved authorization source layer, transformation, visibility basis, backend-support ref, evidence/provenance, and limitations | authorization resolver, deny-first item exposure selector, and context-view validation | augmentation records source, authorized transformation, disclosure basis, and limits | scaffold guidance or augmentation metadata enters through caller-owned gate booleans | I3, I13, I17 / #296 | -| SEM-226 E: exposure changes are anchored | `ParticipantViewTransition`, resolved policy-revision effective order, immutable exposure-policy version/digest, behavior-history anchor, and authorization evidence refs | compiler ordering plus authoritative policy and exact observation-occurrence resolution at both surface and delivery order | disclosure transition changes later surfaces with evidence while revocation leaves prior surfaces intact | a stale authorization is replayed under a replaced same-id policy or exposure changes without a history event, order, or evidence anchor | I2, I8, I9 / #296 | -| SEM-226 F: realized exposure is not inferred from policy | resolved exposure-policy coordinates plus optional `ParticipantDecisionSurfaceExposureRealizationModel` item, authorization-record, and occurrence binding | semantic occurrence-identity resolution, independent of sequence position, followed by exact item/authorization and participant/episode/action/boundary/order/evidence/provenance agreement with observation history and delivery-time authority | selected policy and the exact authorized item occurrence agree, with limitations | an unrelated observation, positional list entry, or later authorization is attached to another item's realization | I11, I13, I15 / #296 | +| SEM-226 E: exposure changes are cut-anchored | exact state cut, policy-decision ref, immutable exposure-policy version/digest, derivation anchor, and authorization evidence | exact-cut policy/authorization resolution at derivation plus fresh delivery-cut authority | revocation prevents later delivery without retroactively erasing an earlier delivery | decision epoch or a later policy revision is used as authorization order | I2, I8, I9 / #296, #909 | +| SEM-226 F: delivery is not inferred from projection or disclosure | `ParticipantDecisionSurfaceDeliveryV2Model` and delivered lifecycle state | delivery resolver at transition and again at admission, bound to participant-view digest | trusted emission-is-delivery or transport occurrence makes the exact view actionable | projection, policy selection, or an unrelated observation is treated as delivery | I11, I13, I15 / #296, #909 | ### Adversarial counterexamples @@ -1655,7 +1673,7 @@ and order-relative label projection, independent control and information-flow operations, dynamic purge and declassification semantics, and the exact baseline `policy-noninterference` obligation. -The relation is bound through taxonomy revision `rev2` rather than a local +The relation is bound through taxonomy revision `rev3` rather than a local registry. Its current assurance is definition-complete and bounded-tested but deliberately unproved. The test-local model can falsify finite cases; it is not runtime mediation, backend realization, or a universal information-flow proof. diff --git a/specs/formal/participant-semantics/information-flow-control.md b/specs/formal/participant-semantics/information-flow-control.md index 81e22716e..936a17720 100644 --- a/specs/formal/participant-semantics/information-flow-control.md +++ b/specs/formal/participant-semantics/information-flow-control.md @@ -4,7 +4,7 @@ Classification: FM3. Requirement: `SEM-230`. -Authority revision: `sem-230/rev1`. +Authority revision: `sem-230/rev2`. ## Scope And Authority @@ -25,6 +25,13 @@ This document is definition authority. The executable cases in bounded falsification evidence. Neither artifact is production enforcement or a universal proof. +Revision `sem-230/rev2`, adopted by ADR-095, makes decision-surface output a +first-class low observation, binds policy decisions to exact state cuts, and +quantifies the baseline over adaptive participant strategies. Revision +`sem-230/rev1` remains the historical open-loop formulation; its +`observation/order` scalar must not be reused as a v2 decision epoch or +state-cut identity. + ## Prior-Art Adaptation Rule SEM-230 does not rename settled formal concepts and present them as RAES @@ -35,12 +42,12 @@ by participant/runtime governance. | Semantic element | Formal lineage reused | RAES adaptation | Necessary extension, not reinvention | | --- | --- | --- | --- | -| participant-relative information state and low equivalence | Fagin, Halpern, Moses, and Vardi's interpreted-systems construction: points in runs are indistinguishable when the agent-local state agrees | local state is the existing `V_p,o` plus occurrence-preserving `H_{tr,rho}(p,e,o)` and every projection-visible control/policy coordinate | policy revision, audience, marking, and declared order are included because they can change the RAES projection | -| noninterference and purge | Goguen and Meseguer's policy-relative noninterference and purge treatment of high actions | high variation is evaluated against RAES crossings, and low observations are participant-projected history support sets | policy changes are evaluated at each occurrence's effective order; the baseline composes an explicit declassification schedule | -| declassification | Sabelfeld and Sands' dimensions and principles for what may be released, by whom, where, and when | release records bind source dimensions, participant/audience, actor/controller/authority, policy revision, order, markings, evidence, and provenance | RAES adds stable carrier/evidence coordinates and deny-first intersection with admission and marking; it does not redefine declassification as redaction | -| labelled transition and hidden-action treatment | Milner's labelled transition/`tau` discipline and van Glabbeek's separation of trace and branching-time relations | the SEM-230 alphabet maps each label to an existing RAES action, lifecycle, visibility, control, delivery, or evidence owner | observability is indexed by participant, audience, policy revision, and order; this definition does not claim weak or strong bisimulation | +| participant-relative information state and low equivalence | Fagin, Halpern, Moses, and Vardi's interpreted-systems construction: points in runs are indistinguishable when the agent-local state agrees | local state is the existing `V_p,c` plus occurrence-preserving `H_{tr,rho}(p,e,c)` and every projection-visible control/policy coordinate | policy decision, audience, marking, memory scope, and declared cut/order model are included because they can change the RAES projection | +| noninterference and purge | Goguen and Meseguer's policy-relative noninterference and purge treatment of high actions, extended by reactive noninterference over strategies | high variation is evaluated against RAES crossings, and low observations are participant-projected history support sets under adaptive low strategies | policy changes are evaluated at each occurrence's exact state cut; the baseline composes an explicit declassification schedule and strategy class | +| declassification | Sabelfeld and Sands' dimensions and principles for what may be released, by whom, where, and when | release records bind source dimensions, participant/audience, actor/controller/authority, policy decision, state cut, markings, evidence, and provenance | RAES adds stable carrier/evidence coordinates and deny-first intersection with admission and marking; it does not redefine declassification as redaction | +| labelled transition and hidden-action treatment | Milner's labelled transition/`tau` discipline and van Glabbeek's separation of trace and branching-time relations | the SEM-230 alphabet maps each label to an existing RAES action, lifecycle, visibility, control, delivery, or evidence owner | observability is indexed by participant, audience, exact-cut policy decision, and declared order model; this definition does not claim weak or strong bisimulation | | visible history and knowledge persistence | interpreted systems and the existing ADR-054 constructive visible-history/perfect-recall treatment | disclosure appends a stable visible occurrence to `H`; later concealment or revocation changes future projection | RAES preserves evidence/provenance and visible-order identity across rollback and supersession | -| causal and partial ordering | Lamport happened-before plus the Winskel event-structure and Mazurkiewicz trace-theory lineage already adopted by ADR-054 | SEM-230 reuses the existing `R_o`, visible partial order, and simultaneity groups | policy revisions and declassification events are located in that order; no new clock or concurrency formalism is introduced | +| causal and partial ordering | Lamport happened-before plus the Winskel event-structure and Mazurkiewicz trace-theory lineage already adopted by ADR-054 | SEM-230 reuses the existing `R_c`, visible partial order, and simultaneity groups | policy decisions and declassification events are located at exact cuts; no new clock or concurrency formalism is introduced | The indirect derivations in the final two rows are intentional: SEM-230 reuses the accepted participant-runtime authority and its formal sources instead of @@ -50,32 +57,32 @@ art and stronger evidence rather than silently strengthening this baseline. ## Existing Objects And State -For participant `p`, episode `e`, trace `tr`, policy sequence `rho`, and order -point `o`, SEM-230 reuses: +For participant `p`, episode `e`, trace `tr`, policy sequence `rho`, and exact +state cut `c`, SEM-230 reuses: -- `W_o`: world, backend, and evaluator truth; -- `V_p,o`: the participant view relation from ADR-022; -- `H_{tr,rho}(p,e,o)`: the occurrence-preserving participant-visible local +- `W_c`: world, backend, and evaluator truth; +- `V_p,c`: the participant view relation from ADR-022; +- `H_{tr,rho}(p,e,c)`: the occurrence-preserving participant-visible local history under the effective projection, marking/redaction rules, and visible order relation; -- `X_o`: archival evidence and authorized audit state; +- `X_c`: archival evidence and authorized audit state; - existing action proposals, admission records, results, observation envelopes, lifecycle histories, and evidence/provenance references; -- `R_o`: the declared total, partial, causal, simultaneous, or +- `R_c`: the declared total, partial, causal, simultaneous, or backend-serialized order relation; and -- `C_o` and `A_o`: controller coordinates and participant/actor authority - coordinates at `o`. +- `C_c` and `A_c`: controller coordinates and participant/actor authority + coordinates at `c`. The SEM-230 policy state is: ```text -Q_o = (W_o, V_p,o, H_{tr,rho}(p,e,o), X_o, - R_o, C_o, A_o, M_o, rho_o) +Q_c = (W_c, V_p,c, H_{tr,rho}(p,e,c), X_c, + R_c, C_c, A_c, M_c, rho_c) ``` -where `M_o` is the governed marking state and `rho_o` is the effective policy -revision. These coordinates remain distinct. In particular, `W_o` is not a -participant observation, `X_o` is not participant egress, and control-plane +where `M_c` is the governed marking state and `rho_c` is the exact-cut policy +decision. These coordinates remain distinct. In particular, `W_c` is not a +participant observation, `X_c` is not participant egress, and control-plane caller authorization is not participant authority. ## Revisioned Crossing Relation @@ -85,7 +92,7 @@ A crossing decision is evaluated over: ```text C = (participant, episode, audience, direction, interaction_kind, source_ref, actor, controller, authority_basis, - action_or_projection_ref, observation_point, order_point, + action_or_projection_ref, decision_epoch, state_cut_ref, order_model, policy_id, policy_revision, markings, authorization, admission, visibility, declassification, redaction_or_transformation, @@ -98,31 +105,32 @@ a payload bag. Unknown required coordinates fail closed. An owning vocabulary may represent an optional coordinate as `not-applicable`, `unknown`, `unsupported`, or disclosed loss; none of those states is implicit success. -For an attempted crossing `c` at order point `o`, let: +For an attempted crossing `x` at exact state cut `c`, let: ```text -Effective(rho, o) = the unique revision r whose effective order is - maximal among revisions not later than o +Effective(rho, c) = the unique authoritative policy decision r_c + returned for exactly c ``` -The policy sequence is valid only when revision identity and effective order -are explicit and unambiguous. A revision cannot authorize a crossing earlier -than its effective order. Receipt order, timestamp equality, last-writer-wins, -or a later final-state snapshot cannot substitute for `R_o`. +The policy sequence is valid only when decision, revision, state-cut identity, +and applicable order model are explicit and unambiguous. A later or +incomparable decision cannot authorize `c`. Decision epoch, receipt order, +timestamp equality, last-writer-wins, or a later final-state snapshot cannot +substitute for state-cut resolution. The deny-first admission predicate is: ```text -MayCross(c, Q_o) = - AuthenticatedActor(c.actor) - and TargetAuthorized(c.actor, c.source_ref) - and ParticipantAuthority(c.participant, c.controller, c.authority_basis, o) - and ApplicableAndAdmitted(c.action_or_projection_ref, o) - and VisibleTo(c.source_ref, c.participant, c.audience, V_p,o) - and MarkingAuthorized(c.markings, c.participant, c.audience, o) - and DeclassificationOK(c.declassification, Effective(rho, o)) - and BackendSupports(c, o) - and TransformationResultValid(c.redaction_or_transformation, o) +MayCross(x, Q_c) = + AuthenticatedActor(x.actor) + and TargetAuthorized(x.actor, x.source_ref) + and ParticipantAuthority(x.participant, x.controller, x.authority_basis, c) + and ApplicableAndAdmitted(x.action_or_projection_ref, c) + and VisibleTo(x.source_ref, x.participant, x.audience, V_p,c) + and MarkingAuthorized(x.markings, x.participant, x.audience, c) + and DeclassificationOK(x.declassification, Effective(rho, c)) + and BackendSupports(x, c) + and TransformationResultValid(x.redaction_or_transformation, c) ``` Each conjunct has its existing owner and evidence. No successful conjunct can @@ -162,7 +170,7 @@ audience, not participant egress. ## Labelled Transitions -The closed semantic alphabet for revision `sem-230/rev1` is: +The closed semantic alphabet for revision `sem-230/rev2` is: | Label class | State owner or incumbent | Visibility rule | | --- | --- | --- | @@ -177,7 +185,7 @@ The closed semantic alphabet for revision `sem-230/rev1` is: | delivery / observation | runtime occurrence and observation envelopes | Delivery order, observation, and acknowledgement remain distinct. | | concealment / revocation | view transition and future authority | Cannot erase an earlier visible occurrence. | | policy change | revisioned policy state | Never applies retroactively. | -| evidence / audit | archival state `X_o` | Visible only to its authorized evidence audience. | +| evidence / audit | archival state `X_c` | Visible only to its authorized evidence audience. | Let `Q -l-> Q'` mean a valid transition carrying one label from this table and its owned evidence. Labels are semantic classes; they do not authorize a new @@ -185,14 +193,14 @@ implementation-local enum or wire field in issue #796. ## Participant-Relative Projection And Hiding -For participant `p`, audience `a`, policy revision `r`, and order point `o`, +For participant `p`, audience `a`, policy decision `r_c`, and state cut `c`, define a revisioned projection: ```text -Pi[p,a,r,o] : labelled occurrence history -> visible occurrence history +Pi[p,a,r_c,c] : labelled occurrence history -> visible occurrence history ``` -An occurrence is retained only when the effective `V_p,o`, audience scope, +An occurrence is retained only when the effective `V_p,c`, audience scope, marking/declassification intersection, delivery basis, and order model retain it. Retained occurrences preserve stable occurrence identity, visible order, and simultaneity. Equal payload values never collapse repeated occurrences. @@ -200,7 +208,7 @@ and simultaneity. Equal payload values never collapse repeated occurrences. The hidden set is: ```text -Tau[p,a,r,o] = { l | Pi[p,a,r,o](l) = epsilon } +Tau[p,a,r_c,c] = { l | Pi[p,a,r_c,c](l) = epsilon } ``` Membership is participant-, audience-, policy-, and order-relative. It is not @@ -212,31 +220,32 @@ must select another governed relation and evidence it. Concealment, revocation, rollback, supersession, redaction, and later policy change alter future projection or append new visible occurrences. They never -delete or mutate an occurrence already present in `H_{tr,rho}(p,e,o)`. +delete or mutate an occurrence already present in `H_{tr,rho}(p,e,c)`. ## Low Equivalence, Dynamic Purge, And Declassification Fix participant `p`, episode scope `e`, audience `a`, policy sequence `rho`, -and initial order point `o0`. +and initial state cut `c0`. Two initial states are low-equivalent, -`q1 ~=_{p,e,a,rho,o0} q2`, exactly when these participant-policy coordinates +`q1 ~=_{p,e,a,rho,c0} q2`, exactly when these participant-policy coordinates are equal: - participant and episode identity; -- `V_p,o0`, participant-visible local history, and visible occurrence order; +- `V_p,c0`, participant-visible local history, delivered decision-surface + views, and visible occurrence order; - controller identity and participant/actor authority visible to `p`; -- effective policy identity/revision and its effective order; +- exact policy-decision identity/revision and its state-cut ref; - public markings, declassification authority visible to `p`, and declared loss/weakening state; and -- every other state coordinate that `Pi[p,a,rho,o0]` retains. +- every other state coordinate that `Pi[p,a,rho,c0]` retains. -`W_o0` and `X_o0` may differ only in coordinates classified high for this +`W_c0` and `X_c0` may differ only in coordinates classified high for this participant-policy comparison. Equality of hidden backend state is not required. Conversely, calling two states low-equivalent without naming all projection-affecting coordinates is invalid. -For an input/action history `alpha`, dynamic purge is: +For a run prefix `alpha`, dynamic purge is: ```text purge[p,e,a,rho](alpha) = @@ -244,21 +253,22 @@ purge[p,e,a,rho](alpha) = admitted low inputs, policy-change events, and permitted declassification events, - while removing unauthorized high inputs at the policy/order point where + while removing unauthorized high inputs at the exact state cut where each input was evaluated ``` -Purge is dynamic: each occurrence is evaluated against the effective revision -at its own order point. A later policy revision cannot cause an earlier high -input to be retained. A permitted declassification event records at least the -released dimensions, participant/audience, actor/controller/authority basis, -policy identity/revision, effective order, markings, evidence, and provenance. +Purge is dynamic: each occurrence is evaluated against the authoritative +policy decision at its exact state cut. A later or incomparable policy decision +cannot cause an earlier high input to be retained. A permitted declassification +event records at least the released dimensions, participant/audience, +actor/controller/authority basis, policy identity/revision/decision, exact +state-cut ref, markings, evidence, and provenance. Two declassification schedules are equal only when those governed coordinates and their visible order are equal. Merely releasing equal values is insufficient. -## Baseline Policy-Noninterference Obligation +## Baseline Reactive Policy-Noninterference Obligation Fix: @@ -266,15 +276,21 @@ Fix: - model `M` and valid-transition predicate; - environment class `Env`, scheduler class `Sched`, and order model `Ord`; - policy sequence `rho` and permitted declassification schedule `D`; and -- initial order point `o0`. +- initial state cut `c0`; +- participant-memory scope `Mem` (`episode_local_reset` only with + authoritative reset of every visible memory channel, otherwise + `persistent_across_episodes`); and +- a class `Sigma_L` of low participant strategies mapping delivered local + histories to proposal choices or nondeterministic choice sets. -Let `Runs(M, q, alpha, Env, Sched, Ord, rho, D)` be every valid run admitted by -those fixed parameters. Let: +Let `Runs(M, q, sigma, Env, Sched, Ord, rho, D, Mem)` be every valid run +generated when strategy `sigma` reacts only to its participant-visible local +history under those fixed parameters. Let: ```text -LowHist(M, q, alpha, ...) = +LowHist(M, q, sigma, ...) = { Pi[p,a,rho](tr) | - tr in Runs(M, q, alpha, Env, Sched, Ord, rho, D) } + tr in Runs(M, q, sigma, Env, Sched, Ord, rho, D, Mem) } ``` For total order, each element is an occurrence-preserving sequence. For partial @@ -284,22 +300,30 @@ simultaneity groups—not one convenient linearization. The baseline obligation is: ```text -forall q1, q2, alpha1, alpha2: +forall q1, q2, sigma in Sigma_L: ValidInitial(q1) and ValidInitial(q2) - and q1 ~=_{p,e,a,rho,o0} q2 - and AdmittedLowInputs(alpha1) = AdmittedLowInputs(alpha2) - and purge[p,e,a,rho](alpha1) = purge[p,e,a,rho](alpha2) - and DeclassificationSchedule(alpha1) = D - and DeclassificationSchedule(alpha2) = D - => LowHist(M, q1, alpha1, Env, Sched, Ord, rho, D) + and q1 ~=_{p,e,a,rho,c0} q2 + and StrategyIsLow(sigma, p, a, rho, Mem) + and DeclassificationSchedule(q1) = D + and DeclassificationSchedule(q2) = D + => LowHist(M, q1, sigma, Env, Sched, Ord, rho, D, Mem) = - LowHist(M, q2, alpha2, Env, Sched, Ord, rho, D) + LowHist(M, q2, sigma, Env, Sched, Ord, rho, D, Mem) ``` -The quantifiers over states, traces/runs, inputs, environments, schedulers, and -observations are explicit above. `M`, `Env`, `Sched`, `Ord`, `rho`, and `D` are -fixed parameters of one claim; changing any of them defines another claim -instance. +The same strategy is run against both low-equivalent initial states, but its +future choices may differ when the delivered low histories differ. The +obligation requires those complete projected support sets not to differ due +only to unauthorized high variation. Fixed open-loop input histories remain a +useful special case and falsification model, not the complete claim for an +adaptive human or agent. + +The quantifiers over states, strategies, traces/runs, environments, schedulers, +and observations are explicit above. `M`, `Env`, `Sched`, `Ord`, `rho`, `D`, +`Mem`, and `Sigma_L` are fixed parameters of one claim; changing any of them +defines another claim instance. A probabilistic strategy requires a governed +probability kernel and measure-sensitive relation; support-set equality alone +does not establish probabilistic noninterference. ### Assumption Boundary @@ -327,18 +351,22 @@ The test-local model exercises these falsification cases: 1. unauthorized high variations are purged and leave the same projected support set; -2. authorized declassification changes visible history only at its governed - effective order; -3. a future policy revision cannot authorize a past crossing; +2. authorized declassification changes visible history only at its exact + governed state cut; +3. a future or incomparable policy decision cannot authorize another cut; 4. hiding varies by participant and audience; 5. authorization, admission, visibility, marking, backend support, and transformation validity compose deny-first; 6. redaction does not grant authority and a transformation requires fresh admission; 7. concealment and revocation do not erase prior participant knowledge; -8. nondeterministic support-set differences are detected; and -9. positive noninterference prose must bind the governed relation and an - evidence boundary. +8. nondeterministic support-set differences are detected; +9. adaptive low strategies cannot react to undelivered high variation but do + react when that variation leaks; +10. reset preserves persistent participant information unless an + authoritative episode-local memory reset is declared; and +11. positive noninterference prose must bind the governed relation and an + evidence boundary. These are finite counterexamples and mutation/property checks. They can refute the bounded model when an invariant is broken. They do not establish the @@ -350,10 +378,10 @@ realization. | SEM-230 clause | Normative / machine-readable artifact | Executable or policy evidence | Assurance status and nonclaim | | --- | --- | --- | --- | | participant-relative world, view, local history, archival evidence, controller, authority, and order coordinates | this specification, “Existing Objects And State” | existing participant/runtime validators plus the bounded model | defined; existing adjacent carriers are partly implemented; no complete runtime policy path claimed | -| revisioned crossings, policy changes, labels, transitions, hidden actions, and projection | this specification, crossing/label/projection sections | policy-order, audience-relative hiding, and append-only-history tests | defined and bounded-tested; no wire contract or runtime mediation claimed | +| revisioned crossings, exact-cut policy decisions, labels, transitions, hidden actions, and projection | this specification, crossing/label/projection sections | exact-cut resolution, audience-relative hiding, append-only-history, memory-scope, and adaptive-strategy tests | defined and bounded-tested; no wire contract or universal runtime enforcement claimed | | authorization, admission, withholding, projection, redaction, declassification, disclosure, concealment, revocation, transformation, loss, and weakening remain distinct | this specification, “Distinct Operations” | deny-first, redaction, transformation, concealment, and revocation tests | defined and bounded-tested; no production enforcement claimed | -| exact noninterference relation, low equivalence, purge, declassification, quantifiers, scheduler/environment, order, termination/progress/timing, nondeterminism, and probability | this specification plus catalog relation `policy-noninterference` | finite support-set/property cases | definition complete; test status bounded; proof deliberately unproved | -| claims bind through the relation catalog with evidence status and nonclaims | behavioral-relation catalog revision `rev2` and claim surface `participant-information-flow-policy` | `tools/check_behavioral_relation_claims.py` and catalog/claim tests | catalog implemented and tested; no claim truth inferred from a valid binding | +| exact reactive noninterference relation, low equivalence, purge, declassification, participant memory, strategy quantifiers, scheduler/environment, order, termination/progress/timing, nondeterminism, and probability | this specification plus catalog relation `policy-noninterference` | finite support-set, adaptive-strategy, exact-cut, and memory-scope cases | definition complete; test status bounded; proof deliberately unproved | +| claims bind through the relation catalog with evidence status and nonclaims | behavioral-relation catalog revision `rev3` and claim surface `participant-information-flow-policy` | `tools/check_behavioral_relation_claims.py` and catalog/claim tests | catalog implemented and tested; no claim truth inferred from a valid binding | | intellectual lineage and exact RAES mappings | `docs/explain/sdl/lineage.md`, lineage ledger, and source audit | SDL-lineage policy gate | reviewed derivation record; no source syntax or compatibility claim | ## Follow-On Ownership And Nonclaims From 717ae8978867e4ef4ac9fc7824d4cbea43f0210d Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 19:21:45 +0200 Subject: [PATCH 15/55] docs: integrate decision surface v2 references --- ...ant-decision-epoch-state-cut-and-delivery-semantics.md | 8 ++++---- docs/decisions/adrs/adr-index.yaml | 2 +- docs/index.md | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md b/docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md index da93491b9..22989b4fd 100644 --- a/docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md +++ b/docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md @@ -380,10 +380,10 @@ bisimulation is a stronger optional claim. - [ADR-081](adr-081-behavioral-relation-taxonomy-and-claim-discipline.md) - [ADR-083](adr-083-participant-tool-decision-surface-and-exposure-semantics.md) - [ADR-085](adr-085-participant-information-flow-and-control.md) -- [Participant semantics](../../../specs/formal/participant-semantics/README.md) -- [Participant information-flow control](../../../specs/formal/participant-semantics/information-flow-control.md) -- [Participant runtime semantics](../../../specs/formal/participant-runtime/README.md) -- [Behavioral-relation taxonomy](../../../specs/formal/behavioral-relations/README.md) +- `specs/formal/participant-semantics/README.md` +- `specs/formal/participant-semantics/information-flow-control.md` +- `specs/formal/participant-runtime/README.md` +- `specs/formal/behavioral-relations/README.md` - [V2 migration guidance](../../explain/reference/participant-decision-surface-v2-migration.md) Primary intellectual lineage is revision-pinned in diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index cc29fb7cb..842e3b15a 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -443,4 +443,4 @@ adrs: pin: 90578754323795ca8775c47dd6095752ccb5a769a0f932ebe60abbe1d8954246 - id: ADR-095 path: docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md - pin: 914e7f7ce5d65ac75f2cbfe70ff17e6740f1157575c48802d85cbe692e78ea17 + pin: a37f5897755d573a1665641abb4a3695765e657b68018dde2b956fe99fe38edb diff --git a/docs/index.md b/docs/index.md index 0df62efd3..1ba00d739 100644 --- a/docs/index.md +++ b/docs/index.md @@ -144,6 +144,7 @@ explain/reference/explicitness-realization-semantics explain/reference/realization-envelopes explain/reference/scenario-variation-and-trial-realization explain/reference/experiment-binding-contracts +explain/reference/participant-decision-surface-v2-migration ``` ```{toctree} From 75bfc7734775397562651bdbbcdbbb7da3996dea Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 19:30:17 +0200 Subject: [PATCH 16/55] Fix SonarCloud findings (cycle 2) --- .../raes_backend_protocols/capability_admission.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/implementations/python/packages/raes_backend_protocols/capability_admission.py b/implementations/python/packages/raes_backend_protocols/capability_admission.py index a6357f71a..ff10b4672 100644 --- a/implementations/python/packages/raes_backend_protocols/capability_admission.py +++ b/implementations/python/packages/raes_backend_protocols/capability_admission.py @@ -125,16 +125,16 @@ def _validate_participant_feature_evidence( raise ValueError(f"participant feature '{feature}' has no conformance evidence") -def _resolve_participant_feature_strength( +def _validate_participant_feature_strength( feature: str, declaration: ParticipantFeatureSupport, required_level: ParticipantFeatureSupportLevel, allowed_downgrade_level: ParticipantFeatureSupportLevel | None, -) -> ParticipantFeatureSupport: +) -> None: declared_rank = _PARTICIPANT_FEATURE_SUPPORT_RANK[declaration.support_level] required_rank = _PARTICIPANT_FEATURE_SUPPORT_RANK[required_level] if declared_rank >= required_rank: - return declaration + return if allowed_downgrade_level is None: raise ValueError( f"participant feature '{feature}' requires {required_level.value} support; " @@ -145,7 +145,6 @@ def _resolve_participant_feature_strength( f"participant feature '{feature}' authorized downgrade is " f"{allowed_downgrade_level.value}; backend declares {declaration.support_level.value}" ) - return declaration def resolve_participant_feature_support( @@ -176,12 +175,13 @@ def resolve_participant_feature_support( if declaration is None: return None _validate_participant_feature_evidence(manifest, feature, declaration) - return _resolve_participant_feature_strength( + _validate_participant_feature_strength( feature, declaration, required_level, allowed_downgrade_level, ) + return declaration def participant_feature_support_gaps( From e699c097a6d711284b74cc340fee6e8eb85306d1 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 19:50:18 +0200 Subject: [PATCH 17/55] refactor: simplify decision surface v2 validation --- ...articipant_decision_surface_exposure_v2.py | 72 ++++--- .../participant_decision_surface_v2.py | 104 +-------- ...ticipant_decision_surface_v2_validation.py | 151 +++++++++++++ .../raes_contracts/participant_binding_v2.py | 200 +++++++++++++----- .../raes_processor/models/__init__.py | 2 + .../models/decision_surface_anchor_v2.py | 150 +++++++------ .../models/decision_surface_v2.py | 88 ++++++-- .../models/participant_exposure_v2.py | 64 +++++- ...sem_220_participant_decision_surface_v2.py | 3 +- ...participant_decision_surface_v2_runtime.py | 18 +- 10 files changed, 579 insertions(+), 273 deletions(-) create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2_validation.py 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 a0ad4e952..1075b2a07 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 @@ -16,6 +16,43 @@ def _require_unique(values: list[str], field_name: str) -> None: raise ValueError(f"{field_name} must not contain duplicates") +_EXPOSURE_REF_COLLECTIONS = ( + "source_marking_definition_refs", + "result_marking_definition_refs", + "source_provenance_refs", + "result_provenance_refs", + "evidence_refs", + "provenance_refs", + "loss_and_limitations", +) + + +def _validate_operation_requirements(binding: ParticipantDecisionSurfaceExposureBindingV2Model) -> None: + if binding.source_ref != binding.item_ref and binding.transformation_rule_ref is None: + raise ValueError("derived exposure items require transformation_rule_ref") + if binding.operation in {"masking", "redaction", "transformation"} and binding.transformation_rule_ref is None: + raise ValueError(f"{binding.operation} exposure operations require transformation_rule_ref") + if binding.operation == "redaction" and binding.redaction_policy_ref is None: + raise ValueError("redaction exposure operations require redaction_policy_ref") + if binding.operation == "declassification" and binding.declassification_basis_ref is None: + raise ValueError("declassification exposure operations require declassification_basis_ref") + + +def _validate_inherited_information(binding: ParticipantDecisionSurfaceExposureBindingV2Model) -> None: + if binding.declassification_basis_ref is not None: + return + if not set(binding.source_marking_definition_refs).issubset(binding.result_marking_definition_refs): + raise ValueError("derived exposure results must inherit source markings unless declassification is explicit") + if not set(binding.source_provenance_refs).issubset(binding.result_provenance_refs): + raise ValueError("derived exposure results must inherit source provenance unless declassification is explicit") + + +def _validate_provenance_carriage(binding: ParticipantDecisionSurfaceExposureBindingV2Model) -> None: + exposed_provenance = {*binding.source_provenance_refs, *binding.result_provenance_refs} + if not exposed_provenance.issubset(binding.provenance_refs): + raise ValueError("source and result provenance refs must be carried by provenance_refs") + + class ParticipantDecisionSurfaceExposureBindingV2Model(ContractModel): """One exact-cut authorization for an item admitted to a projected view. @@ -58,38 +95,11 @@ class ParticipantDecisionSurfaceExposureBindingV2Model(ContractModel): @model_validator(mode="after") def _validate_exposure_basis(self) -> ParticipantDecisionSurfaceExposureBindingV2Model: - for field_name in ( - "source_marking_definition_refs", - "result_marking_definition_refs", - "source_provenance_refs", - "result_provenance_refs", - "evidence_refs", - "provenance_refs", - "loss_and_limitations", - ): + for field_name in _EXPOSURE_REF_COLLECTIONS: _require_unique(getattr(self, field_name), field_name) - if self.source_ref != self.item_ref and self.transformation_rule_ref is None: - raise ValueError("derived exposure items require transformation_rule_ref") - if self.operation in {"masking", "redaction", "transformation"} and self.transformation_rule_ref is None: - raise ValueError(f"{self.operation} exposure operations require transformation_rule_ref") - if self.operation == "redaction" and self.redaction_policy_ref is None: - raise ValueError("redaction exposure operations require redaction_policy_ref") - if self.operation == "declassification" and self.declassification_basis_ref is None: - raise ValueError("declassification exposure operations require declassification_basis_ref") - if self.declassification_basis_ref is None and not set(self.source_marking_definition_refs).issubset( - self.result_marking_definition_refs - ): - raise ValueError( - "derived exposure results must inherit source markings unless declassification is explicit" - ) - if self.declassification_basis_ref is None and not set(self.source_provenance_refs).issubset( - self.result_provenance_refs - ): - raise ValueError( - "derived exposure results must inherit source provenance unless declassification is explicit" - ) - if not {*self.source_provenance_refs, *self.result_provenance_refs}.issubset(self.provenance_refs): - raise ValueError("source and result provenance refs must be carried by provenance_refs") + _validate_operation_requirements(self) + _validate_inherited_information(self) + _validate_provenance_carriage(self) return self 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 b0f628329..386a6c314 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 @@ -26,10 +26,13 @@ ParticipantDecisionSurfaceExposureBindingV2Model, ParticipantDecisionSurfaceStateCutOrderModel, ) +from .participant_decision_surface_v2_validation import _validate_participant_decision_surface_v2 from .participant_manifests import DigestString from .participant_runtime import ParticipantRuntimeDeliveryBasis from .schema_invariants import _add_aces_invariant +_SURFACE_V2_VALIDATOR = "raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface" + class ParticipantDecisionSurfaceSequenceCutModel(ContractModel): """A complete prefix ending at one event in a declared total order.""" @@ -281,14 +284,6 @@ def _validate_delivery_refs(self) -> ParticipantDecisionSurfaceDeliveryV2Model: return self -def _view_exposed_refs(view: ParticipantDecisionSurfaceViewV2Model) -> set[str]: - return { - *view.visible_context_refs, - *(entry.action_contract_address for entry in view.action_entries), - *view.affordance_refs, - } - - class ParticipantDecisionSurfaceV2Model(ContractModel): """A projected or delivered v2 decision surface with separated trust planes.""" @@ -300,92 +295,7 @@ class ParticipantDecisionSurfaceV2Model(ContractModel): @model_validator(mode="after") def _validate_surface(self) -> ParticipantDecisionSurfaceV2Model: - view = self.participant_view - assurance = self.assurance - coordinate_comparisons = ( - ("participant_address", assurance.participant_address, view.participant_address), - ("episode_id", assurance.episode_id, view.episode_id), - ("decision_epoch", assurance.decision_epoch, view.decision_epoch), - ) - mismatched = [ - name for name, assurance_value, view_value in coordinate_comparisons if assurance_value != view_value - ] - if mismatched: - raise ValueError("assurance disagrees with the participant view on: " + ", ".join(mismatched)) - - from ..satisfiability import canonical_contract_digest - - if assurance.participant_view_digest != canonical_contract_digest(view): - raise ValueError("assurance participant_view_digest must match the canonical participant view") - - bindings = {binding.item_ref: binding for binding in assurance.exposure_bindings} - _require_unique([binding.item_ref for binding in assurance.exposure_bindings], "exposure_bindings.item_ref") - expected = _view_exposed_refs(view) - if bindings.keys() != expected: - missing = sorted(expected - bindings.keys()) - extra = sorted(bindings.keys() - expected) - details = [] - if missing: - details.append("missing " + ", ".join(missing)) - if extra: - details.append("unexpected " + ", ".join(extra)) - raise ValueError("exposure_bindings must exactly cover participant view refs: " + "; ".join(details)) - for binding in assurance.exposure_bindings: - comparisons = ( - ("participant_address", binding.participant_address, view.participant_address), - ("episode_id", binding.episode_id, view.episode_id), - ("decision_epoch", binding.decision_epoch, view.decision_epoch), - ("decision_cut_ref", binding.decision_cut_ref, assurance.derivation_anchor.state_cut.cut_ref), - ("audience_scope_ref", binding.audience_scope_ref, assurance.audience_scope_ref), - ("projection_policy_ref", binding.projection_policy_ref, assurance.projection_policy_ref), - ( - "projection_policy_revision", - binding.projection_policy_revision, - assurance.projection_policy_revision, - ), - ( - "projection_policy_decision_ref", - binding.projection_policy_decision_ref, - assurance.projection_policy_decision_ref, - ), - ("exposure_policy_ref", binding.exposure_policy_ref, assurance.exposure_policy_ref), - ) - binding_mismatches = [ - name for name, binding_value, expected_value in comparisons if binding_value != expected_value - ] - if binding_mismatches: - raise ValueError( - f"exposure binding {binding.item_ref!r} disagrees with the surface on: " - + ", ".join(binding_mismatches) - ) - if not set(binding.evidence_refs).issubset(assurance.evidence_refs): - raise ValueError(f"exposure binding {binding.item_ref!r} evidence must be carried by assurance") - if not set(binding.provenance_refs).issubset(assurance.provenance_refs): - raise ValueError(f"exposure binding {binding.item_ref!r} provenance must be carried by assurance") - - if self.surface_state == "projected" and self.delivery is not None: - raise ValueError("projected surfaces must not carry delivery") - if self.surface_state == "delivered" and self.delivery is None: - raise ValueError("delivered surfaces require delivery") - if self.delivery is not None: - delivery_comparisons = ( - ("surface_id", self.delivery.surface_id, view.surface_id), - ("participant_address", self.delivery.participant_address, view.participant_address), - ("episode_id", self.delivery.episode_id, view.episode_id), - ("decision_epoch", self.delivery.decision_epoch, view.decision_epoch), - ( - "participant_view_digest", - self.delivery.participant_view_digest, - assurance.participant_view_digest, - ), - ) - delivery_mismatches = [ - name - for name, delivery_value, expected_value in delivery_comparisons - if delivery_value != expected_value - ] - if delivery_mismatches: - raise ValueError("delivery disagrees with the participant view on: " + ", ".join(delivery_mismatches)) + _validate_participant_decision_surface_v2(self) return self @classmethod @@ -401,7 +311,7 @@ def __get_pydantic_json_schema__( "decision-surface-v2-plane-separation", "The participant view contains only participant-available choice material; derivation, policy, evidence, " "provenance, and delivery remain in separate assurance and delivery planes.", - validator="raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface", + validator=_SURFACE_V2_VALIDATOR, inputs=[{"contract_id": "participant-decision-surface-v2", "instance_path": "#"}], ) _add_aces_invariant( @@ -409,14 +319,14 @@ def __get_pydantic_json_schema__( "decision-surface-v2-exact-cut-policy", "Each exposed item is bound to the derivation state cut and exact policy decision, independently of the " "participant decision epoch.", - validator="raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface", + validator=_SURFACE_V2_VALIDATOR, inputs=[{"contract_id": "participant-decision-surface-v2", "instance_path": "#/assurance"}], ) _add_aces_invariant( json_schema, "decision-surface-v2-delivery-before-selection", "A surface is actionable only in delivered state, with delivery bound to the canonical participant view.", - validator="raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface", + validator=_SURFACE_V2_VALIDATOR, inputs=[{"contract_id": "participant-decision-surface-v2", "instance_path": "#/delivery"}], ) _add_aces_invariant( diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2_validation.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2_validation.py new file mode 100644 index 000000000..347a31fd0 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2_validation.py @@ -0,0 +1,151 @@ +"""Semantic validation helpers for participant decision-surface v2.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .participant_decision_surface_exposure_v2 import ParticipantDecisionSurfaceExposureBindingV2Model + from .participant_decision_surface_v2 import ( + ParticipantDecisionSurfaceAssuranceV2Model, + ParticipantDecisionSurfaceDeliveryV2Model, + ParticipantDecisionSurfaceV2Model, + ParticipantDecisionSurfaceViewV2Model, + ) + + +def _mismatched_names(comparisons: tuple[tuple[str, object, object], ...]) -> list[str]: + return [name for name, actual, expected in comparisons if actual != expected] + + +def _validate_surface_coordinates( + view: ParticipantDecisionSurfaceViewV2Model, + assurance: ParticipantDecisionSurfaceAssuranceV2Model, +) -> None: + mismatched = _mismatched_names( + ( + ("participant_address", assurance.participant_address, view.participant_address), + ("episode_id", assurance.episode_id, view.episode_id), + ("decision_epoch", assurance.decision_epoch, view.decision_epoch), + ) + ) + if mismatched: + raise ValueError("assurance disagrees with the participant view on: " + ", ".join(mismatched)) + + +def _validate_participant_view_digest( + view: ParticipantDecisionSurfaceViewV2Model, + assurance: ParticipantDecisionSurfaceAssuranceV2Model, +) -> None: + from ..satisfiability import canonical_contract_digest + + if assurance.participant_view_digest != canonical_contract_digest(view): + raise ValueError("assurance participant_view_digest must match the canonical participant view") + + +def _view_exposed_refs(view: ParticipantDecisionSurfaceViewV2Model) -> set[str]: + return { + *view.visible_context_refs, + *(entry.action_contract_address for entry in view.action_entries), + *view.affordance_refs, + } + + +def _validate_exposure_binding_coverage( + view: ParticipantDecisionSurfaceViewV2Model, + assurance: ParticipantDecisionSurfaceAssuranceV2Model, +) -> None: + item_refs = [binding.item_ref for binding in assurance.exposure_bindings] + if len(item_refs) != len(set(item_refs)): + raise ValueError("exposure_bindings.item_ref must not contain duplicates") + actual = set(item_refs) + expected = _view_exposed_refs(view) + if actual == expected: + return + details = [] + missing = sorted(expected - actual) + unexpected = sorted(actual - expected) + if missing: + details.append("missing " + ", ".join(missing)) + if unexpected: + details.append("unexpected " + ", ".join(unexpected)) + raise ValueError("exposure_bindings must exactly cover participant view refs: " + "; ".join(details)) + + +def _validate_exposure_binding( + binding: ParticipantDecisionSurfaceExposureBindingV2Model, + view: ParticipantDecisionSurfaceViewV2Model, + assurance: ParticipantDecisionSurfaceAssuranceV2Model, +) -> None: + mismatched = _mismatched_names( + ( + ("participant_address", binding.participant_address, view.participant_address), + ("episode_id", binding.episode_id, view.episode_id), + ("decision_epoch", binding.decision_epoch, view.decision_epoch), + ("decision_cut_ref", binding.decision_cut_ref, assurance.derivation_anchor.state_cut.cut_ref), + ("audience_scope_ref", binding.audience_scope_ref, assurance.audience_scope_ref), + ("projection_policy_ref", binding.projection_policy_ref, assurance.projection_policy_ref), + ( + "projection_policy_revision", + binding.projection_policy_revision, + assurance.projection_policy_revision, + ), + ( + "projection_policy_decision_ref", + binding.projection_policy_decision_ref, + assurance.projection_policy_decision_ref, + ), + ("exposure_policy_ref", binding.exposure_policy_ref, assurance.exposure_policy_ref), + ) + ) + if mismatched: + raise ValueError( + f"exposure binding {binding.item_ref!r} disagrees with the surface on: " + ", ".join(mismatched) + ) + if not set(binding.evidence_refs).issubset(assurance.evidence_refs): + raise ValueError(f"exposure binding {binding.item_ref!r} evidence must be carried by assurance") + if not set(binding.provenance_refs).issubset(assurance.provenance_refs): + raise ValueError(f"exposure binding {binding.item_ref!r} provenance must be carried by assurance") + + +def _validate_exposure_bindings( + view: ParticipantDecisionSurfaceViewV2Model, + assurance: ParticipantDecisionSurfaceAssuranceV2Model, +) -> None: + _validate_exposure_binding_coverage(view, assurance) + for binding in assurance.exposure_bindings: + _validate_exposure_binding(binding, view, assurance) + + +def _validate_delivery_coordinates( + delivery: ParticipantDecisionSurfaceDeliveryV2Model, + view: ParticipantDecisionSurfaceViewV2Model, + assurance: ParticipantDecisionSurfaceAssuranceV2Model, +) -> None: + mismatched = _mismatched_names( + ( + ("surface_id", delivery.surface_id, view.surface_id), + ("participant_address", delivery.participant_address, view.participant_address), + ("episode_id", delivery.episode_id, view.episode_id), + ("decision_epoch", delivery.decision_epoch, view.decision_epoch), + ("participant_view_digest", delivery.participant_view_digest, assurance.participant_view_digest), + ) + ) + if mismatched: + raise ValueError("delivery disagrees with the participant view on: " + ", ".join(mismatched)) + + +def _validate_delivery_state(surface: ParticipantDecisionSurfaceV2Model) -> None: + if surface.surface_state == "projected" and surface.delivery is not None: + raise ValueError("projected surfaces must not carry delivery") + if surface.surface_state == "delivered" and surface.delivery is None: + raise ValueError("delivered surfaces require delivery") + if surface.delivery is not None: + _validate_delivery_coordinates(surface.delivery, surface.participant_view, surface.assurance) + + +def _validate_participant_decision_surface_v2(surface: ParticipantDecisionSurfaceV2Model) -> None: + _validate_surface_coordinates(surface.participant_view, surface.assurance) + _validate_participant_view_digest(surface.participant_view, surface.assurance) + _validate_exposure_bindings(surface.participant_view, surface.assurance) + _validate_delivery_state(surface) diff --git a/implementations/python/packages/raes_contracts/participant_binding_v2.py b/implementations/python/packages/raes_contracts/participant_binding_v2.py index 22d2e17ae..6717e7e12 100644 --- a/implementations/python/packages/raes_contracts/participant_binding_v2.py +++ b/implementations/python/packages/raes_contracts/participant_binding_v2.py @@ -6,6 +6,7 @@ from typing import Protocol, cast from .contracts import ( + ParticipantDecisionSurfaceActionEntryModel, ParticipantDecisionSurfaceCandidateSetFormModel, ParticipantDecisionSurfaceConstrainedFormModel, ParticipantDecisionSurfaceSelectionV2Model, @@ -45,78 +46,142 @@ class ParticipantDecisionSurfaceBindingResolversV2: delivery: ParticipantDecisionSurfaceDeliveryResolverV2 -def bind_participant_decision_surface_selection_v2( - *, +def _validate_selection_identity( surface: ParticipantDecisionSurfaceV2Model, selection: ParticipantDecisionSurfaceSelectionV2Model, - admission_request: ParticipantActionAdmissionRequest, - argument_shape_resolver: ParticipantDecisionSurfaceArgumentShapeResolver, - apparatus_resolver: ParticipantDecisionSurfaceApparatusResolverV2, - delivery_resolver: ParticipantDecisionSurfaceDeliveryResolverV2, -) -> ParticipantActionAdmissionRequest: - """Validate a selection from an exact, currently delivered v2 view.""" - - validate_participant_decision_surface_v2_delivery(surface, delivery_resolver) +) -> None: view = surface.participant_view - assurance = surface.assurance delivery = surface.delivery assert delivery is not None - if selection.surface_id != view.surface_id: - raise ValueError("selection surface_id must match the participant decision surface") - if selection.decision_epoch != view.decision_epoch: - raise ValueError("selection decision_epoch must match the participant decision surface") - if selection.participant_view_digest != assurance.participant_view_digest: - raise ValueError("selection participant_view_digest must match the canonical participant view") - if selection.delivery_ref != delivery.delivery_ref: - raise ValueError("selection delivery_ref must match the authoritative surface delivery") - entries = {entry.action_contract_address: entry for entry in view.action_entries} + comparisons = ( + (selection.surface_id, view.surface_id, "selection surface_id must match the participant decision surface"), + ( + selection.decision_epoch, + view.decision_epoch, + "selection decision_epoch must match the participant decision surface", + ), + ( + selection.participant_view_digest, + surface.assurance.participant_view_digest, + "selection participant_view_digest must match the canonical participant view", + ), + ( + selection.delivery_ref, + delivery.delivery_ref, + "selection delivery_ref must match the authoritative surface delivery", + ), + ) + for actual, expected, message in comparisons: + if actual != expected: + raise ValueError(message) + + +def _selected_action_entry( + surface: ParticipantDecisionSurfaceV2Model, + selection: ParticipantDecisionSurfaceSelectionV2Model, +) -> ParticipantDecisionSurfaceActionEntryModel: + entries = {entry.action_contract_address: entry for entry in surface.participant_view.action_entries} entry = entries.get(selection.action_contract_address) if entry is None: raise ValueError("selection action_contract_address is not carried by the participant decision surface") + return entry + + +def _validate_selected_entry( + entry: ParticipantDecisionSurfaceActionEntryModel, + selection: ParticipantDecisionSurfaceSelectionV2Model, +) -> None: if entry.eligibility != "eligible": raise ValueError("participant decision surface selection is not eligible") if entry.support != "supported": raise ValueError("participant decision surface selection is not supported") if entry.selection_shape_ref != selection.argument_shape_ref: raise ValueError("selection argument_shape_ref must match the participant decision surface action entry") - if isinstance(view.form, ParticipantDecisionSurfaceCandidateSetFormModel): - if entry.entry_id not in view.form.candidate_entry_ids: + + +def _validate_form_selection( + surface: ParticipantDecisionSurfaceV2Model, + entry: ParticipantDecisionSurfaceActionEntryModel, + selection: ParticipantDecisionSurfaceSelectionV2Model, +) -> None: + form = surface.participant_view.form + if isinstance(form, ParticipantDecisionSurfaceCandidateSetFormModel): + if entry.entry_id not in form.candidate_entry_ids: raise ValueError("selection action is not a member of the participant candidate-action set") - elif isinstance(view.form, ParticipantDecisionSurfaceConstrainedFormModel): - if entry.entry_id != view.form.action_entry_id or selection.argument_shape_ref != view.form.argument_shape_ref: + return + if isinstance(form, ParticipantDecisionSurfaceConstrainedFormModel): + if entry.entry_id != form.action_entry_id or selection.argument_shape_ref != form.argument_shape_ref: raise ValueError("selection does not match the constrained-form action and argument shape") - else: - if selection.action_contract_address not in view.form.allowed_action_contract_addresses: - raise ValueError("open-ended proposal does not bind to an allowed governed action contract") - if selection.argument_shape_ref != view.form.argument_shape_ref: - raise ValueError("open-ended proposal does not bind to the governed argument shape") - if admission_request.participant_address != view.participant_address: - raise ValueError("admission request participant_address must match the participant decision surface") - if admission_request.action_contract_address != selection.action_contract_address: - raise ValueError("admission request action_contract_address must match the validated selection") - if admission_request.observation_boundary_address != assurance.observation_boundary_address: - raise ValueError("admission request observation_boundary_address must match the participant decision surface") - if admission_request.implementation_selection.selected_decision_surface_mode != view.decision_control_mode: - raise ValueError("admission request decision-control mode must match the participant decision surface") - if ( - "participant-decision-surface-v2" - not in admission_request.implementation_selection.participant_contract_versions - ): + return + if selection.action_contract_address not in form.allowed_action_contract_addresses: + raise ValueError("open-ended proposal does not bind to an allowed governed action contract") + if selection.argument_shape_ref != form.argument_shape_ref: + raise ValueError("open-ended proposal does not bind to the governed argument shape") + + +def _validate_admission_agreement( + surface: ParticipantDecisionSurfaceV2Model, + selection: ParticipantDecisionSurfaceSelectionV2Model, + admission_request: ParticipantActionAdmissionRequest, +) -> None: + view = surface.participant_view + assurance = surface.assurance + comparisons = ( + ( + admission_request.participant_address, + view.participant_address, + "admission request participant_address must match the participant decision surface", + ), + ( + admission_request.action_contract_address, + selection.action_contract_address, + "admission request action_contract_address must match the validated selection", + ), + ( + admission_request.observation_boundary_address, + assurance.observation_boundary_address, + "admission request observation_boundary_address must match the participant decision surface", + ), + ( + admission_request.implementation_selection.selected_decision_surface_mode, + view.decision_control_mode, + "admission request decision-control mode must match the participant decision surface", + ), + ) + for actual, expected, message in comparisons: + if actual != expected: + raise ValueError(message) + supported_versions = admission_request.implementation_selection.participant_contract_versions + if "participant-decision-surface-v2" not in supported_versions: raise ValueError("participant implementation selection must declare participant-decision-surface-v2 support") + + +def _resolve_apparatus_selection( + surface: ParticipantDecisionSurfaceV2Model, + admission_request: ParticipantActionAdmissionRequest, + resolver: ParticipantDecisionSurfaceApparatusResolverV2, +) -> None: + assurance = surface.assurance try: - resolved_selection = apparatus_resolver( + resolved = resolver( implementation_selection_ref=assurance.implementation_selection_ref, exposure_policy_ref=assurance.exposure_policy_ref, decision_cut_ref=assurance.derivation_anchor.state_cut.cut_ref, ) except Exception as exc: raise ValueError("participant decision surface exact-cut apparatus resolution failed") from exc - if resolved_selection is None: + if resolved is None: raise ValueError("participant decision surface apparatus refs did not resolve at the derivation cut") - if resolved_selection.model_dump(mode="json") != admission_request.implementation_selection.model_dump(mode="json"): + if resolved.model_dump(mode="json") != admission_request.implementation_selection.model_dump(mode="json"): raise ValueError("admission request implementation selection and exposure policy must match the surface refs") + + +def _resolve_validated_selection( + selection: ParticipantDecisionSurfaceSelectionV2Model, + resolver: ParticipantDecisionSurfaceArgumentShapeResolver, +) -> ParticipantValidatedActionSelection: try: - validated_selection = argument_shape_resolver( + validated = resolver( action_contract_address=selection.action_contract_address, argument_shape_ref=selection.argument_shape_ref, proposal_ref=selection.proposal_ref, @@ -124,18 +189,27 @@ def bind_participant_decision_surface_selection_v2( ) except Exception as exc: raise ValueError("participant decision surface argument-shape resolution failed") from exc - if not isinstance(validated_selection, ParticipantValidatedActionSelection): + if not isinstance(validated, ParticipantValidatedActionSelection): raise ValueError("participant decision surface proposal failed governed argument-shape validation") - if ( - validated_selection.action_contract_address, - validated_selection.argument_shape_ref, - validated_selection.proposal_ref, - ) != ( + coordinates = ( + validated.action_contract_address, + validated.argument_shape_ref, + validated.proposal_ref, + ) + expected = ( selection.action_contract_address, selection.argument_shape_ref, selection.proposal_ref, - ): + ) + if coordinates != expected: raise ValueError("validated participant action selection must match the governed proposal coordinates") + return validated + + +def _bind_validated_selection( + admission_request: ParticipantActionAdmissionRequest, + validated_selection: ParticipantValidatedActionSelection, +) -> ParticipantActionAdmissionRequest: bound_request = cast( ParticipantActionAdmissionRequest, replace(admission_request, validated_selection=validated_selection), @@ -146,6 +220,28 @@ def bind_participant_decision_surface_selection_v2( return bound_request +def bind_participant_decision_surface_selection_v2( + *, + surface: ParticipantDecisionSurfaceV2Model, + selection: ParticipantDecisionSurfaceSelectionV2Model, + admission_request: ParticipantActionAdmissionRequest, + argument_shape_resolver: ParticipantDecisionSurfaceArgumentShapeResolver, + apparatus_resolver: ParticipantDecisionSurfaceApparatusResolverV2, + delivery_resolver: ParticipantDecisionSurfaceDeliveryResolverV2, +) -> ParticipantActionAdmissionRequest: + """Validate a selection from an exact, currently delivered v2 view.""" + + validate_participant_decision_surface_v2_delivery(surface, delivery_resolver) + _validate_selection_identity(surface, selection) + entry = _selected_action_entry(surface, selection) + _validate_selected_entry(entry, selection) + _validate_form_selection(surface, entry, selection) + _validate_admission_agreement(surface, selection, admission_request) + _resolve_apparatus_selection(surface, admission_request, apparatus_resolver) + validated_selection = _resolve_validated_selection(selection, argument_shape_resolver) + return _bind_validated_selection(admission_request, validated_selection) + + __all__ = ( "ParticipantDecisionSurfaceApparatusResolverV2", "ParticipantDecisionSurfaceBindingResolversV2", diff --git a/implementations/python/packages/raes_processor/models/__init__.py b/implementations/python/packages/raes_processor/models/__init__.py index 58ad2a1ae..df6bfd3c1 100644 --- a/implementations/python/packages/raes_processor/models/__init__.py +++ b/implementations/python/packages/raes_processor/models/__init__.py @@ -133,6 +133,7 @@ project_participant_decision_surface, ) from .decision_surface_anchor_v2 import ( + ParticipantBehaviorProjectionAnchorRequestV2, resolve_participant_behavior_projection_anchor_v2, resolve_participant_episode_readiness_anchor_v2, validate_participant_decision_surface_v2_anchor, @@ -276,6 +277,7 @@ "ParticipantExposureRealizationAssessment", "ParticipantExposureResolvers", "ParticipantBehaviorRuntime", + "ParticipantBehaviorProjectionAnchorRequestV2", "ParticipantBehaviorSpecificationRuntime", "ParticipantInjectDeliveryRuntime", "ParticipantAutonomousExecutionRuntime", diff --git a/implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py b/implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py index 7f3176422..7e05aa7b0 100644 --- a/implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py +++ b/implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py @@ -5,6 +5,7 @@ import hashlib import json from collections.abc import Mapping, Sequence +from dataclasses import dataclass from raes_contracts.contracts import ( ParticipantDecisionSurfaceBehaviorAnchorV2Model, @@ -27,6 +28,18 @@ from .runtime_model import RuntimeModel +@dataclass(frozen=True) +class ParticipantBehaviorProjectionAnchorRequestV2: + """Coordinates and assurance refs for one behavior-derived state cut.""" + + participant_address: str + episode_id: str + decision_epoch: int + behavior_history_order: int + evidence_refs: Sequence[str] + provenance_refs: Sequence[str] + + def _stable_projection_event_ref(event_domain: str, payload: Mapping[str, object]) -> str: try: canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") @@ -153,26 +166,24 @@ def resolve_participant_behavior_projection_anchor_v2( runtime_snapshot: RuntimeSnapshot, *, runtime_model: RuntimeModel, - participant_address: str, - episode_id: str, - decision_epoch: int, - behavior_history_order: int, - evidence_refs: Sequence[str], - provenance_refs: Sequence[str], + request: ParticipantBehaviorProjectionAnchorRequestV2, ) -> ParticipantDecisionSurfaceBehaviorAnchorV2Model: """Resolve a later decision epoch from the exact current behavior prefix.""" - state, episode_history = _participant_episode_snapshot_context(runtime_snapshot, participant_address) - if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != episode_id: + state, episode_history = _participant_episode_snapshot_context( + runtime_snapshot, + request.participant_address, + ) + if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != request.episode_id: raise ValueError("behavior projection anchor must identify the current running participant episode") events = _current_episode_behavior_events( runtime_snapshot, - participant_address=participant_address, - episode_id=episode_id, + participant_address=request.participant_address, + episode_id=request.episode_id, ) - if behavior_history_order < 0 or behavior_history_order >= len(events): + if request.behavior_history_order < 0 or request.behavior_history_order >= len(events): raise ValueError("behavior_history_order must identify an event in the current episode behavior history") - if behavior_history_order != len(events) - 1: + if request.behavior_history_order != len(events) - 1: raise ValueError("behavior projection anchor must identify the exact current behavior-history prefix head") violations = tuple( iter_participant_behavior_history_violations( @@ -180,7 +191,7 @@ def resolve_participant_behavior_projection_anchor_v2( action_contracts=runtime_model.action_contracts, observation_boundaries=runtime_model.observation_boundaries, participant_episode_history=[event.to_payload() for event in episode_history], - expected_participant_address=participant_address, + expected_participant_address=request.participant_address, ) ) if violations: @@ -189,7 +200,7 @@ def resolve_participant_behavior_projection_anchor_v2( if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: raise ValueError("behavior decision surfaces require a terminal observation_emitted event") resolved_epoch = _behavior_decision_epoch(events) - if decision_epoch != resolved_epoch: + if request.decision_epoch != resolved_epoch: raise ValueError("decision_epoch must equal the number of completed participant observations") event_refs = tuple(_stable_projection_event_ref("behavior", item.to_payload()) for item in events) state_cut = _sequence_cut( @@ -199,15 +210,73 @@ def resolve_participant_behavior_projection_anchor_v2( ) return ParticipantDecisionSurfaceBehaviorAnchorV2Model( anchor_kind="behavior_event", - participant_address=participant_address, - episode_id=episode_id, - decision_epoch=decision_epoch, + participant_address=request.participant_address, + episode_id=request.episode_id, + decision_epoch=request.decision_epoch, event_ref=event_refs[-1], state_cut=state_cut, event_type=event.event_type.value, action_instance_id=event.action_instance_id, - evidence_refs=list(evidence_refs), - provenance_refs=list(dict.fromkeys((*provenance_refs, event_refs[-1]))), + evidence_refs=list(request.evidence_refs), + provenance_refs=list(dict.fromkeys((*request.provenance_refs, event_refs[-1]))), + ) + + +def _resolve_behavior_anchor_without_runtime_model( + runtime_snapshot: RuntimeSnapshot, + anchor: ParticipantDecisionSurfaceBehaviorAnchorV2Model, +) -> ParticipantDecisionSurfaceBehaviorAnchorV2Model: + state, _ = _participant_episode_snapshot_context(runtime_snapshot, anchor.participant_address) + events = _current_episode_behavior_events( + runtime_snapshot, + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + ) + if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != anchor.episode_id: + raise ValueError("behavior derivation anchor is outside the current running episode") + if not events or events[-1].event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + raise ValueError("behavior derivation anchor is not at a terminal participant observation") + event_refs = tuple(_stable_projection_event_ref("behavior", item.to_payload()) for item in events) + return ParticipantDecisionSurfaceBehaviorAnchorV2Model( + anchor_kind="behavior_event", + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + decision_epoch=_behavior_decision_epoch(events), + event_ref=event_refs[-1], + state_cut=_sequence_cut( + history_domain="participant_behavior_history", + order_model="behavior_history_order", + event_refs=event_refs, + ), + event_type=events[-1].event_type.value, + action_instance_id=events[-1].action_instance_id, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + + +def _resolve_behavior_anchor( + runtime_snapshot: RuntimeSnapshot, + anchor: ParticipantDecisionSurfaceBehaviorAnchorV2Model, + runtime_model: RuntimeModel | None, +) -> ParticipantDecisionSurfaceBehaviorAnchorV2Model: + state_cut = anchor.state_cut + if not isinstance(state_cut, ParticipantDecisionSurfaceSequenceCutModel): + raise ValueError("the reference runtime cannot re-resolve a causal-frontier behavior anchor") + if runtime_model is None: + return _resolve_behavior_anchor_without_runtime_model(runtime_snapshot, anchor) + request = ParticipantBehaviorProjectionAnchorRequestV2( + participant_address=anchor.participant_address, + episode_id=anchor.episode_id, + decision_epoch=anchor.decision_epoch, + behavior_history_order=state_cut.anchor_order, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + return resolve_participant_behavior_projection_anchor_v2( + runtime_snapshot, + runtime_model=runtime_model, + request=request, ) @@ -228,47 +297,6 @@ def validate_participant_decision_surface_v2_anchor( provenance_refs=anchor.provenance_refs, ) else: - state_cut = anchor.state_cut - if not isinstance(state_cut, ParticipantDecisionSurfaceSequenceCutModel): - raise ValueError("the reference runtime cannot re-resolve a causal-frontier behavior anchor") - if runtime_model is not None: - resolved = resolve_participant_behavior_projection_anchor_v2( - runtime_snapshot, - runtime_model=runtime_model, - participant_address=anchor.participant_address, - episode_id=anchor.episode_id, - decision_epoch=anchor.decision_epoch, - behavior_history_order=state_cut.anchor_order, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - else: - state, _ = _participant_episode_snapshot_context(runtime_snapshot, anchor.participant_address) - events = _current_episode_behavior_events( - runtime_snapshot, - participant_address=anchor.participant_address, - episode_id=anchor.episode_id, - ) - if state.status != ParticipantEpisodeStatus.RUNNING or state.episode_id != anchor.episode_id: - raise ValueError("behavior derivation anchor is outside the current running episode") - if not events or events[-1].event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - raise ValueError("behavior derivation anchor is not at a terminal participant observation") - event_refs = tuple(_stable_projection_event_ref("behavior", item.to_payload()) for item in events) - resolved = ParticipantDecisionSurfaceBehaviorAnchorV2Model( - anchor_kind="behavior_event", - participant_address=anchor.participant_address, - episode_id=anchor.episode_id, - decision_epoch=_behavior_decision_epoch(events), - event_ref=event_refs[-1], - state_cut=_sequence_cut( - history_domain="participant_behavior_history", - order_model="behavior_history_order", - event_refs=event_refs, - ), - event_type=events[-1].event_type.value, - action_instance_id=events[-1].action_instance_id, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) + resolved = _resolve_behavior_anchor(runtime_snapshot, anchor, runtime_model) if resolved != anchor: raise ValueError("participant decision-surface v2 derivation anchor is stale or does not resolve") diff --git a/implementations/python/packages/raes_processor/models/decision_surface_v2.py b/implementations/python/packages/raes_processor/models/decision_surface_v2.py index 1e526c0db..8c9266f4d 100644 --- a/implementations/python/packages/raes_processor/models/decision_surface_v2.py +++ b/implementations/python/packages/raes_processor/models/decision_surface_v2.py @@ -29,6 +29,7 @@ _surface_action_assessments, ) from .decision_surface_anchor_v2 import ( + ParticipantBehaviorProjectionAnchorRequestV2, resolve_participant_behavior_projection_anchor_v2, resolve_participant_episode_readiness_anchor_v2, ) @@ -94,13 +95,10 @@ def _resolve_projection_scope( return behavior, boundary -def _validate_and_resolve_anchor( - runtime_model: RuntimeModel, - runtime_snapshot: RuntimeSnapshot, - history_events: Sequence[ParticipantBehaviorHistoryEvent], +def _validate_anchor_projection_agreement( + anchor: ParticipantDecisionSurfaceDerivationAnchorV2Model, projection: ParticipantDecisionSurfaceProjectionInputV2, -) -> int | None: - anchor = projection.derivation_anchor +) -> None: coordinates = ( ("participant_address", anchor.participant_address, projection.participant_address), ("episode_id", anchor.episode_id, projection.episode_id), @@ -109,28 +107,49 @@ def _validate_and_resolve_anchor( mismatched = [name for name, anchor_value, projected_value in coordinates if anchor_value != projected_value] if mismatched: raise ValueError("derivation anchor disagrees with projection input on: " + ", ".join(mismatched)) + + +def _validate_anchor_assurance_carriage( + anchor: ParticipantDecisionSurfaceDerivationAnchorV2Model, + projection: ParticipantDecisionSurfaceProjectionInputV2, +) -> None: if not set(anchor.evidence_refs).issubset(projection.evidence_refs): raise ValueError("derivation anchor evidence must be carried by assurance") if not set(anchor.provenance_refs).issubset(projection.provenance_refs): raise ValueError("derivation anchor provenance must be carried by assurance") - if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model): - resolved = resolve_participant_episode_readiness_anchor_v2( - runtime_snapshot, - participant_address=projection.participant_address, - decision_epoch=projection.decision_epoch, - evidence_refs=anchor.evidence_refs, - provenance_refs=anchor.provenance_refs, - ) - if history_events: - raise ValueError("initial decision epoch requires empty current-episode behavior history") - if resolved != anchor: - raise ValueError("episode-readiness anchor does not match the current trusted snapshot") - return None + + +def _resolve_readiness_projection_anchor( + runtime_snapshot: RuntimeSnapshot, + history_events: Sequence[ParticipantBehaviorHistoryEvent], + projection: ParticipantDecisionSurfaceProjectionInputV2, +) -> None: + anchor = projection.derivation_anchor + assert isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model) + resolved = resolve_participant_episode_readiness_anchor_v2( + runtime_snapshot, + participant_address=projection.participant_address, + decision_epoch=projection.decision_epoch, + evidence_refs=anchor.evidence_refs, + provenance_refs=anchor.provenance_refs, + ) + if history_events: + raise ValueError("initial decision epoch requires empty current-episode behavior history") + if resolved != anchor: + raise ValueError("episode-readiness anchor does not match the current trusted snapshot") + + +def _resolve_behavior_projection_anchor( + runtime_model: RuntimeModel, + runtime_snapshot: RuntimeSnapshot, + history_events: Sequence[ParticipantBehaviorHistoryEvent], + projection: ParticipantDecisionSurfaceProjectionInputV2, +) -> int: + anchor = projection.derivation_anchor + assert not isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model) if not isinstance(anchor.state_cut, ParticipantDecisionSurfaceSequenceCutModel): raise ValueError("the reference projector cannot resolve a causal-frontier behavior anchor") - resolved = resolve_participant_behavior_projection_anchor_v2( - runtime_snapshot, - runtime_model=runtime_model, + request = ParticipantBehaviorProjectionAnchorRequestV2( participant_address=projection.participant_address, episode_id=projection.episode_id, decision_epoch=projection.decision_epoch, @@ -138,6 +157,11 @@ def _validate_and_resolve_anchor( evidence_refs=anchor.evidence_refs, provenance_refs=anchor.provenance_refs, ) + resolved = resolve_participant_behavior_projection_anchor_v2( + runtime_snapshot, + runtime_model=runtime_model, + request=request, + ) if resolved != anchor: raise ValueError("behavior derivation anchor does not match the current trusted snapshot") current = tuple( @@ -150,6 +174,26 @@ def _validate_and_resolve_anchor( return anchor.state_cut.anchor_order +def _validate_and_resolve_anchor( + runtime_model: RuntimeModel, + runtime_snapshot: RuntimeSnapshot, + history_events: Sequence[ParticipantBehaviorHistoryEvent], + projection: ParticipantDecisionSurfaceProjectionInputV2, +) -> int | None: + anchor = projection.derivation_anchor + _validate_anchor_projection_agreement(anchor, projection) + _validate_anchor_assurance_carriage(anchor, projection) + if isinstance(anchor, ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model): + _resolve_readiness_projection_anchor(runtime_snapshot, history_events, projection) + return None + return _resolve_behavior_projection_anchor( + runtime_model, + runtime_snapshot, + history_events, + projection, + ) + + def _visibility_relation( history_events: Sequence[ParticipantBehaviorHistoryEvent], *, diff --git a/implementations/python/packages/raes_processor/models/participant_exposure_v2.py b/implementations/python/packages/raes_processor/models/participant_exposure_v2.py index afc52d588..2dfc10dc3 100644 --- a/implementations/python/packages/raes_processor/models/participant_exposure_v2.py +++ b/implementations/python/packages/raes_processor/models/participant_exposure_v2.py @@ -111,11 +111,10 @@ def _resolve_authorization( return authorization -def _validate_authorization( +def _validate_authorization_coordinates( authorization: ParticipantExposureAuthorizationRecordV2, projection: ParticipantExposureProjectionV2, policy: ParticipantExposurePolicyModel, - relation: Mapping[str, str], ) -> None: exact_coordinates = { "participant_address": projection.participant_address, @@ -139,10 +138,22 @@ def _validate_authorization( f"exposure authorization {authorization.item_ref!r} has mismatched exact-cut coordinates: " + ", ".join(mismatches) ) + + +def _validate_authorization_visibility_and_policy( + authorization: ParticipantExposureAuthorizationRecordV2, + relation: Mapping[str, str], + policy: ParticipantExposurePolicyModel, +) -> None: if relation.get(authorization.item_ref) not in _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS: raise ValueError(f"exposure item {authorization.item_ref!r} is not participant-visible at the state cut") if not _policy_permits_item(policy, authorization.item_ref): raise ValueError(f"selected exposure policy does not permit item {authorization.item_ref!r}") + + +def _validate_authorization_authority_refs( + authorization: ParticipantExposureAuthorizationRecordV2, +) -> None: required_strings = ( authorization.source_ref, authorization.source_layer_ref, @@ -155,6 +166,11 @@ def _validate_authorization( ) if any(not value for value in required_strings): raise ValueError("participant exposure authorization requires non-empty authority refs") + + +def _validate_authorization_ref_collections( + authorization: ParticipantExposureAuthorizationRecordV2, +) -> None: for field_name in ( "source_marking_definition_refs", "result_marking_definition_refs", @@ -169,6 +185,11 @@ def _validate_authorization( raise ValueError(f"exposure authorization {authorization.item_ref!r} requires {field_name}") if len(values) != len(set(values)) or any(not value for value in values): raise ValueError(f"exposure authorization {authorization.item_ref!r} has invalid {field_name}") + + +def _validate_authorization_transformation( + authorization: ParticipantExposureAuthorizationRecordV2, +) -> None: if authorization.source_ref != authorization.item_ref and authorization.transformation_rule_ref is None: raise ValueError("derived exposure items require a transformation rule") if authorization.operation in {"masking", "redaction", "transformation"} and ( @@ -179,6 +200,11 @@ def _validate_authorization( raise ValueError("redaction exposure operations require a redaction policy") if authorization.operation == "declassification" and authorization.declassification_basis_ref is None: raise ValueError("declassification exposure operations require a declassification basis") + + +def _validate_authorization_inheritance( + authorization: ParticipantExposureAuthorizationRecordV2, +) -> None: if authorization.declassification_basis_ref is None and not set( authorization.source_marking_definition_refs ).issubset(authorization.result_marking_definition_refs): @@ -187,10 +213,21 @@ def _validate_authorization( authorization.result_provenance_refs ): raise ValueError("derived exposure results must inherit source provenance unless declassification is explicit") + + +def _validate_authorization_provenance_carriage( + authorization: ParticipantExposureAuthorizationRecordV2, +) -> None: if not {*authorization.source_provenance_refs, *authorization.result_provenance_refs}.issubset( authorization.provenance_refs ): raise ValueError("source and result exposure provenance must be carried by provenance_refs") + + +def _validate_authorization_assurance_carriage( + authorization: ParticipantExposureAuthorizationRecordV2, + projection: ParticipantExposureProjectionV2, +) -> None: carried = ( ("evidence", authorization.evidence_refs, projection.evidence_refs), ("provenance", authorization.provenance_refs, projection.provenance_refs), @@ -199,6 +236,12 @@ def _validate_authorization( for label, refs, carrier in carried: if not set(refs).issubset(carrier): raise ValueError(f"exposure authorization {authorization.item_ref!r} {label} must be carried by assurance") + + +def _validate_authorization_redaction_policy( + authorization: ParticipantExposureAuthorizationRecordV2, + projection: ParticipantExposureProjectionV2, +) -> None: if ( authorization.redaction_policy_ref is not None and authorization.redaction_policy_ref != projection.redaction_policy_ref @@ -206,6 +249,23 @@ def _validate_authorization( raise ValueError("exposure authorization redaction policy must match the participant view") +def _validate_authorization( + authorization: ParticipantExposureAuthorizationRecordV2, + projection: ParticipantExposureProjectionV2, + policy: ParticipantExposurePolicyModel, + relation: Mapping[str, str], +) -> None: + _validate_authorization_coordinates(authorization, projection, policy) + _validate_authorization_visibility_and_policy(authorization, relation, policy) + _validate_authorization_authority_refs(authorization) + _validate_authorization_ref_collections(authorization) + _validate_authorization_transformation(authorization) + _validate_authorization_inheritance(authorization) + _validate_authorization_provenance_carriage(authorization) + _validate_authorization_assurance_carriage(authorization, projection) + _validate_authorization_redaction_policy(authorization, projection) + + def _binding_payload( authorization: ParticipantExposureAuthorizationRecordV2, projection: ParticipantExposureProjectionV2, diff --git a/implementations/python/tests/test_sem_220_participant_decision_surface_v2.py b/implementations/python/tests/test_sem_220_participant_decision_surface_v2.py index c5b685499..58355b19b 100644 --- a/implementations/python/tests/test_sem_220_participant_decision_surface_v2.py +++ b/implementations/python/tests/test_sem_220_participant_decision_surface_v2.py @@ -350,9 +350,10 @@ def test_v2_is_closed_against_legacy_or_hidden_order_fields() -> None: payload = _projected_surface().model_dump(mode="json") payload["observation_order"] = 0 payload["participant_view"]["anchor_order"] = 1 + invalid_payload = copy.deepcopy(payload) with pytest.raises(ValidationError): - ParticipantDecisionSurfaceV2Model.model_validate(copy.deepcopy(payload)) + ParticipantDecisionSurfaceV2Model.model_validate(invalid_payload) def test_v2_published_schema_and_fixtures_match_the_contract_model() -> None: diff --git a/implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py b/implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py index 5f4e3b32a..b76e552ca 100644 --- a/implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py +++ b/implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py @@ -14,6 +14,7 @@ from raes_contracts.participant_decision_surface_delivery import deliver_participant_decision_surface_v2 from raes_processor.models import ( ParticipantBehaviorHistoryEvent, + ParticipantBehaviorProjectionAnchorRequestV2, ParticipantBehaviorRuntime, ParticipantDecisionSurfaceProjectionInputV2, ParticipantExposureAssessment, @@ -437,10 +438,11 @@ def test_projection_policy_must_be_the_decision_at_the_exact_state_cut() -> None ) projection = _projection(anchor) resolvers, _ = _exposure_resolvers(projection, resolved_cut_ref="participant-state-cuts.stale") + runtime_model = _runtime_model() with pytest.raises(ValueError, match="decision_cut_ref"): project_participant_decision_surface_v2( - _runtime_model(), + runtime_model, snapshot, history_events=(), projection=projection, @@ -525,12 +527,14 @@ def test_later_epoch_uses_behavior_cut_without_reinterpreting_decision_epoch_as_ anchor = resolve_participant_behavior_projection_anchor_v2( snapshot, runtime_model=runtime_model, - participant_address=PARTICIPANT, - episode_id=EPISODE, - decision_epoch=1, - behavior_history_order=2, - evidence_refs=("evidence.scan-result",), - provenance_refs=("provenance.runtime-control-plane",), + request=ParticipantBehaviorProjectionAnchorRequestV2( + participant_address=PARTICIPANT, + episode_id=EPISODE, + decision_epoch=1, + behavior_history_order=2, + evidence_refs=("evidence.scan-result",), + provenance_refs=("provenance.runtime-control-plane",), + ), ) projection = _projection(anchor) resolvers, _ = _exposure_resolvers(projection) From f17a366cb08c1b4a1b04b0d423b8962debf20fb0 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 19:59:52 +0200 Subject: [PATCH 18/55] Add autonomous participant activity policy --- .../backend-manifest-v2/valid/stub.json | 6 + .../valid/blake3-xof-participant-v1.json | 34 + .../participant-occurrence-bounded-draw.json | 36 + .../blake3-xof-participant-v1.json | 34 + .../provenance/sdl-lineage-ledger-v1.json | 7 +- .../entries/backend-manifest-v2.json | 6 +- .../instantiated-scenario-snapshot-v1.json | 6 +- .../entries/instantiated-scenario-v1.json | 6 +- ...pant-behavior-history-event-stream-v1.json | 6 +- .../entries/participant-history-view-v1.json | 6 +- .../entries/random-stream-vector-v1.json | 6 +- .../entries/runtime-snapshot-v1.json | 6 +- .../scenario-satisfiability-evidence-v1.json | 6 +- .../entries/sdl-authoring-input-v1.json | 6 +- .../backend-manifest/backend-manifest-v2.json | 83 +- ...pant-behavior-history-event-stream-v1.json | 204 ++++- .../participant-history-view-v1.json | 204 ++++- .../profiles/random-stream-vector-v1.json | 86 ++- .../scenario-satisfiability-evidence-v1.json | 244 +++++- .../instantiated-scenario-snapshot-v1.json | 275 ++++++- .../schemas/sdl/instantiated-scenario-v1.json | 275 ++++++- .../schemas/sdl/sdl-authoring-input-v1.json | 244 +++++- .../snapshots/runtime-snapshot-v1.json | 302 ++++++++ ...s-benign-participants-under-shared-time.md | 58 ++ docs/decisions/adrs/adr-index.yaml | 5 +- ...97-autonomous-activity-policy-preflight.md | 415 ++++++++++ docs/explain/reference/backend-conformance.md | 15 +- docs/migration/README.md | 4 + docs/migration/autonomous-execution-v2.md | 73 ++ .../lineage/source-audit-2026-07-12.md | 18 + .../packages/raes/participant_execution.py | 123 ++- .../raes/semantics/participant_behavior.py | 64 +- .../_participant_execution_renderers.py | 18 + .../capability_admission.py | 48 +- .../raes_backend_protocols/manifest.py | 20 + .../participant_capabilities.py | 21 +- .../raes_contracts/contracts/__init__.py | 4 +- .../raes_contracts/contracts/manifests.py | 114 ++- .../contracts/participant_runtime.py | 71 +- .../contracts/participant_views.py | 2 + .../raes_contracts/contracts/random_stream.py | 43 +- .../contracts/time_manifest_capabilities.py | 77 ++ .../raes_contracts/random_stream_engine.py | 15 +- .../raes_contracts/random_stream_profiles.py | 2 +- .../participant_autonomous_execution.py | 163 ++++ .../compiler/participant_behaviors.py | 102 +-- .../models/behavior_resources.py | 16 + .../python/packages/raes_runtime/manager.py | 5 + .../raes_runtime/participant_activity.py | 296 +++++++ .../participant_activity_support.py | 154 ++++ .../participant_execution_control.py | 4 + .../raes_runtime/participant_scheduler.py | 143 +++- .../participant_scheduler_operations.py | 269 ++++++- .../participant_scheduler_time.py | 33 + ...st_dsl_437_benign_participant_execution.py | 721 +++++++++++++++++- .../tests/test_random_stream_determinism.py | 82 +- .../tests/test_random_stream_profile.py | 12 +- .../tests/test_random_stream_vectors.py | 11 +- .../autonomous-execution.md | 91 ++- 59 files changed, 5108 insertions(+), 292 deletions(-) create mode 100644 contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-participant-v1.json create mode 100644 contracts/fixtures/random-stream-vectors/blake3-xof-participant-v1/participant-occurrence-bounded-draw.json create mode 100644 contracts/profiles/random-stream/blake3-xof-participant-v1.json create mode 100644 docs/decisions/issue-897-autonomous-activity-policy-preflight.md create mode 100644 docs/migration/autonomous-execution-v2.md create mode 100644 implementations/python/packages/raes_contracts/contracts/time_manifest_capabilities.py create mode 100644 implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py create mode 100644 implementations/python/packages/raes_runtime/participant_activity.py create mode 100644 implementations/python/packages/raes_runtime/participant_activity_support.py create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_time.py 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 21cc7cbb6..3fccd6a87 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -127,11 +127,17 @@ "constraints": {}, "feature_support": [], "max_autonomous_action_attempts": null, + "max_autonomous_burst_size": null, "max_autonomous_in_flight": null, + "max_autonomous_occurrences": null, "max_autonomous_participants": null, + "max_autonomous_retries_per_occurrence": null, "name": "stub-participant-runtime", "supported_autonomous_action_contracts": [], + "supported_autonomous_activity_features": [], "supported_autonomous_observation_boundaries": [], + "supported_autonomous_policy_profiles": [], + "supported_autonomous_random_stream_profiles": [], "supported_autonomous_selection_strategies": [], "supported_autonomous_target_addresses": [], "supported_behavior_features": [ diff --git a/contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-participant-v1.json b/contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-participant-v1.json new file mode 100644 index 000000000..f28ef52b2 --- /dev/null +++ b/contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-participant-v1.json @@ -0,0 +1,34 @@ +{ + "schema_version": "random-stream-profile/v1", + "profile_id": "blake3-xof-participant-v1", + "title": "BLAKE3 Participant Occurrence Random Stream Profile", + "description": "Immutable counter-addressable profile for bounded within-run participant timing and weighted action policy draws. It uses a closed participant occurrence address and the unbiased bounded-integer transform. Cryptographic secrecy is not a claim.", + "generator": { + "family": "blake3", + "version": "1", + "mode": "keyed-hash-derive-key-seekable-xof" + }, + "root_entropy": { + "encoding": "hex-fixed-width", + "byte_length": 32 + }, + "address_encoding": { + "canonicalization": "rfc8785-jcs" + }, + "derivation": { + "key_derivation_function": "blake3.derive_key", + "context_template": "aces-random-stream-v1|profile={profile_id}" + }, + "block_encoding": { + "block_bytes": 32, + "byte_order": "big-endian" + }, + "transforms": { + "bounded-integer": { + "transform_id": "bounded-integer", + "version": "1", + "kind": "bounded-integer", + "max_rejection_attempts": 32 + } + } +} diff --git a/contracts/fixtures/random-stream-vectors/blake3-xof-participant-v1/participant-occurrence-bounded-draw.json b/contracts/fixtures/random-stream-vectors/blake3-xof-participant-v1/participant-occurrence-bounded-draw.json new file mode 100644 index 000000000..634327443 --- /dev/null +++ b/contracts/fixtures/random-stream-vectors/blake3-xof-participant-v1/participant-occurrence-bounded-draw.json @@ -0,0 +1,36 @@ +{ + "schema_version": "random-stream-vector/v1", + "vector_id": "participant-occurrence-bounded-draw", + "description": "Participant occurrence address and bounded-integer draw over [0, 9], independently computed with direct rfc8785 and blake3 library calls.", + "profile_id": "blake3-xof-participant-v1", + "root_entropy": { + "kind": "public-seed", + "encoding": "hex-fixed-width", + "value": "4242424242424242424242424242424242424242424242424242424242424242" + }, + "stream_key_hex": "19a95b495cd0de909a71985c7282a1bc3b9df2c1c38c9a1f05cfe0d83a5b7e27", + "address": { + "namespace": "green-activity", + "policy_address": "participant.autonomous-execution.participant-behavior", + "participant_address": "participant.behavior.participant-agent", + "time_segment": 2, + "occurrence_ordinal": 7, + "draw_purpose": "agent-policy", + "local_coordinate": 1 + }, + "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a226167656e742d706f6c696379222c226c6f63616c5f636f6f7264696e617465223a312c226e616d657370616365223a22677265656e2d6163746976697479222c226f6363757272656e63655f6f7264696e616c223a372c227061727469636970616e745f61646472657373223a227061727469636970616e742e6265686176696f722e7061727469636970616e742d6167656e74222c22706f6c6963795f61646472657373223a227061727469636970616e742e6175746f6e6f6d6f75732d657865637574696f6e2e7061727469636970616e742d6265686176696f72222c2274696d655f7365676d656e74223a327d", + "raw_block_hex": "a2bb4b7bff79ea0df9145637889e34e97eb82479225ab2bd7912fd5e4b27b300", + "transform": { + "transform_id": "bounded-integer", + "transform_version": "1", + "minimum": 0, + "maximum": 9, + "max_rejection_attempts": 32, + "expected_rejection_attempts": 0, + "expected_rejection_exhausted": false, + "outcome": { + "kind": "public-value", + "value": "2" + } + } +} diff --git a/contracts/profiles/random-stream/blake3-xof-participant-v1.json b/contracts/profiles/random-stream/blake3-xof-participant-v1.json new file mode 100644 index 000000000..f28ef52b2 --- /dev/null +++ b/contracts/profiles/random-stream/blake3-xof-participant-v1.json @@ -0,0 +1,34 @@ +{ + "schema_version": "random-stream-profile/v1", + "profile_id": "blake3-xof-participant-v1", + "title": "BLAKE3 Participant Occurrence Random Stream Profile", + "description": "Immutable counter-addressable profile for bounded within-run participant timing and weighted action policy draws. It uses a closed participant occurrence address and the unbiased bounded-integer transform. Cryptographic secrecy is not a claim.", + "generator": { + "family": "blake3", + "version": "1", + "mode": "keyed-hash-derive-key-seekable-xof" + }, + "root_entropy": { + "encoding": "hex-fixed-width", + "byte_length": 32 + }, + "address_encoding": { + "canonicalization": "rfc8785-jcs" + }, + "derivation": { + "key_derivation_function": "blake3.derive_key", + "context_template": "aces-random-stream-v1|profile={profile_id}" + }, + "block_encoding": { + "block_bytes": 32, + "byte_order": "big-endian" + }, + "transforms": { + "bounded-integer": { + "transform_id": "bounded-integer", + "version": "1", + "kind": "bounded-integer", + "max_rejection_attempts": 32 + } + } +} diff --git a/contracts/provenance/sdl-lineage-ledger-v1.json b/contracts/provenance/sdl-lineage-ledger-v1.json index f370e2945..582863211 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-24", + "reviewed_on": "2026-07-26", "citations": [ { "citation_id": "ocr-sdl-v0.21.2", @@ -846,7 +846,7 @@ "symbol_or_pointer": "sections 7.2 and 8.4: storyboard/entity/action/trigger lifecycle and execution" } ], - "divergence": "ACES adapts only the participant-environment, multi-participant ordering, shared-time coordination, and clock-jump callback concerns. The nested autonomous-execution policy, green-role restriction, evaluation-authority boundary, canonical references, reachable-cadence rule, runtime-owned wall pacing, capability admission, and clock/episode/scheduler readback contracts are ACES-defined. Externally paced autonomous execution remains unsupported pending a portable transition-notification contract. ACES adopts no source syntax, actor ontology, scheduler algorithm, clock protocol, federation middleware, automotive hierarchy, or wire compatibility.", + "divergence": "ACES adapts only the participant-environment, multi-participant ordering, shared-time coordination, and clock-jump callback concerns. The nested autonomous-execution profiles, green-role restriction, evaluation-authority boundary, canonical references, reachable cadence and activity timing rules, work/pause window algebra, weighted candidate/dependency/retry/cooldown/burst policy, participant-occurrence random address, runtime-owned wall pacing, exact capability admission, and clock/episode/scheduler/provenance readback contracts are ACES-defined. V2 reuses ACES's separately governed random-stream engine and bounded-integer transform but remains within-run participant execution, not trial variation. Externally paced autonomous execution remains unsupported pending a portable transition-notification contract. ACES adopts no source syntax, actor ontology, scheduler algorithm, clock protocol, federation middleware, automotive hierarchy, RNG API, or wire compatibility.", "compatibility": "none", "citation_refs": [ "cyborg-2021", @@ -861,9 +861,12 @@ ], "internal_authority_refs": [ "specs/sdl/sections.md", + "docs/decisions/adrs/adr-084-scenario-variation-and-deterministic-trial-realization.md", "docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md", "specs/formal/participant-semantics/autonomous-execution.md", + "specs/formal/scenario-variation-trial-realization/README.md", "docs/research/participant-backend-contracts/prior-art-and-design-criteria.md", + "docs/research/scenario-variation-trial-realization/prior-art-and-design-criteria.md", "docs/research/time-model/prior-art-and-design-criteria.md" ], "compatibility_direction": "aces_relative_to_source" diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index f5d62636a..698d9bd91 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": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48", + "content_hash": "56cd3a5d5f5f08618066975f449c6c86109c4c7cb8230039034db7f41deefcb5", "last_change": { - "summary": "Added a typed, owner-governed apparatus configuration target registry and experiment binding contract support.", - "content_hash": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48" + "summary": "Added exact autonomous participant policy-profile, activity-feature, random-stream-profile, occurrence, retry, and burst capability declarations for issue #897.", + "content_hash": "56cd3a5d5f5f08618066975f449c6c86109c4c7cb8230039034db7f41deefcb5" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json index 31b28e636..2beb2f884 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": "6833f448f35ead054ba95e4a621309c320b5bc227e32d2e12472f276482e5f59", + "content_hash": "40acc1309750eaaec3057ec2d9e2dd15e7023bb5658396291cc8e66620682659", "last_change": { - "summary": "Published participant-directed inject deliveries and governed participant action-argument domains in immutable scenario snapshots on the RAES-only package surface.", - "content_hash": "6833f448f35ead054ba95e4a621309c320b5bc227e32d2e12472f276482e5f59" + "summary": "Published the explicit autonomous activity v2 policy union in immutable instantiated scenario snapshots for issue #897.", + "content_hash": "40acc1309750eaaec3057ec2d9e2dd15e7023bb5658396291cc8e66620682659" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-v1.json b/contracts/schema-publication/entries/instantiated-scenario-v1.json index cfccdfdec..4ddcd0f1c 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": "1017a5db698151a50081675174b6c0f3cfb3eb2f22ec0532cd7334f7ee3ec76a", + "content_hash": "dd58e333371ee8996f17111cd89864edc9afab3a01428db1283db86b48ca9e08", "last_change": { - "summary": "Published participant-directed inject deliveries and governed participant action-argument domains in instantiated scenarios on the RAES-only package surface.", - "content_hash": "1017a5db698151a50081675174b6c0f3cfb3eb2f22ec0532cd7334f7ee3ec76a" + "summary": "Published the explicit autonomous activity v2 policy union in instantiated scenarios for issue #897.", + "content_hash": "dd58e333371ee8996f17111cd89864edc9afab3a01428db1283db86b48ca9e08" } } diff --git a/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json b/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json index 90cc7d6ca..c5d23bc0b 100644 --- a/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json +++ b/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-behavior-history-event-stream-v1", "schema_path": "contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json", "stability": "draft", - "content_hash": "4aeeaba0d4834a38bed72aca834733806fe8625d763b15e7d2dba764637f4234", + "content_hash": "d6169c0d811857ac5c2bea7a3db471e105a365a0eb0d3ce20785d1c413bacb03", "last_change": { - "summary": "Revised the participant behavior-history event-stream contract as part of the backend-facing contract family publication (ADR-060, issue #76).", - "content_hash": "4aeeaba0d4834a38bed72aca834733806fe8625d763b15e7d2dba764637f4234" + "summary": "Added safe typed autonomous activity occurrence, attempt, timing, dependency, burst, and random-address provenance for issue #897.", + "content_hash": "d6169c0d811857ac5c2bea7a3db471e105a365a0eb0d3ce20785d1c413bacb03" } } diff --git a/contracts/schema-publication/entries/participant-history-view-v1.json b/contracts/schema-publication/entries/participant-history-view-v1.json index 04f76e8d1..ac15213ed 100644 --- a/contracts/schema-publication/entries/participant-history-view-v1.json +++ b/contracts/schema-publication/entries/participant-history-view-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-history-view-v1", "schema_path": "contracts/schemas/control-plane/participant-history-view-v1.json", "stability": "draft", - "content_hash": "380257f288c098c0b94e343dbeb6d51c5216c6b14d91543cc2c147dd06633435", + "content_hash": "1fbe4794585f8ae1c71ba85a2cb7249227163301046ddbb8a2172c1bc201ccaa", "last_change": { - "summary": "Initial publication of the participant history-view control-plane contract: the participant-local interaction-history projection in the backend-facing contract family (ADR-060, issue #76).", - "content_hash": "380257f288c098c0b94e343dbeb6d51c5216c6b14d91543cc2c147dd06633435" + "summary": "Projected safe autonomous activity occurrence provenance through API-408 participant history views for issue #897.", + "content_hash": "1fbe4794585f8ae1c71ba85a2cb7249227163301046ddbb8a2172c1bc201ccaa" } } diff --git a/contracts/schema-publication/entries/random-stream-vector-v1.json b/contracts/schema-publication/entries/random-stream-vector-v1.json index 6e1df2df4..da03f8c8d 100644 --- a/contracts/schema-publication/entries/random-stream-vector-v1.json +++ b/contracts/schema-publication/entries/random-stream-vector-v1.json @@ -2,9 +2,9 @@ "contract_id": "random-stream-vector-v1", "schema_path": "contracts/schemas/profiles/random-stream-vector-v1.json", "stability": "draft", - "content_hash": "211900771cefa8c7e21b6194850d776a367a32afa9b270bc4f9a71798c58476e", + "content_hash": "c3ed8e8febd8dcf705f897a149e15bf3e8e5fddc479b3fba1d8143460f4420ca", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "211900771cefa8c7e21b6194850d776a367a32afa9b270bc4f9a71798c58476e" + "summary": "Added the closed participant-occurrence stream-address variant to canonical random-stream vectors for issue #897.", + "content_hash": "c3ed8e8febd8dcf705f897a149e15bf3e8e5fddc479b3fba1d8143460f4420ca" } } diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index 3ca616461..b237cabd4 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": "ce74f9bb39d44c18badc1451d7f4d363d58e3938de8e360dc020ea2d1e4bcdf7", + "content_hash": "d0b31b768af1ebd963e590429e0d38f4b16e816f25c59b95703d3166d463985b", "last_change": { - "summary": "Added first-class append-only API-409 participant control history for RUN-310 supervisory lifecycle persistence and replay.", - "content_hash": "ce74f9bb39d44c18badc1451d7f4d363d58e3938de8e360dc020ea2d1e4bcdf7" + "summary": "Added typed autonomous activity v2 continuation and safe occurrence provenance to runtime snapshots for issue #897.", + "content_hash": "d0b31b768af1ebd963e590429e0d38f4b16e816f25c59b95703d3166d463985b" } } diff --git a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json index 74fe6fb5e..cf2f01305 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": "01e502976847abfed1d37f7b6e972971d9109fac34e4e94ef31b5971a0a93032", + "content_hash": "cc580a03334f7f1d248ccb17d7151fdb9ed8cfc2f8abfe89d2f6462c2b489854", "last_change": { - "summary": "Carried participant-directed inject deliveries and governed participant action-argument domains into satisfiability evidence on the RAES-only package surface.", - "content_hash": "01e502976847abfed1d37f7b6e972971d9109fac34e4e94ef31b5971a0a93032" + "summary": "Carried the explicit autonomous activity v2 policy union through satisfiability evidence scenario definitions for issue #897.", + "content_hash": "cc580a03334f7f1d248ccb17d7151fdb9ed8cfc2f8abfe89d2f6462c2b489854" } } diff --git a/contracts/schema-publication/entries/sdl-authoring-input-v1.json b/contracts/schema-publication/entries/sdl-authoring-input-v1.json index 57098c56a..2facbde91 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": "65ae337e1fbe2a4b66e8d0cbbb98c66c172360d328fd82c5352a223a1e6429cc", + "content_hash": "adc8e52852a98d07b14cfa48d0dc78a57935b6cf25d4777eda271d75ec68485d", "last_change": { - "summary": "Published closed participant-directed inject delivery and governed participant action-argument authoring semantics on the RAES-only package surface.", - "content_hash": "65ae337e1fbe2a4b66e8d0cbbb98c66c172360d328fd82c5352a223a1e6429cc" + "summary": "Published the explicit autonomous activity v2 authoring profile with governed windows, timing, weighted candidates, retries, cooldowns, and bursts for issue #897.", + "content_hash": "adc8e52852a98d07b14cfa48d0dc78a57935b6cf25d4777eda271d75ec68485d" } } diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index 11b6c1de7..cb0d8a608 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -815,6 +815,19 @@ "default": null, "title": "Max Autonomous Action Attempts" }, + "max_autonomous_burst_size": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Autonomous Burst Size" + }, "max_autonomous_in_flight": { "anyOf": [ { @@ -828,6 +841,19 @@ "default": null, "title": "Max Autonomous In Flight" }, + "max_autonomous_occurrences": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Autonomous Occurrences" + }, "max_autonomous_participants": { "anyOf": [ { @@ -841,6 +867,19 @@ "default": null, "title": "Max Autonomous Participants" }, + "max_autonomous_retries_per_occurrence": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Autonomous Retries Per Occurrence" + }, "name": { "minLength": 1, "title": "Name", @@ -855,6 +894,24 @@ "type": "array", "uniqueItems": true }, + "supported_autonomous_activity_features": { + "items": { + "enum": [ + "work-windows", + "timing-variation", + "weighted-selection", + "dependencies", + "bounded-retries", + "cooldowns", + "limited-bursts", + "occurrence-provenance" + ], + "type": "string" + }, + "title": "Supported Autonomous Activity Features", + "type": "array", + "uniqueItems": true + }, "supported_autonomous_observation_boundaries": { "items": { "minLength": 1, @@ -864,9 +921,33 @@ "type": "array", "uniqueItems": true }, + "supported_autonomous_policy_profiles": { + "items": { + "enum": [ + "participant-autonomous-execution/v1", + "participant-autonomous-execution/v2" + ], + "type": "string" + }, + "title": "Supported Autonomous Policy Profiles", + "type": "array", + "uniqueItems": true + }, + "supported_autonomous_random_stream_profiles": { + "items": { + "const": "blake3-xof-participant-v1", + "type": "string" + }, + "title": "Supported Autonomous Random Stream Profiles", + "type": "array", + "uniqueItems": true + }, "supported_autonomous_selection_strategies": { "items": { - "const": "ordered_cycle", + "enum": [ + "ordered_cycle", + "weighted" + ], "type": "string" }, "title": "Supported Autonomous Selection Strategies", diff --git a/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json b/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json index 5e3808aa0..d405b5031 100644 --- a/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json +++ b/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json @@ -264,6 +264,112 @@ "title": "ParticipantActionResultModel", "type": "object" }, + "ParticipantActivityOccurrenceProvenanceModel": { + "additionalProperties": false, + "description": "Safe within-run scheduler provenance for one native action attempt.", + "properties": { + "attempt_id": { + "minLength": 1, + "title": "Attempt Id", + "type": "string" + }, + "burst_position": { + "minimum": 0, + "title": "Burst Position", + "type": "integer" + }, + "candidate_id": { + "minLength": 1, + "title": "Candidate Id", + "type": "string" + }, + "dependency_candidate_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Dependency Candidate Ids", + "type": "array" + }, + "occurrence_id": { + "minLength": 1, + "title": "Occurrence Id", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "policy_profile": { + "const": "participant-autonomous-execution/v2", + "title": "Policy Profile", + "type": "string" + }, + "predecessor_attempt_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Predecessor Attempt Id" + }, + "random_address": { + "$ref": "#/$defs/ParticipantStreamAddressModel" + }, + "random_control_id": { + "minLength": 1, + "title": "Random Control Id", + "type": "string" + }, + "random_profile_id": { + "minLength": 1, + "title": "Random Profile Id", + "type": "string" + }, + "terminal_outcome": { + "minLength": 1, + "title": "Terminal Outcome", + "type": "string" + }, + "timing_disposition": { + "enum": [ + "drawn", + "next_opening", + "retry", + "burst" + ], + "title": "Timing Disposition", + "type": "string" + }, + "timing_tick": { + "minimum": 0, + "title": "Timing Tick", + "type": "integer" + } + }, + "required": [ + "policy_address", + "policy_profile", + "occurrence_id", + "attempt_id", + "candidate_id", + "timing_tick", + "timing_disposition", + "burst_position", + "random_control_id", + "random_profile_id", + "random_address", + "terminal_outcome" + ], + "title": "ParticipantActivityOccurrenceProvenanceModel", + "type": "object" + }, "ParticipantAdmissionDisposition": { "description": "RUN-306 selection/admission disposition values.", "enum": [ @@ -888,6 +994,82 @@ "title": "ParticipantRuntimeLifecyclePhase", "type": "string" }, + "ParticipantStreamAddressModel": { + "additionalProperties": false, + "description": "Closed within-run address for one participant-policy draw.", + "properties": { + "draw_purpose": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Draw Purpose", + "type": "string" + }, + "local_coordinate": { + "minimum": 0, + "title": "Local Coordinate", + "type": "integer" + }, + "namespace": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Namespace", + "type": "string" + }, + "occurrence_ordinal": { + "minimum": 0, + "title": "Occurrence Ordinal", + "type": "integer" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "time_segment": { + "minimum": 0, + "title": "Time Segment", + "type": "integer" + } + }, + "required": [ + "namespace", + "policy_address", + "participant_address", + "time_segment", + "occurrence_ordinal", + "draw_purpose", + "local_coordinate" + ], + "title": "ParticipantStreamAddressModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "draw_purpose must be governed and policy_address/participant_address must be compiled autonomous participant addresses.", + "id": "participant-random-stream-address-governed", + "inputs": [ + { + "contract_id": "controlled-vocabularies-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.random_stream.ParticipantStreamAddressModel._validate_address" + } + ] + }, "ParticipantTemporalEventPoint": { "description": "Named participant event points used by temporal contracts.", "enum": [ @@ -1026,6 +1208,17 @@ ], "default": null }, + "activity_provenance": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantActivityOccurrenceProvenanceModel" + }, + { + "type": "null" + } + ], + "default": null + }, "actor_provenance": { "anyOf": [ { @@ -1258,5 +1451,14 @@ "type": "object" }, "title": "ParticipantBehaviorHistoryEventStream", - "type": "array" + "type": "array", + "x-aces-semantic-profile": { + "contract_id": "participant-behavior-history-event-stream-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } } diff --git a/contracts/schemas/control-plane/participant-history-view-v1.json b/contracts/schemas/control-plane/participant-history-view-v1.json index 7fb10cb17..e5ec1033d 100644 --- a/contracts/schemas/control-plane/participant-history-view-v1.json +++ b/contracts/schemas/control-plane/participant-history-view-v1.json @@ -264,6 +264,112 @@ "title": "ParticipantActionResultModel", "type": "object" }, + "ParticipantActivityOccurrenceProvenanceModel": { + "additionalProperties": false, + "description": "Safe within-run scheduler provenance for one native action attempt.", + "properties": { + "attempt_id": { + "minLength": 1, + "title": "Attempt Id", + "type": "string" + }, + "burst_position": { + "minimum": 0, + "title": "Burst Position", + "type": "integer" + }, + "candidate_id": { + "minLength": 1, + "title": "Candidate Id", + "type": "string" + }, + "dependency_candidate_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Dependency Candidate Ids", + "type": "array" + }, + "occurrence_id": { + "minLength": 1, + "title": "Occurrence Id", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "policy_profile": { + "const": "participant-autonomous-execution/v2", + "title": "Policy Profile", + "type": "string" + }, + "predecessor_attempt_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Predecessor Attempt Id" + }, + "random_address": { + "$ref": "#/$defs/ParticipantStreamAddressModel" + }, + "random_control_id": { + "minLength": 1, + "title": "Random Control Id", + "type": "string" + }, + "random_profile_id": { + "minLength": 1, + "title": "Random Profile Id", + "type": "string" + }, + "terminal_outcome": { + "minLength": 1, + "title": "Terminal Outcome", + "type": "string" + }, + "timing_disposition": { + "enum": [ + "drawn", + "next_opening", + "retry", + "burst" + ], + "title": "Timing Disposition", + "type": "string" + }, + "timing_tick": { + "minimum": 0, + "title": "Timing Tick", + "type": "integer" + } + }, + "required": [ + "policy_address", + "policy_profile", + "occurrence_id", + "attempt_id", + "candidate_id", + "timing_tick", + "timing_disposition", + "burst_position", + "random_control_id", + "random_profile_id", + "random_address", + "terminal_outcome" + ], + "title": "ParticipantActivityOccurrenceProvenanceModel", + "type": "object" + }, "ParticipantAdmissionDisposition": { "description": "RUN-306 selection/admission disposition values.", "enum": [ @@ -599,6 +705,17 @@ ], "default": null }, + "activity_provenance": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantActivityOccurrenceProvenanceModel" + }, + { + "type": "null" + } + ], + "default": null + }, "actor_provenance": { "anyOf": [ { @@ -1194,6 +1311,82 @@ "title": "ParticipantRuntimeLifecyclePhase", "type": "string" }, + "ParticipantStreamAddressModel": { + "additionalProperties": false, + "description": "Closed within-run address for one participant-policy draw.", + "properties": { + "draw_purpose": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Draw Purpose", + "type": "string" + }, + "local_coordinate": { + "minimum": 0, + "title": "Local Coordinate", + "type": "integer" + }, + "namespace": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Namespace", + "type": "string" + }, + "occurrence_ordinal": { + "minimum": 0, + "title": "Occurrence Ordinal", + "type": "integer" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "time_segment": { + "minimum": 0, + "title": "Time Segment", + "type": "integer" + } + }, + "required": [ + "namespace", + "policy_address", + "participant_address", + "time_segment", + "occurrence_ordinal", + "draw_purpose", + "local_coordinate" + ], + "title": "ParticipantStreamAddressModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "draw_purpose must be governed and policy_address/participant_address must be compiled autonomous participant addresses.", + "id": "participant-random-stream-address-governed", + "inputs": [ + { + "contract_id": "controlled-vocabularies-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.random_stream.ParticipantStreamAddressModel._validate_address" + } + ] + }, "ParticipantTemporalEventPoint": { "description": "Named participant event points used by temporal contracts.", "enum": [ @@ -1431,5 +1624,14 @@ "completeness" ], "title": "ParticipantHistoryViewModel", - "type": "object" + "type": "object", + "x-aces-semantic-profile": { + "contract_id": "participant-history-view-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } } diff --git a/contracts/schemas/profiles/random-stream-vector-v1.json b/contracts/schemas/profiles/random-stream-vector-v1.json index 818fc4a82..2eb530935 100644 --- a/contracts/schemas/profiles/random-stream-vector-v1.json +++ b/contracts/schemas/profiles/random-stream-vector-v1.json @@ -28,6 +28,82 @@ "title": "GovernedRandomOutcomeRefModel", "type": "object" }, + "ParticipantStreamAddressModel": { + "additionalProperties": false, + "description": "Closed within-run address for one participant-policy draw.", + "properties": { + "draw_purpose": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Draw Purpose", + "type": "string" + }, + "local_coordinate": { + "minimum": 0, + "title": "Local Coordinate", + "type": "integer" + }, + "namespace": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Namespace", + "type": "string" + }, + "occurrence_ordinal": { + "minimum": 0, + "title": "Occurrence Ordinal", + "type": "integer" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "time_segment": { + "minimum": 0, + "title": "Time Segment", + "type": "integer" + } + }, + "required": [ + "namespace", + "policy_address", + "participant_address", + "time_segment", + "occurrence_ordinal", + "draw_purpose", + "local_coordinate" + ], + "title": "ParticipantStreamAddressModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "draw_purpose must be governed and policy_address/participant_address must be compiled autonomous participant addresses.", + "id": "participant-random-stream-address-governed", + "inputs": [ + { + "contract_id": "controlled-vocabularies-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.random_stream.ParticipantStreamAddressModel._validate_address" + } + ] + }, "PublicRandomOutcomeModel": { "additionalProperties": false, "description": "Public drawn value, recorded as canonical text.", @@ -301,7 +377,15 @@ "description": "One canonical cross-language conformance vector case.\n\nComputed independently of the reference engine (a throwaway script that\ncalls the ``blake3`` library directly), so the vector tests do not just\ntest the engine against itself.", "properties": { "address": { - "$ref": "#/$defs/StreamAddressModel" + "anyOf": [ + { + "$ref": "#/$defs/StreamAddressModel" + }, + { + "$ref": "#/$defs/ParticipantStreamAddressModel" + } + ], + "title": "Address" }, "address_canonical_bytes_hex": { "minLength": 1, diff --git a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json index fe0723eae..b8b5f6cff 100644 --- a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json +++ b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json @@ -5837,9 +5837,92 @@ "title": "ParticipantActionPrecondition", "type": "object" }, - "ParticipantAutonomousExecutionPolicy": { + "ParticipantActivityActionCandidate": { "additionalProperties": false, - "description": "Deterministic scheduler binding over existing participant semantics.", + "description": "Stable weighted action candidate and its bounded recovery policy.", + "properties": { + "action_ref": { + "minLength": 1, + "title": "Action Ref", + "type": "string" + }, + "cooldown_ticks": { + "default": 0, + "maximum": 1000000000, + "minimum": 0, + "title": "Cooldown Ticks", + "type": "integer" + }, + "depends_on": { + "items": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 1024, + "title": "Depends On", + "type": "array" + }, + "max_retries": { + "default": 0, + "maximum": 1024, + "minimum": 0, + "title": "Max Retries", + "type": "integer" + }, + "retryable_failure_classes": { + "items": { + "$ref": "#/$defs/ParticipantFailureClass" + }, + "maxItems": 32, + "title": "Retryable Failure Classes", + "type": "array" + }, + "weight": { + "maximum": 1000000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "action_ref", + "weight" + ], + "title": "ParticipantActivityActionCandidate", + "type": "object" + }, + "ParticipantActivityTiming": { + "additionalProperties": false, + "description": "Inclusive bounded interval for the next occurrence on the shared clock.", + "properties": { + "maximum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Maximum Ticks", + "type": "integer" + }, + "minimum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Minimum Ticks", + "type": "integer" + } + }, + "required": [ + "minimum_ticks", + "maximum_ticks" + ], + "title": "ParticipantActivityTiming", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV1": { + "additionalProperties": false, + "description": "Fixed-cadence deterministic scheduler binding.", "properties": { "action_order": { "items": { @@ -5920,7 +6003,154 @@ "max_action_attempts", "evaluation_authority" ], - "title": "ParticipantAutonomousExecutionPolicy", + "title": "ParticipantAutonomousExecutionPolicyV1", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV2": { + "additionalProperties": false, + "description": "Governed within-run activity policy for ordinary participants.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v2", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "title": "Progression Policy Ref", + "type": "string" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority" + ], + "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, "ParticipantBackendFailureMapping": { @@ -6031,13 +6261,17 @@ "autonomous_execution": { "anyOf": [ { - "$ref": "#/$defs/ParticipantAutonomousExecutionPolicy" + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV1" + }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, { "type": "null" } ], - "default": null + "default": null, + "title": "Autonomous Execution" }, "backend_feature_support_refs": { "items": { diff --git a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json index 02f26ecc7..45b1c9a90 100644 --- a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -7051,9 +7051,102 @@ "title": "ParticipantActionPrecondition", "type": "object" }, - "ParticipantAutonomousExecutionPolicy": { + "ParticipantActivityActionCandidate": { "additionalProperties": false, - "description": "Deterministic scheduler binding over existing participant semantics.", + "description": "Stable weighted action candidate and its bounded recovery policy.", + "properties": { + "action_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Action Ref", + "type": "string" + }, + "cooldown_ticks": { + "default": 0, + "maximum": 1000000000, + "minimum": 0, + "title": "Cooldown Ticks", + "type": "integer" + }, + "depends_on": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 1024, + "title": "Depends On", + "type": "array" + }, + "max_retries": { + "default": 0, + "maximum": 1024, + "minimum": 0, + "title": "Max Retries", + "type": "integer" + }, + "retryable_failure_classes": { + "items": { + "$ref": "#/$defs/ParticipantFailureClass" + }, + "maxItems": 32, + "title": "Retryable Failure Classes", + "type": "array" + }, + "weight": { + "maximum": 1000000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "action_ref", + "weight" + ], + "title": "ParticipantActivityActionCandidate", + "type": "object" + }, + "ParticipantActivityTiming": { + "additionalProperties": false, + "description": "Inclusive bounded interval for the next occurrence on the shared clock.", + "properties": { + "maximum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Maximum Ticks", + "type": "integer" + }, + "minimum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Minimum Ticks", + "type": "integer" + } + }, + "required": [ + "minimum_ticks", + "maximum_ticks" + ], + "title": "ParticipantActivityTiming", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV1": { + "additionalProperties": false, + "description": "Fixed-cadence deterministic scheduler binding.", "properties": { "action_order": { "items": { @@ -7152,7 +7245,175 @@ "max_action_attempts", "evaluation_authority" ], - "title": "ParticipantAutonomousExecutionPolicy", + "title": "ParticipantAutonomousExecutionPolicyV1", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV2": { + "additionalProperties": false, + "description": "Governed within-run activity policy for ordinary participants.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v2", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Progression Policy Ref", + "type": "string" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority" + ], + "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, "ParticipantBackendFailureMapping": { @@ -7290,13 +7551,17 @@ "autonomous_execution": { "anyOf": [ { - "$ref": "#/$defs/ParticipantAutonomousExecutionPolicy" + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV1" + }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, { "type": "null" } ], - "default": null + "default": null, + "title": "Autonomous Execution" }, "backend_feature_support_refs": { "items": { diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index 2de7fb686..163537da2 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -6408,9 +6408,102 @@ "title": "ParticipantActionPrecondition", "type": "object" }, - "ParticipantAutonomousExecutionPolicy": { + "ParticipantActivityActionCandidate": { "additionalProperties": false, - "description": "Deterministic scheduler binding over existing participant semantics.", + "description": "Stable weighted action candidate and its bounded recovery policy.", + "properties": { + "action_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Action Ref", + "type": "string" + }, + "cooldown_ticks": { + "default": 0, + "maximum": 1000000000, + "minimum": 0, + "title": "Cooldown Ticks", + "type": "integer" + }, + "depends_on": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 1024, + "title": "Depends On", + "type": "array" + }, + "max_retries": { + "default": 0, + "maximum": 1024, + "minimum": 0, + "title": "Max Retries", + "type": "integer" + }, + "retryable_failure_classes": { + "items": { + "$ref": "#/$defs/ParticipantFailureClass" + }, + "maxItems": 32, + "title": "Retryable Failure Classes", + "type": "array" + }, + "weight": { + "maximum": 1000000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "action_ref", + "weight" + ], + "title": "ParticipantActivityActionCandidate", + "type": "object" + }, + "ParticipantActivityTiming": { + "additionalProperties": false, + "description": "Inclusive bounded interval for the next occurrence on the shared clock.", + "properties": { + "maximum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Maximum Ticks", + "type": "integer" + }, + "minimum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Minimum Ticks", + "type": "integer" + } + }, + "required": [ + "minimum_ticks", + "maximum_ticks" + ], + "title": "ParticipantActivityTiming", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV1": { + "additionalProperties": false, + "description": "Fixed-cadence deterministic scheduler binding.", "properties": { "action_order": { "items": { @@ -6509,7 +6602,175 @@ "max_action_attempts", "evaluation_authority" ], - "title": "ParticipantAutonomousExecutionPolicy", + "title": "ParticipantAutonomousExecutionPolicyV1", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV2": { + "additionalProperties": false, + "description": "Governed within-run activity policy for ordinary participants.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v2", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Progression Policy Ref", + "type": "string" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority" + ], + "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, "ParticipantBackendFailureMapping": { @@ -6647,13 +6908,17 @@ "autonomous_execution": { "anyOf": [ { - "$ref": "#/$defs/ParticipantAutonomousExecutionPolicy" + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV1" + }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, { "type": "null" } ], - "default": null + "default": null, + "title": "Autonomous Execution" }, "backend_feature_support_refs": { "items": { diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index d6a3b6b24..f21e1b217 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -5693,9 +5693,92 @@ "title": "ParticipantActionPrecondition", "type": "object" }, - "ParticipantAutonomousExecutionPolicy": { + "ParticipantActivityActionCandidate": { "additionalProperties": false, - "description": "Deterministic scheduler binding over existing participant semantics.", + "description": "Stable weighted action candidate and its bounded recovery policy.", + "properties": { + "action_ref": { + "minLength": 1, + "title": "Action Ref", + "type": "string" + }, + "cooldown_ticks": { + "default": 0, + "maximum": 1000000000, + "minimum": 0, + "title": "Cooldown Ticks", + "type": "integer" + }, + "depends_on": { + "items": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 1024, + "title": "Depends On", + "type": "array" + }, + "max_retries": { + "default": 0, + "maximum": 1024, + "minimum": 0, + "title": "Max Retries", + "type": "integer" + }, + "retryable_failure_classes": { + "items": { + "$ref": "#/$defs/ParticipantFailureClass" + }, + "maxItems": 32, + "title": "Retryable Failure Classes", + "type": "array" + }, + "weight": { + "maximum": 1000000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "action_ref", + "weight" + ], + "title": "ParticipantActivityActionCandidate", + "type": "object" + }, + "ParticipantActivityTiming": { + "additionalProperties": false, + "description": "Inclusive bounded interval for the next occurrence on the shared clock.", + "properties": { + "maximum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Maximum Ticks", + "type": "integer" + }, + "minimum_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Minimum Ticks", + "type": "integer" + } + }, + "required": [ + "minimum_ticks", + "maximum_ticks" + ], + "title": "ParticipantActivityTiming", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV1": { + "additionalProperties": false, + "description": "Fixed-cadence deterministic scheduler binding.", "properties": { "action_order": { "items": { @@ -5776,7 +5859,154 @@ "max_action_attempts", "evaluation_authority" ], - "title": "ParticipantAutonomousExecutionPolicy", + "title": "ParticipantAutonomousExecutionPolicyV1", + "type": "object" + }, + "ParticipantAutonomousExecutionPolicyV2": { + "additionalProperties": false, + "description": "Governed within-run activity policy for ordinary participants.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v2", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "title": "Progression Policy Ref", + "type": "string" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority" + ], + "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, "ParticipantBackendFailureMapping": { @@ -5887,13 +6117,17 @@ "autonomous_execution": { "anyOf": [ { - "$ref": "#/$defs/ParticipantAutonomousExecutionPolicy" + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV1" + }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, { "type": "null" } ], - "default": null + "default": null, + "title": "Autonomous Execution" }, "backend_feature_support_refs": { "items": { diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index d6cd31459..0760a31fb 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -587,6 +587,112 @@ "title": "ParticipantActionResultModel", "type": "object" }, + "ParticipantActivityOccurrenceProvenanceModel": { + "additionalProperties": false, + "description": "Safe within-run scheduler provenance for one native action attempt.", + "properties": { + "attempt_id": { + "minLength": 1, + "title": "Attempt Id", + "type": "string" + }, + "burst_position": { + "minimum": 0, + "title": "Burst Position", + "type": "integer" + }, + "candidate_id": { + "minLength": 1, + "title": "Candidate Id", + "type": "string" + }, + "dependency_candidate_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Dependency Candidate Ids", + "type": "array" + }, + "occurrence_id": { + "minLength": 1, + "title": "Occurrence Id", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "policy_profile": { + "const": "participant-autonomous-execution/v2", + "title": "Policy Profile", + "type": "string" + }, + "predecessor_attempt_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Predecessor Attempt Id" + }, + "random_address": { + "$ref": "#/$defs/ParticipantStreamAddressModel" + }, + "random_control_id": { + "minLength": 1, + "title": "Random Control Id", + "type": "string" + }, + "random_profile_id": { + "minLength": 1, + "title": "Random Profile Id", + "type": "string" + }, + "terminal_outcome": { + "minLength": 1, + "title": "Terminal Outcome", + "type": "string" + }, + "timing_disposition": { + "enum": [ + "drawn", + "next_opening", + "retry", + "burst" + ], + "title": "Timing Disposition", + "type": "string" + }, + "timing_tick": { + "minimum": 0, + "title": "Timing Tick", + "type": "integer" + } + }, + "required": [ + "policy_address", + "policy_profile", + "occurrence_id", + "attempt_id", + "candidate_id", + "timing_tick", + "timing_disposition", + "burst_position", + "random_control_id", + "random_profile_id", + "random_address", + "terminal_outcome" + ], + "title": "ParticipantActivityOccurrenceProvenanceModel", + "type": "object" + }, "ParticipantAdmissionDisposition": { "description": "RUN-306 selection/admission disposition values.", "enum": [ @@ -1020,11 +1126,47 @@ "title": "Attempted Actions", "type": "integer" }, + "burst_position": { + "default": 0, + "minimum": 0, + "title": "Burst Position", + "type": "integer" + }, + "burst_size": { + "default": 1, + "minimum": 1, + "title": "Burst Size", + "type": "integer" + }, + "candidate_cooldown_until": { + "additionalProperties": { + "type": "integer" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Candidate Cooldown Until", + "type": "object" + }, "clock_address": { "minLength": 1, "title": "Clock Address", "type": "string" }, + "completed_candidate_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Completed Candidate Ids", + "type": "array" + }, + "current_retry": { + "default": 0, + "minimum": 0, + "title": "Current Retry", + "type": "integer" + }, "episode_id": { "minLength": 1, "title": "Episode Id", @@ -1053,6 +1195,18 @@ "default": null, "title": "Last Action Instance Id" }, + "last_candidate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Last Candidate Id" + }, "lifecycle_state": { "enum": [ "running", @@ -1073,6 +1227,22 @@ "title": "Next Tick", "type": "integer" }, + "next_timing_disposition": { + "default": "cadence", + "enum": [ + "cadence", + "drawn", + "next_opening" + ], + "title": "Next Timing Disposition", + "type": "string" + }, + "occurrence_ordinal": { + "default": 0, + "minimum": 0, + "title": "Occurrence Ordinal", + "type": "integer" + }, "participant_address": { "minLength": 1, "title": "Participant Address", @@ -1093,6 +1263,51 @@ "title": "Policy Digest", "type": "string" }, + "profile": { + "default": "participant-autonomous-execution/v1", + "enum": [ + "participant-autonomous-execution/v1", + "participant-autonomous-execution/v2" + ], + "title": "Profile", + "type": "string" + }, + "random_control_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Random Control Id" + }, + "random_namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Random Namespace" + }, + "random_profile_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Random Profile Id" + }, "succeeded_actions": { "minimum": 0, "title": "Succeeded Actions", @@ -1154,6 +1369,17 @@ ], "default": null }, + "activity_provenance": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantActivityOccurrenceProvenanceModel" + }, + { + "type": "null" + } + ], + "default": null + }, "actor_provenance": { "anyOf": [ { @@ -4836,6 +5062,82 @@ "title": "ParticipantSharedStateRecordModel", "type": "object" }, + "ParticipantStreamAddressModel": { + "additionalProperties": false, + "description": "Closed within-run address for one participant-policy draw.", + "properties": { + "draw_purpose": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Draw Purpose", + "type": "string" + }, + "local_coordinate": { + "minimum": 0, + "title": "Local Coordinate", + "type": "integer" + }, + "namespace": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Namespace", + "type": "string" + }, + "occurrence_ordinal": { + "minimum": 0, + "title": "Occurrence Ordinal", + "type": "integer" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "time_segment": { + "minimum": 0, + "title": "Time Segment", + "type": "integer" + } + }, + "required": [ + "namespace", + "policy_address", + "participant_address", + "time_segment", + "occurrence_ordinal", + "draw_purpose", + "local_coordinate" + ], + "title": "ParticipantStreamAddressModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "draw_purpose must be governed and policy_address/participant_address must be compiled autonomous participant addresses.", + "id": "participant-random-stream-address-governed", + "inputs": [ + { + "contract_id": "controlled-vocabularies-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.random_stream.ParticipantStreamAddressModel._validate_address" + } + ] + }, "ParticipantTemporalEventPoint": { "description": "Named participant event points used by temporal contracts.", "enum": [ 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 21348c070..f1ca7b2a2 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 @@ -118,6 +118,53 @@ and experiment apparatus contracts; the deterministic `ordered_cycle` scheduler needs no seed field. Historical files remain ordinary initial service state. Exercise injects remain orchestration, not simulated users. +### 7. Extend autonomous execution through a versioned activity policy + +The fixed-cadence `participant-autonomous-execution/v1` profile remains +unchanged. Richer admitted behavior is a new profile variant under the same +`ParticipantBehaviorSpecification.autonomous_execution` authority, not +optional fields that silently change v1 meaning and not a second activity +root. + +The richer profile composes: + +- inclusion and pause windows that resolve existing shared-time `window` + constraints on the policy clock; +- positive bounded logical-tick timing intervals and cooldowns; +- stable keyed action candidates with exact integer weights, explicit + dependency guards, portable failure-class retry rules, and finite burst, + retry, attempt, and in-flight bounds; and +- a reference to an admitted run/apparatus stochastic control whose role is + `agent-policy`. + +It reuses the governed random-stream engine and publishes a new immutable +profile/address variant when participant-runtime coordinates or transforms are +needed. A participant draw address is based on the run randomness namespace, +policy and participant addresses, shared-time segment/reset generation, +occurrence ordinal, governed draw purpose, and stable local draw coordinate. +It does not relabel experiment selection-policy or variation-point fields, use +the aggregate scenario/experiment digest, or include worker, thread, host, +wall-time, retry, or call-order coordinates. Exact weighted choice and bounded +timing reuse the admitted bounded-integer transform. Their canonical range and +prefix-interval mappings are policy semantics with conformance vectors; they +are not new random transforms or library RNG behavior. + +Within-run activity draws are not scenario-family selection, factor +allocation, or trial compilation. SDL declares the activity policy and a +stochastic-control reference; admitted run apparatus supplies the exact +profile, namespace, and public seed or governed entropy reference. Raw entropy +never enters SDL, compiled addresses, snapshots, behavior history, diagnostics, +logs, argv, or telemetry. + +Scheduler continuation remains typed autonomous participant state. Append-only +behavior history carries occurrence, selection, timing, dependency, attempt, +terminal outcome, and safe random-draw provenance. Reset creates a new +shared-time segment and participant episode, starts a new scheduler generation, +and preserves predecessor lineage; it does not infer rollback of service state +or causality from timestamps. Backend admission is exact for profile, policy +features, selection strategy, random-stream profile/transform support, time +constraint kinds, and finite limits. + ## Consequences - Human, AI, scripted, and benign simulated participants share one semantic and @@ -127,6 +174,9 @@ state. Exercise injects remain orchestration, not simulated users. declared controls and observable contracts. - The reference implementation proves protocol behavior, not production backend fidelity or throughput. +- Existing v1 policies retain fixed-cadence `ordered_cycle` semantics. Consumers + opt into the richer profile explicitly and fail closed when any activity + feature or governed random-stream profile is unsupported. ## Rejected Alternatives @@ -137,9 +187,17 @@ state. Exercise injects remain orchestration, not simulated users. - Adding a seed field to a deterministic scheduler that performs no random draws. - Inferring native execution from a backend capability boolean. +- Adding activity-policy fields to v1 and changing its existing meaning. +- Reusing experiment variation-point address fields as participant occurrence + coordinates. +- Adding a weighted-choice random transform when the governed bounded-integer + transform plus a canonical policy mapping already expresses the choice. +- A mutable or process-global RNG, host-local calendar, cron expression, or + wall-clock sleep as participant semantic authority. ## Amendments | Date | Commit/PR | Summary | |------|-----------|---------| | 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. | diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 64d3fc73e..d93f83977 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -422,11 +422,14 @@ adrs: pin: c2a0e6ac9fb87aa10fbb571b0f70efe0b99520b806f6ffe706f1556b0ec7e84b - id: ADR-092 path: docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md - pin: f813aebad329c94ad78bd97f945c683ec1426e781666b7136a5dd2904476acd5 + pin: a4398471e58b052d6b7c05cf9014aa4a76adf3e39c7dddd6b1db8a8301047577 amendments: - date: 2026-07-24 ref: "#861" summary: "Required exact action provenance and capability-specific atomic participant batching." + - date: 2026-07-26 + ref: "#897" + summary: "Kept v1 stable and governed richer within-run timing, weighted selection, lifecycle state, and provenance as a versioned autonomous-execution profile." - id: ADR-093 path: docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md pin: 72725b8f09063b0b1d8ec65c4fda58e5648113035588ee4effbece5e0b1c3436 diff --git a/docs/decisions/issue-897-autonomous-activity-policy-preflight.md b/docs/decisions/issue-897-autonomous-activity-policy-preflight.md new file mode 100644 index 000000000..1d6515471 --- /dev/null +++ b/docs/decisions/issue-897-autonomous-activity-policy-preflight.md @@ -0,0 +1,415 @@ +# Issue 897 Autonomous Participant Activity Policy Preflight + +Issue: #897 + +Requirement: none; the issue is the authoritative contract. + +Date: 2026-07-26 + +This note fixes architecture guardrails before implementation. It is not an +implementation plan and adds no schema, compiler, runtime, backend, or release +behavior. + +## Decision And Incumbent Authority + +Issue #897 extends +`ParticipantBehaviorSpecification.autonomous_execution`, ADR-092, and the +formal autonomous-execution semantics. It does not create a live-activity, +background-actor, benign-user, scheduler, calendar, or randomizer root. + +The fixed-cadence `participant-autonomous-execution/v1` profile keeps its +current `ordered_cycle` meaning. The richer contract is a discriminated v2 +profile under the same field. This is required because adding optional policy +fields to v1 would make an already-admitted document change meaning under the +same profile id. + +The internal authorities to compose are: + +- ADR-013, ADR-022, ADR-041, ADR-054, ADR-067, and ADR-085 for participant + episode, action, implementation, history, behavior, and information-flow + authority; +- ADR-090 and ADR-091 for exact shared time, clock ownership, lifecycle, + capability, runtime readback, and realized-time provenance; +- ADR-084 and the EXP-718 random-stream suite for stateless governed draws, + profile versioning, canonical addressing, exact transforms, and archival + stochastic provenance; +- ADR-094 for explicit cross-plane owner identity and fail-closed binding + resolution; an activity policy's stochastic-control reference is not a + configuration target and must not be inferred from matching names; +- ADR-061 and ADR-075 for schema compatibility, profile/version meaning, + migration, and release governance; and +- ADR-080 and the SDL lineage ledger for revision-pinned source and internal + authority traceability. + +Existing lineage already records CybORG benign-agent/reset precedent, +Gymnasium/PettingZoo/OpenSpiel participant and chance/ordering boundaries, +ROS 2 clock lifecycle, FMI scheduled execution, HLA time management, TENA +execution/archive separation, and OpenSCENARIO entity/action/trigger +separation. The random-stream research records L'Ecuyer-style semantic streams, +counter-addressable generation, NumPy's seed-versus-algorithm distinction, and +schedule-independent derivation. Issue #897 must extend those existing audit +and ledger claims; it must not add an unpinned second bibliography or imply +wire compatibility. + +## Semantic Shape And Boundaries + +### Availability and shared time + +Work windows are inclusion references and pauses are exclusion references to +existing shared-time `window` constraints. Every referenced constraint resolves +to the policy clock and time domain, is actually of kind `window`, and names +the governed behavior specification or every participant to which the policy +applies as a subject. An unrelated object's window is not reusable merely +because it uses the same clock. Eligibility is the normalized union of work +windows minus pause windows; declaration order is not semantic. + +Window endpoint semantics belong to the shared-time authority and must be +defined once over the existing superdense `(tick, microstep)` coordinate before +runtime behavior depends on them. The safe default for adjacent work/pause +windows is half-open `[start, end)`, which avoids executing twice at a shared +boundary. A participant-only competing interpretation is forbidden. + +V2 admits finite declared windows. Locale names, IANA time zones, daylight +saving rules, holiday feeds, cron syntax, and host calendars are not portable +time authority. The extension seam for recurring civil calendars is a future +versioned shared-time constraint profile; the participant policy continues to +reference constraints rather than embedding calendar calculation. + +Global clock pause/resume remains the existing lifecycle operation. An authored +pause window changes action eligibility; it does not pause the shared clock or +mint a private lifecycle state. + +### Bounded deterministic timing + +Timing variation is expressed in positive integer ticks on the policy clock, +with explicit inclusive lower/upper bounds and finite occurrence/attempt +budgets. The next eligible coordinate is a pure function of the admitted +policy, prior typed scheduler state, shared-time state, and addressed draw. + +No implementation may clamp an out-of-window draw, redraw until a convenient +time appears, consult wall time, sleep outside the existing clock driver, or +advance a backend/system/external-authority clock. The policy must give one +deterministic disposition for a candidate outside eligibility, such as skip or +move to the next declared opening, and that disposition is part of policy +identity and backend admission. Searching for a next opening is bounded. + +Clock transition preflight continues to reject a transition that would skip +the next governed due coordinate. Repeated work at one tick uses superdense +microsteps or an explicit serialized/joint-action record; equal ticks alone do +not mean simultaneous execution. + +### Governed random streams + +The SDL policy carries a stochastic-control reference, not a seed. The admitted +run/apparatus control has role `agent-policy` and supplies the existing +`RandomStreamControlBindingModel`: exact immutable profile, randomness +namespace, and public seed or governed entropy reference. + +SDL validation can prove only the local policy shape and reference syntax. +It cannot claim that an experiment-owned control resolves when no experiment +context is present. Standalone scenario compilation must preserve that +external reference without treating the scenario as executable. The +run-scoped join resolves exactly once by `control_id`, requires role +`agent-policy` and an executable binding, and is sealed into ADR-084's admitted +trial/execution handoff before runtime admission. If that incumbent handoff is +not yet available, it is a delivery dependency or the authority under which a +minimal typed carrier is completed—not permission to place the binding in +`RuntimeSnapshot.metadata`, SDL `spec`, an environment variable, or a backend +constraint string. + +This is activity during one run. It is not: + +- scenario-family variation, variation-point selection, allocation, blocking, + factor assignment, or trial compilation; +- permission to reinterpret legacy descriptive stochastic fields as + executable; or +- permission for a backend to resample after rejection, timeout, retry, + capability failure, or service failure. + +The current `StreamAddressModel` is intentionally experiment-selection-shaped. +Do not place participant values in `selection_policy_id` or +`variation_point_id`. Extend the existing versioned random-stream address +family with a closed participant-runtime variant containing: + +- the admitted randomness namespace; +- autonomous policy and participant canonical addresses; +- shared-time segment/reset generation; +- occurrence ordinal; +- governed draw purpose; and +- stable local draw coordinate. + +Worker, process, thread, host, queue, map order, wall time, retry count, call +count, backend availability, and aggregate scenario/experiment digests are +forbidden coordinates. Retries reuse the occurrence's timing and selection; +they do not consume a new choice merely because transport or native execution +was retried. + +Exact weighted choice uses stable keyed candidate ids and positive integer +weights in canonical candidate order. At least one candidate must be eligible, +the bounded sum must be representable, and dependency filtering happens before +the addressed draw. Floating-point weights, implicit normalization, hash/map +order, library defaults, and repeated sampling are forbidden. + +The accepted `blake3-xof-v1` profile is immutable and its address shape is +experiment-selection-specific. Participant-runtime addressing therefore +requires a new immutable profile/version, closed dispatch, published profile +data, independent conformance vectors, and exact backend/apparatus support. +The new profile should reuse the incumbent bounded-integer transform unless a +demonstrated requirement cannot be expressed by it. Bounded timing draws an +integer from the declared inclusive interval. Weighted selection draws an +integer from the bounded total weight and maps it through canonical +candidate-order prefix intervals. Those mappings are participant-policy +semantics and need conformance cases, but they are not a second RNG transform. +Do not edit `blake3-xof-v1` into a different compatibility unit. + +### Actions, dependencies, retries, cooldowns, and bursts + +V2 action policy uses stable keyed candidates that reference existing +participant action contracts. Candidate identity, weight, dependency guards, +and recovery policy are scheduler policy; applicability, typed preconditions, +effects, portable failure classes, interactions, and service targets remain +owned by `ParticipantActionContract`. + +Dependency guards resolve prior typed occurrence/attempt outcomes and explicit +participant or service-state observations already admitted to the participant +boundary. They do not copy service truth into a scheduler state bag, treat a +runtime resource dependency as an action dependency, or infer causality from +timestamps. A causal claim still requires the existing +`ParticipantAttributionEdgeModel` evidence basis. + +Dependency graphs are acyclic where statically expressible and must admit an +initial candidate. At runtime an empty eligible set follows one declared +bounded disposition; it never falls back to all actions or the first action. + +Retries are new uniquely identified attempts within one occurrence. They are +allowed only for declared portable `ParticipantFailureClass` values, have a +finite per-occurrence bound, and cite the predecessor attempt. A success is +never retried. Protocol-invalid or indeterminate native work is not blindly +replayed. Global action-attempt limits include retries; occurrence, attempt, +success, failure, retry, burst, and in-flight counters remain distinct. + +Cooldown is an exact logical-tick eligibility guard on the policy clock. +Limited bursts have a separate positive bound and never reuse +`max_in_flight` as a burst-size alias. The reference runtime may serialize a +burst, but must record realized order and distinct occurrence/action identities. +A backend claiming actual concurrency uses the existing joint-action and +shared-state contracts. + +### State, reset, service continuity, and provenance + +Continuation extends `ParticipantAutonomousExecutionStateModel` rather than +adding a second store. It carries only the minimum restart cursor and accounting +needed to reproduce the next transition: policy/profile digest, clock segment, +episode/generation identity, next eligibility coordinate, occurrence and +attempt counters, cooldown/burst continuation, and safe random-control/profile +identity. Append-only history, not mutable state, is occurrence evidence. + +`ParticipantBehaviorHistoryEventModel` and its API-408 projection are the +occurrence provenance surface. A typed nested record should preserve policy +address/profile, occurrence and attempt ids, predecessor/dependency refs, +selected candidate, timing coordinate/disposition, burst position, safe +random-control/address/transform refs, and terminal outcome. It must not expose +root entropy, derived keys, raw blocks, secret refs beyond their governed safe +identity, complete candidate domains, or backend-private objects. + +Every native attempt still passes the current sequence: + +1. bind the selected participant implementation and action; +2. execute through `ParticipantRuntime.admit_action`; +3. require a typed terminal action result and exact ordered behavior history; +4. commit native/service snapshot and portable history; then +5. advance scheduler continuation. + +A valid terminal failure may have changed service state; subsequent dependency +and retry evaluation uses the returned committed snapshot. A protocol-invalid +result restores the predecessor portable snapshot and consumes identity without +claiming native rollback. + +Shared-clock reset continues through the capability-specific atomic +`reset_with_participants` plus participant `reset_many` transaction. Only after +that transaction succeeds may scheduler generation change. The new time segment +and participant episode start a new occurrence generation whose random address +includes the segment; predecessor episode/generation lineage remains +observable. Service state is whatever its owning backend lifecycle actually +preserves or resets—scheduler state cannot fabricate rollback or continuity. + +Plan reapplication preserves continuation only when the complete resolved v2 +policy, windows, time declarations, stochastic-control/profile identity, action +entries, weights, dependencies, recovery rules, and bounds match the policy +digest. Any material drift fails before mutation. + +## Canonical Cross-Cutting Incumbents + +| Layer | Incumbent to extend | +| --- | --- | +| Authored SDL shape | `raes.participant_execution.ParticipantAutonomousExecutionPolicy`; `ParticipantBehaviorSpecification.autonomous_execution`; `SDLModel` closed-shape validation | +| Source ingress | `raes.parser.parse_sdl`, `load_sdl_yaml`, YAML 1.2 core resolution, duplicate/merge-key checks, `SDLParserLimits` | +| Semantic validation | `raes.semantics.participant_behavior` and `raes.validator._participant_execution_renderers`; `raes.validator._time_model` | +| Shared time | `raes.time_model`, `raes_processor.compiler.time_model`, `CompiledTimeModel`, `RuntimeTimeControlMixin`, and time capability admission | +| Canonical compilation | `_compile_autonomous_execution`, `ParticipantAutonomousExecutionRuntime`, compiled-address helpers, the stable v1 `_policy_digest`, and `canonical_contract_digest()` for a new typed v2 resolved-policy identity | +| Random streams | `RandomStreamControlBindingModel`, `ExperimentStochasticControlModel`, versioned random-stream profiles/corpus, controlled draw-purpose vocabulary, stateless engine and bounded-integer transform, RFC 8785/JCS canonicalization, diagnostics, and vectors | +| Cross-plane binding | ADR-094 owner/binding discipline and admitted trial/run carriage; do not reuse `ExperimentBindingDescriptorModel` as a stochastic-control DTO or infer a control from matching ids | +| Planning/admission | `participant_autonomous_execution_capability_gaps`, `_participant_execution_diagnostics`, backend `ParticipantRuntimeCapabilities`, `TimeCapabilities`, and exact manifest conversion | +| Runtime target gate | `RuntimeTarget` registry probes and capability-specific participant/time protocols | +| Execution | `ParticipantScheduler`, `participant_scheduler_operations`, `autonomous_action_result_violation`, `RuntimeParticipantExecutionMixin`, and `ParticipantClockDriver` | +| Persistence | `RuntimeSnapshot.participant_autonomous_execution_states`, `require_participant_autonomous_runtime_snapshot`, `ControlPlaneStore`, `LocalControlPlaneStore`, and API snapshot conversion | +| Occurrence evidence | `ParticipantBehaviorHistoryEventModel`, `ParticipantHistoryViewBehaviorEventModel`, participant retrieval projection, action-result/temporal-context/attribution contracts, and experiment-run stochastic draw provenance | +| Conformance | `raes_conformance.conformance.snapshot_semantics`, schema fixtures, backend-manifest fixtures, random vectors, and cross-process determinism witnesses | +| Publication/workflow | authored `contracts/schemas/`, `schema_bundle()` parity, `contracts/schema-publication-manifest.json`, release-please, the canonical nox `verify` graph, and Ground Control issue/PR checks | + +Do not duplicate these models in a scenario-pack DTO, backend adapter, API +request, or persistence repository. + +## Security And Whole-Path Gates + +1. **SDL source gate.** Policy input passes input-byte, scalar, depth, node, + alias, import, composition, and namespace limits; safe YAML construction; + duplicate/merge-key policy; closed Pydantic shape; then semantic validation. + Candidate ids, integer weights, bounds, references, and graph sizes need + explicit limits before compilation. Unknown v2 fields fail; they are not + stored as extensions. +2. **Experiment/config gate.** The stochastic control passes the existing + `ExperimentSpecModel`/apparatus/run shapes, + `ExperimentStochasticControlModel`, and + `RandomStreamControlBindingModel`. The run-scoped join—not standalone SDL + validation—must resolve a policy reference exactly once to role + `agent-policy`; profile, namespace, and control identity must agree across + admitted execution handoff, runtime, draw, and run provenance. Do not add + another YAML loader, configuration descriptor, config registry, or generic + metadata bag. +3. **Secret gate.** Public fixed-width seeds and governed entropy references + remain the existing closed union. The current reference engine executes + public seeds only; issue #897 does not authorize inventing a secret resolver. + A governed reference fails admission unless a separately governed, + authorized in-process resolver already exists and verifies immutable + version, caller/scope, purpose, and byte length. SDL, + environment-variable names, URI credentials, file paths, and backend + constraint strings are not secret-resolution mechanisms. +4. **Authentication/authorization gate.** Issue #897 needs no new endpoint. + Existing API-408 history/status retrieval remains behind + `ControlPlaneSecurityConfig.strict_defaults()`, bearer or verified-proxy + identity, read-role/target checks, request-size guards, and audit events. + If a later mutation endpoint is proposed, it requires the existing + operator/backend authorization and idempotency path; policy presence does + not grant participant control or entropy-read authority. +5. **Compiler and admission gate.** All refs compile to canonical addresses; + semantic validation proves role/evaluation and graph/window invariants; + planning proves exact participant policy/profile/strategy/action/ + observation/target/random/time support and finite maxima. Manifest booleans, + free-form `constraints`, installed libraries, or native method presence are + not substitutes for exact claims. +6. **Runtime protocol gate.** Registry signature probes still require native + binding and coordinated reset methods when claimed. Runtime validates every + action result, history append, clock/episode/generation binding, counter + equation, and policy digest before durable commit. Retry never bypasses + native binding or typed terminal outcome validation. +7. **Persistence/conformance gate.** Save, load, API conversion, and conformance + all invoke the canonical autonomous snapshot invariants. Random continuation + does not live in `RuntimeSnapshot.metadata`, operation `details`, tags, audit + details, or a new seed database. Unknown, contradictory, or stale state + fails closed. +8. **OS exposure gate.** Draws execute in process through the stateless engine. + Seeds, governed refs, derived keys, candidate sets, selected values, and + participant stream addresses never enter environment variables, process + argv, shell interpolation, temporary command files, or `shell=True`. + `PYTHONHASHSEED` may vary only as test apparatus and never enters semantics. +9. **Error-envelope gate.** Authoring failures use existing + `SDLParseError`/`SDLValidationError`; operational failures use bounded + `Diagnostic` values with safe code, domain, canonical concern address, + profile id, and counts. Do not add an activity exception hierarchy or echo + raw Pydantic input, entropy, complete weights/candidates, backend exception + strings, environment, or traceback. HTTP retains the generic redacted 500 + envelope. +10. **Logging/observability gate.** Scientific inspection uses typed snapshot, + behavior history, random-draw, run, and conformance records. Logs and audit + events may carry safe ids, profile versions, counts, stage outcomes, and + durations only—not entropy, derived material, candidate domains, + selections, sensitive service state, or raw backend failures. + +## Compatibility, Extensibility, And Traceability + +- V1 documents and manifests retain their exact fixed-cadence meaning. V2 is + opt-in and unsupported consumers reject its profile before provisioning. +- V1 policy-digest bytes remain stable for persisted snapshots. V2 identity + uses a typed resolved-policy payload plus the incumbent + `canonical_contract_digest()` rather than extending the scheduler's ad hoc + JSON hashing or introducing another canonicalizer. +- The authoring/compiled/runtime/schema discriminators, random-stream profile, + participant address variant, backend feature terms, and occurrence provenance + version independently. A package version or schema filename does not stand in + for any of them. +- The main extensibility seam is the v2 policy's closed strategy/profile ids + plus stable keyed action entries. A new selection or timing strategy adds an + exact policy mapping and capability term without editing existing profile + semantics; it adds a random transform only when the incumbent bounded-integer + transform cannot express the operation. A future recurring civil calendar + extends shared-time constraint authority, not the participant scheduler. +- Published schema edits require hand-governed schema changes, matching + `schema_bundle()` output, fixtures, and a `last_change` summary/hash in the + schema-publication manifest. Breaking stable changes mint a new contract + lineage; current draft status does not excuse unrecorded drift. +- Migration guidance must show v1 unchanged and an explicit v1-to-v2 authoring + example. There is no automatic migration from `action_order` to weighted + candidates, because weights, dependencies, recovery, windows, and stochastic + control cannot be inferred. +- There is no formal Ground Control requirement UID; do not invent one. Issue + #897 is the contract and must link the ADR/design decision, implementation + files, focused and conformance tests, the PR, and the release. The external + consumer link is `autarchy-ai/penumbra-scenarios#556`. +- Release trace uses a consumer-visible Conventional Commit/PR title so + release-please creates the package release. Do not edit `CHANGELOG.md`, add a + fragment, or hand-edit `_version.py`. + +## Gotchas And Anti-Patterns + +Avoid: + +- a parallel live-activity actor, service, scheduler, clock, calendar, policy + root, action schema, snapshot map, or provenance stream; +- changing v1 semantics through optional fields or treating absence as a new + default; +- embedding raw seed material in SDL or copying a stochastic control into each + action/occurrence; +- overloading pre-run `selection_policy_id`/`variation_point_id` as runtime + coordinates; +- mutable RNG cursors, process-global randomness, floating weights, modulo + bias, resampling, fallback actions, map-order choice, or retry-driven draws; +- a duplicate weighted-choice RNG transform or misuse of cross-plane + configuration descriptors as stochastic-control bindings; +- cron, host locale/time zone, wall clock, sleep, or operational watchdog time + as shared scenario time; +- clamping timing into a window, silently skipping a missed due coordinate, or + allowing reset/reapply to change a policy without changing its digest; +- using `refresh_dependencies` or service placement graphs as action-policy + dependencies; +- treating a failed action as no state change, retrying a success, replaying an + indeterminate native call, or claiming copied snapshots roll back backends; +- treating same-tick burst actions as simultaneous without microstep/order or + joint-action evidence; +- duplicating validation in compiler/runtime after a semantic helper can own + it, or adding a new exception, diagnostic, logger, audit, schema registry, + store, loader, CI workflow, or conformance runner; and +- claiming human realism, causal proof, service fidelity, exactly-once native + execution, or production backend support from reference scheduler tests. + +## Non-Goals And Implementation Boundaries + +- No KeplerOps, Penumbra, pack-local role, product, workflow, action, service, + or historical-content semantics enter RAES. +- The policy does not allocate trials, vary scenario structure, assign + experiment factors, score participants, interpret outcomes, or grant + evaluator authority. +- It does not introduce a general workflow engine, behavior tree, expression + language, state-machine DSL, cron/calendar service, secret manager, HTTP + endpoint, worker queue, or general-purpose RNG plugin system. +- It does not implement governed-secret resolution; the reference path remains + limited to public seeds unless that capability is separately governed. +- It does not define externally paced transition notifications; ADR-092's + fail-closed boundary remains. +- It does not guarantee human-like behavior, unpredictability, cryptographic + secrecy, environmental replay, backend fidelity, native rollback, or causal + attribution. +- The reference runtime owns portable scheduling semantics and evidence. + Backend-native optimizations are allowed only when exact admission, + observable ordering, state, history, provenance, and conformance remain + equivalent. diff --git a/docs/explain/reference/backend-conformance.md b/docs/explain/reference/backend-conformance.md index 8ed59898f..2db47958e 100644 --- a/docs/explain/reference/backend-conformance.md +++ b/docs/explain/reference/backend-conformance.md @@ -140,8 +140,9 @@ DSL-437 autonomous execution is an additional fail-closed participant capability. A backend that includes `autonomous_execution` in `supported_behavior_features` must set `supports_autonomous_execution`, list its supported selection strategies, exact action contracts, observation -boundaries, target addresses, and positive finite limits for participants, -attempts, and in-flight actions. The planner also compares the parent behavior +boundaries, target addresses, policy profiles, and positive finite limits for +participants, attempts, in-flight actions, occurrences, retries per occurrence, +and burst size. The planner also compares the parent behavior specification's required feature set with the runtime capability. Runtime target registration requires the autonomous native-binding method. This is admission evidence only: conformance also requires the backend participant @@ -155,6 +156,16 @@ portable transition-notification contract is governed. Durable readback must agree across scheduler policy identity, clock segment/lifecycle, and live participant episode. +The explicit `participant-autonomous-execution/v2` profile additionally +requires exact support for all governed activity features, `weighted` +selection, shared-time `window` constraints, and +`blake3-xof-participant-v1`. The runtime records dependency, retry, cooldown, +burst, timing-disposition, and safe random-address facts in typed continuation +and participant behavior history. A backend must not substitute the +experiment-selection `blake3-xof-v1` address/profile, silently drop occurrence +provenance, or treat an apparatus stochastic-control declaration as scenario +variation. Missing exact support fails planning. + ## Gotchas And Anti-Patterns Avoid: diff --git a/docs/migration/README.md b/docs/migration/README.md index 2071d1120..e30385409 100644 --- a/docs/migration/README.md +++ b/docs/migration/README.md @@ -10,6 +10,10 @@ to RAES names and identifies the narrow ACES identifiers that remain only as source import paths, governed contract identifiers, historical records, or external references. +The explicit opt-in from fixed-cadence autonomous participant execution to the +governed activity profile is documented in +[Autonomous Execution V2 Migration](autonomous-execution-v2.md). + The reorganization moved existing material into the current long-term buckets: - root `schemas/` -> `contracts/schemas/` diff --git a/docs/migration/autonomous-execution-v2.md b/docs/migration/autonomous-execution-v2.md new file mode 100644 index 000000000..0154ea5dd --- /dev/null +++ b/docs/migration/autonomous-execution-v2.md @@ -0,0 +1,73 @@ +# Autonomous Execution V2 Migration + +`participant-autonomous-execution/v2` is an explicit opt-in profile for +ordinary participant activity with governed work windows, bounded timing +variation, weighted candidates, dependencies, retries, cooldowns, and bursts. +It does not change the meaning of existing +`participant-autonomous-execution/v1` documents. + +## Existing V1 Scenarios + +No migration is required. Keep the v1 cadence constraint, `action_order`, +`ordered_cycle`, and existing finite attempt/in-flight limits. Omitting +`profile` continues to select v1. A v1 backend declaration must now list the +exact `participant-autonomous-execution/v1` policy profile and the occurrence, +retry, and burst capability limits used for common admission accounting. + +## Opting Into V2 + +Replace v1 cadence/order fields with the explicit v2 shape: + +```yaml +autonomous_execution: + profile: participant-autonomous-execution/v2 + participant_implementation_ref: participant-implementation-manifests.green-worker.v1 + clock_ref: scenario-clock + progression_policy_ref: scenario-progression + work_window_refs: [work-window] + pause_window_refs: [break-window] + observation_boundary_ref: participant-view + stochastic_control_ref: green-activity-policy + selection_strategy: weighted + timing: + minimum_ticks: 10 + maximum_ticks: 30 + outside_window_disposition: next_opening + empty_eligible_disposition: complete + action_candidates: + portal-login: + action_ref: probe-customer-portal-login + weight: 3 + depends_on: [] + retryable_failure_classes: [target_unavailable, timeout] + max_retries: 2 + cooldown_ticks: 20 + max_occurrences: 8 + max_action_attempts: 24 + max_burst_size: 2 + max_in_flight: 1 + failure_policy: continue + evaluation_authority: + mode: none +``` + +Work and pause refs must resolve to finite shared-time `window` constraints on +the policy clock and name the behavior specification or every governed +participant as a subject. Windows are half-open `[start, end)`. On stepped +clocks, timing bounds must be multiples of `step_ticks`. + +The SDL contains only `stochastic_control_ref`. Run apparatus must supply an +exact `agent-policy` control bound to `blake3-xof-participant-v1`, a namespace, +and public seed or resolvable governed entropy. Seeds do not belong in SDL. +The reference runtime currently fails closed for governed entropy because no +authorized resolver is installed. + +Backends must explicitly advertise the v2 profile, all activity features, +weighted selection, the participant random-stream profile, window support, and +finite occurrence/retry/burst limits. Existing v1 capability claims do not +implicitly admit v2. + +Runtime snapshots gain typed v2 continuation fields, and participant behavior +history gains safe activity occurrence provenance. Consumers should preserve +unknown optional fields when forwarding current contract payloads. No new API +endpoint or private scheduler state surface is introduced. diff --git a/docs/research/lineage/source-audit-2026-07-12.md b/docs/research/lineage/source-audit-2026-07-12.md index bdea13d88..adeecf5b9 100644 --- a/docs/research/lineage/source-audit-2026-07-12.md +++ b/docs/research/lineage/source-audit-2026-07-12.md @@ -209,6 +209,24 @@ exists. Coordinated clock/participant reset is an ACES backend transaction obligation, not a derivation from any source rollback API. No source callback, transaction protocol, or wire protocol is implied. +### Issue 897 autonomous activity extension review + +The participant and simulation-time source boundaries above were re-reviewed +on 2026-07-26 for issue #897. No new external syntax, API, scheduler, calendar, +or wire contract was adopted. Work/pause window algebra, bounded logical-tick +timing, canonical integer-weight selection, dependency/retry/cooldown/burst +policy, exact backend admission, and typed occurrence provenance are +ACES-defined extensions under ADR-092. + +The participant activity profile reuses the ACES-governed random-stream +principles and sources already audited in +`docs/research/scenario-variation-trial-realization/prior-art-and-design-criteria.md`. +It publishes a distinct participant-occurrence profile/address and does not +reinterpret experiment selection-policy or variation-point coordinates. +Accordingly, NumPy, Random123, and stream-splitting precedents remain design +criteria rather than source compatibility or copied-code claims. No additional +license notice is required by this extension. + ## CRACK Publications Two related works by Russo, Costa, and Armando are distinct and must not share diff --git a/implementations/python/packages/raes/participant_execution.py b/implementations/python/packages/raes/participant_execution.py index 57cc03624..b0b0ea052 100644 --- a/implementations/python/packages/raes/participant_execution.py +++ b/implementations/python/packages/raes/participant_execution.py @@ -8,6 +8,8 @@ from pydantic import Field, field_validator, model_validator from ._base import SDLModel +from ._identifiers import PortableIdentifier +from .participant_action_semantics import ParticipantFailureClass class ParticipantExecutionFailurePolicy(str, Enum): @@ -62,8 +64,8 @@ def _validate_authority_mode(self) -> ParticipantEvaluationAuthority: return self -class ParticipantAutonomousExecutionPolicy(SDLModel): - """Deterministic scheduler binding over existing participant semantics.""" +class ParticipantAutonomousExecutionPolicyV1(SDLModel): + """Fixed-cadence deterministic scheduler binding.""" profile: Literal["participant-autonomous-execution/v1"] = "participant-autonomous-execution/v1" participant_implementation_ref: str = Field(min_length=1) @@ -88,8 +90,125 @@ def _unique_non_empty_refs(cls, values: list[str]) -> list[str]: return values +class ParticipantActivityTiming(SDLModel): + """Inclusive bounded interval for the next occurrence on the shared clock.""" + + minimum_ticks: int = Field(ge=1, le=1_000_000_000) + maximum_ticks: int = Field(ge=1, le=1_000_000_000) + + @model_validator(mode="after") + def _validate_bounds(self) -> ParticipantActivityTiming: + if self.maximum_ticks < self.minimum_ticks: + raise ValueError("activity timing maximum_ticks must be greater than or equal to minimum_ticks") + return self + + +class ParticipantActivityActionCandidate(SDLModel): + """Stable weighted action candidate and its bounded recovery policy.""" + + action_ref: str = Field(min_length=1) + weight: int = Field(ge=1, le=1_000_000_000) + depends_on: list[PortableIdentifier] = Field(default_factory=list, max_length=1024) + retryable_failure_classes: list[ParticipantFailureClass] = Field(default_factory=list, max_length=32) + max_retries: int = Field(default=0, ge=0, le=1024) + cooldown_ticks: int = Field(default=0, ge=0, le=1_000_000_000) + + @field_validator("depends_on", "retryable_failure_classes") + @classmethod + def _require_unique_values(cls, values: list[object]) -> list[object]: + if len(values) != len(set(values)): + raise ValueError("activity candidate dependency and retry values must be unique") + return values + + @model_validator(mode="after") + def _validate_retry_policy(self) -> ParticipantActivityActionCandidate: + if bool(self.retryable_failure_classes) != bool(self.max_retries): + raise ValueError("activity candidate retryable failure classes and max_retries must be declared together") + return self + + +class ParticipantAutonomousExecutionPolicyV2(SDLModel): + """Governed within-run activity policy for ordinary participants.""" + + profile: Literal["participant-autonomous-execution/v2"] + participant_implementation_ref: str = Field(min_length=1) + clock_ref: str = Field(min_length=1) + progression_policy_ref: str = Field(min_length=1) + work_window_refs: list[str] = Field(min_length=1, max_length=1024) + pause_window_refs: list[str] = Field(default_factory=list, max_length=1024) + observation_boundary_ref: str = Field(min_length=1) + stochastic_control_ref: str = Field(min_length=1) + selection_strategy: Literal["weighted"] + timing: ParticipantActivityTiming + outside_window_disposition: Literal["next_opening", "skip"] + empty_eligible_disposition: Literal["complete", "wait"] + action_candidates: dict[PortableIdentifier, ParticipantActivityActionCandidate] = Field( + min_length=1, + max_length=1024, + ) + max_occurrences: int = Field(ge=1, le=1_000_000) + max_action_attempts: int = Field(ge=1, le=1_000_000) + max_burst_size: int = Field(default=1, ge=1, le=1024) + max_in_flight: int = Field(default=1, ge=1, le=1024) + failure_policy: ParticipantExecutionFailurePolicy = ParticipantExecutionFailurePolicy.STOP + evaluation_authority: ParticipantEvaluationAuthority + + @field_validator("work_window_refs", "pause_window_refs") + @classmethod + def _unique_non_empty_refs(cls, values: list[str]) -> list[str]: + if any(not value.strip() for value in values): + raise ValueError("participant activity window refs must be non-empty") + if len(values) != len(set(values)): + raise ValueError("participant activity window refs must be unique") + return values + + @model_validator(mode="after") + def _validate_candidate_graph(self) -> ParticipantAutonomousExecutionPolicyV2: + candidate_ids = set(self.action_candidates) + dependencies = { + candidate_id: set(candidate.depends_on) for candidate_id, candidate in self.action_candidates.items() + } + unknown = sorted( + dependency for values in dependencies.values() for dependency in values if dependency not in candidate_ids + ) + if unknown: + raise ValueError( + "activity candidate dependencies must resolve to declared candidates: " + ", ".join(unknown) + ) + visiting: set[str] = set() + visited: set[str] = set() + + def visit(candidate_id: str) -> None: + if candidate_id in visiting: + raise ValueError("activity candidate dependency graph must be acyclic") + if candidate_id in visited: + return + visiting.add(candidate_id) + for dependency in dependencies[candidate_id]: + visit(str(dependency)) + visiting.remove(candidate_id) + visited.add(candidate_id) + + for candidate_id in self.action_candidates: + visit(str(candidate_id)) + if all(dependencies.values()): + raise ValueError("activity candidate dependency graph must admit an initial candidate") + if self.max_occurrences > self.max_action_attempts: + raise ValueError("activity max_occurrences cannot exceed max_action_attempts") + if self.max_burst_size > self.max_occurrences: + raise ValueError("activity max_burst_size cannot exceed max_occurrences") + return self + + +ParticipantAutonomousExecutionPolicy = ParticipantAutonomousExecutionPolicyV1 | ParticipantAutonomousExecutionPolicyV2 + + __all__ = [ + "ParticipantActivityActionCandidate", + "ParticipantActivityTiming", "ParticipantAutonomousExecutionPolicy", + "ParticipantAutonomousExecutionPolicyV1", + "ParticipantAutonomousExecutionPolicyV2", "ParticipantEvaluationAuthority", "ParticipantEvaluationAuthorityMode", "ParticipantExecutionFailurePolicy", diff --git a/implementations/python/packages/raes/semantics/participant_behavior.py b/implementations/python/packages/raes/semantics/participant_behavior.py index 3bb81b43c..3c8e4f8ea 100644 --- a/implementations/python/packages/raes/semantics/participant_behavior.py +++ b/implementations/python/packages/raes/semantics/participant_behavior.py @@ -484,7 +484,13 @@ def _autonomous_action_issues( ) -> list[ParticipantBehaviorIssue]: issues: list[ParticipantBehaviorIssue] = [] parent_actions = {str(ref) for ref in getattr(context.behavior_spec, "action_contract_refs", []) or []} - for action_ref in context.policy.action_order: + 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: @@ -559,10 +565,24 @@ def _autonomous_constraint_issues( issues: list[ParticipantBehaviorIssue] = [] cadence = None cadence_count = 0 - for constraint_ref in context.policy.temporal_constraint_refs: + profile = getattr(context.policy, "profile", "participant-autonomous-execution/v1") + activity_policy = profile == "participant-autonomous-execution/v2" + constraint_refs = ( + [*context.policy.work_window_refs, *context.policy.pause_window_refs] + if activity_policy + else list(context.policy.temporal_constraint_refs) + ) + for constraint_ref in constraint_refs: if context.is_unresolved(constraint_ref): continue - constraint = context.references.temporal_constraints.get(constraint_ref) + 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: issues.append(_autonomous_issue(context, "participant.autonomous-constraint-unbound", constraint_ref)) continue @@ -570,6 +590,24 @@ def _autonomous_constraint_issues( cadence_count += int(kind == "cadence") if kind == "cadence": cadence = constraint + 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": + subjects = {str(ref) for ref in getattr(constraint, "subject_refs", ())} + if context.spec_name not in subjects and not context.participants.issubset(subjects): + issues.append( + _autonomous_issue( + context, + "participant.autonomous-activity-window-subject-mismatch", + constraint_ref, + ) + ) if getattr(constraint, "clock_ref", None) != context.policy.clock_ref: issues.append( _autonomous_issue( @@ -578,7 +616,7 @@ def _autonomous_constraint_issues( constraint_ref, ) ) - if cadence_count != 1: + 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 @@ -637,9 +675,25 @@ def _autonomous_stepped_cadence_issues( bindings: _AutonomousTimeBindings, progression_mode: str, ) -> list[ParticipantBehaviorIssue]: - if progression_mode != "stepped" or bindings.cadence_count != 1 or bindings.cadence is None: + if progression_mode != "stepped": return [] step_ticks = getattr(bindings.progression, "step_ticks", None) + if getattr(context.policy, "profile", "participant-autonomous-execution/v1") == ( + "participant-autonomous-execution/v2" + ): + minimum_ticks = context.policy.timing.minimum_ticks + maximum_ticks = context.policy.timing.maximum_ticks + if isinstance(step_ticks, int) and not minimum_ticks % step_ticks and not maximum_ticks % step_ticks: + return [] + return [ + _autonomous_issue( + context, + "participant.autonomous-activity-timing-unreachable", + context.policy.progression_policy_ref, + ) + ] + if bindings.cadence_count != 1 or bindings.cadence is None: + return [] 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 diff --git a/implementations/python/packages/raes/validator/_participant_execution_renderers.py b/implementations/python/packages/raes/validator/_participant_execution_renderers.py index c9b23c727..fd15d03b7 100644 --- a/implementations/python/packages/raes/validator/_participant_execution_renderers.py +++ b/implementations/python/packages/raes/validator/_participant_execution_renderers.py @@ -43,6 +43,24 @@ "participant.autonomous-constraint-clock-mismatch": ( lambda i: f"Behavior specification '{i.spec_name}' autonomous temporal constraint '{i.ref}' uses another clock" ), + "participant.autonomous-activity-window-kind-invalid": ( + lambda i: ( + f"Behavior specification '{i.spec_name}' activity work and pause refs must resolve to window constraints; " + f"'{i.ref}' does not" + ) + ), + "participant.autonomous-activity-timing-unreachable": ( + lambda i: ( + f"Behavior specification '{i.spec_name}' activity timing bounds are unreachable by stepped " + f"progression '{i.ref}'" + ) + ), + "participant.autonomous-activity-window-subject-mismatch": ( + lambda i: ( + f"Behavior specification '{i.spec_name}' activity window '{i.ref}' must name the behavior " + "specification or every governed participant as a subject" + ) + ), "participant.autonomous-cadence-missing": ( lambda i: f"Behavior specification '{i.spec_name}' autonomous execution requires exactly one cadence constraint" ), diff --git a/implementations/python/packages/raes_backend_protocols/capability_admission.py b/implementations/python/packages/raes_backend_protocols/capability_admission.py index 52af278dc..65c6953ba 100644 --- a/implementations/python/packages/raes_backend_protocols/capability_admission.py +++ b/implementations/python/packages/raes_backend_protocols/capability_admission.py @@ -22,6 +22,7 @@ class AutonomousExecutionPolicy(Protocol): + profile: str participant_addresses: tuple[str, ...] action_contract_addresses: tuple[str, ...] target_addresses: tuple[str, ...] @@ -29,6 +30,24 @@ class AutonomousExecutionPolicy(Protocol): max_action_attempts: int max_in_flight: int selection_strategy: str + action_candidate_max_retries: tuple[int, ...] + max_occurrences: int + max_burst_size: int + + +_V2_ACTIVITY_FEATURES = frozenset( + { + "work-windows", + "timing-variation", + "weighted-selection", + "dependencies", + "bounded-retries", + "cooldowns", + "limited-bursts", + "occurrence-provenance", + } +) +_V2_RANDOM_STREAM_PROFILE = "blake3-xof-participant-v1" def participant_runtime_capability_contract_gaps(manifest: BackendManifest) -> tuple[str, ...]: @@ -74,6 +93,21 @@ def _autonomous_limit_gaps( max(policy.max_in_flight for policy in policies), capability.max_autonomous_in_flight, ), + ( + "occurrences", + max((policy.max_occurrences or policy.max_action_attempts) for policy in policies), + capability.max_autonomous_occurrences, + ), + ( + "retries per occurrence", + max((max(policy.action_candidate_max_retries, default=0) or 1) for policy in policies), + capability.max_autonomous_retries_per_occurrence, + ), + ( + "burst size", + max(policy.max_burst_size for policy in policies), + capability.max_autonomous_burst_size, + ), ) for label, required, supported in limits: if supported is None or required > supported: @@ -106,12 +140,24 @@ def _unsupported_autonomous_value_gaps( {address for policy in policies for address in policy.target_addresses}, capability.supported_autonomous_target_addresses, ), + ( + "policy profiles", + {policy.profile for policy in policies}, + capability.supported_autonomous_policy_profiles, + ), ) - return [ + gaps = [ f"unsupported autonomous {label}: {', '.join(unsupported)}" for label, required, supported in requirements if (unsupported := sorted(required - supported)) ] + if any(policy.profile == "participant-autonomous-execution/v2" for policy in policies): + missing_features = sorted(_V2_ACTIVITY_FEATURES - capability.supported_autonomous_activity_features) + if missing_features: + gaps.append(f"unsupported autonomous activity features: {', '.join(missing_features)}") + if _V2_RANDOM_STREAM_PROFILE not in capability.supported_autonomous_random_stream_profiles: + gaps.append(f"unsupported autonomous random-stream profiles: {_V2_RANDOM_STREAM_PROFILE}") + return gaps def _requires_coordinated_reset( diff --git a/implementations/python/packages/raes_backend_protocols/manifest.py b/implementations/python/packages/raes_backend_protocols/manifest.py index ff9545895..c57d2f050 100644 --- a/implementations/python/packages/raes_backend_protocols/manifest.py +++ b/implementations/python/packages/raes_backend_protocols/manifest.py @@ -176,9 +176,23 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod "supported_autonomous_target_addresses": sorted( manifest.participant_runtime.supported_autonomous_target_addresses ), + "supported_autonomous_policy_profiles": sorted( + manifest.participant_runtime.supported_autonomous_policy_profiles + ), + "supported_autonomous_activity_features": sorted( + manifest.participant_runtime.supported_autonomous_activity_features + ), + "supported_autonomous_random_stream_profiles": sorted( + manifest.participant_runtime.supported_autonomous_random_stream_profiles + ), "max_autonomous_participants": manifest.participant_runtime.max_autonomous_participants, "max_autonomous_action_attempts": (manifest.participant_runtime.max_autonomous_action_attempts), "max_autonomous_in_flight": manifest.participant_runtime.max_autonomous_in_flight, + "max_autonomous_occurrences": manifest.participant_runtime.max_autonomous_occurrences, + "max_autonomous_retries_per_occurrence": ( + manifest.participant_runtime.max_autonomous_retries_per_occurrence + ), + "max_autonomous_burst_size": manifest.participant_runtime.max_autonomous_burst_size, "constraints": dict(manifest.participant_runtime.constraints), } if manifest.participant_runtime is not None @@ -339,9 +353,15 @@ def _participant_runtime_from_model( supported_autonomous_action_contracts=frozenset(model.supported_autonomous_action_contracts), supported_autonomous_observation_boundaries=frozenset(model.supported_autonomous_observation_boundaries), supported_autonomous_target_addresses=frozenset(model.supported_autonomous_target_addresses), + supported_autonomous_policy_profiles=frozenset(model.supported_autonomous_policy_profiles), + supported_autonomous_activity_features=frozenset(model.supported_autonomous_activity_features), + supported_autonomous_random_stream_profiles=frozenset(model.supported_autonomous_random_stream_profiles), max_autonomous_participants=model.max_autonomous_participants, max_autonomous_action_attempts=model.max_autonomous_action_attempts, max_autonomous_in_flight=model.max_autonomous_in_flight, + max_autonomous_occurrences=model.max_autonomous_occurrences, + max_autonomous_retries_per_occurrence=model.max_autonomous_retries_per_occurrence, + max_autonomous_burst_size=model.max_autonomous_burst_size, 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 aceffd091..be526a79f 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -133,9 +133,15 @@ class ParticipantRuntimeCapabilities: supported_autonomous_action_contracts: frozenset[str] = frozenset() supported_autonomous_observation_boundaries: frozenset[str] = frozenset() supported_autonomous_target_addresses: frozenset[str] = frozenset() + supported_autonomous_policy_profiles: frozenset[str] = frozenset() + supported_autonomous_activity_features: frozenset[str] = frozenset() + supported_autonomous_random_stream_profiles: frozenset[str] = frozenset() max_autonomous_participants: int | None = None max_autonomous_action_attempts: int | None = None max_autonomous_in_flight: int | None = None + max_autonomous_occurrences: int | None = None + max_autonomous_retries_per_occurrence: int | None = None + max_autonomous_burst_size: int | None = None constraints: dict[str, str] = field(default_factory=dict) def __post_init__(self) -> None: @@ -201,13 +207,20 @@ def _validate_autonomous_execution(self) -> None: def _validate_enabled_autonomous_execution(self) -> None: if not self.supported_autonomous_selection_strategies: raise ValueError("autonomous execution requires supported selection strategies") - unknown_strategies = sorted(self.supported_autonomous_selection_strategies - {"ordered_cycle"}) + unknown_strategies = sorted(self.supported_autonomous_selection_strategies - {"ordered_cycle", "weighted"}) if unknown_strategies: raise ValueError("unsupported autonomous selection strategies: " + ", ".join(unknown_strategies)) if not self.supported_autonomous_action_contracts: raise ValueError("autonomous execution requires exact supported action contracts") if not self.supported_autonomous_observation_boundaries: raise ValueError("autonomous execution requires exact supported observation boundaries") + if not self.supported_autonomous_policy_profiles: + raise ValueError("autonomous execution requires exact supported policy profiles") + if "participant-autonomous-execution/v2" in self.supported_autonomous_policy_profiles: + if not self.supported_autonomous_activity_features: + raise ValueError("autonomous execution v2 requires exact supported activity features") + if not self.supported_autonomous_random_stream_profiles: + raise ValueError("autonomous execution v2 requires exact supported random-stream profiles") self._validate_autonomous_addresses() for label, value in self._autonomous_limits(): if value is None or value < 1: @@ -227,6 +240,9 @@ def _autonomous_limits(self) -> tuple[tuple[str, int | None], ...]: ("max_autonomous_participants", self.max_autonomous_participants), ("max_autonomous_action_attempts", self.max_autonomous_action_attempts), ("max_autonomous_in_flight", self.max_autonomous_in_flight), + ("max_autonomous_occurrences", self.max_autonomous_occurrences), + ("max_autonomous_retries_per_occurrence", self.max_autonomous_retries_per_occurrence), + ("max_autonomous_burst_size", self.max_autonomous_burst_size), ) def _has_autonomous_configuration(self) -> bool: @@ -235,6 +251,9 @@ def _has_autonomous_configuration(self) -> bool: or self.supported_autonomous_action_contracts or self.supported_autonomous_observation_boundaries or self.supported_autonomous_target_addresses + or self.supported_autonomous_policy_profiles + or self.supported_autonomous_activity_features + or self.supported_autonomous_random_stream_profiles or any(value is not None for _, value in self._autonomous_limits()) ) diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index 5a9715c2d..f75d30777 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -227,6 +227,7 @@ ParticipantActionEffectResultModel, ParticipantActionPreconditionResultModel, ParticipantActionResultModel, + ParticipantActivityOccurrenceProvenanceModel, ParticipantAttributionCandidateModel, ParticipantAttributionEdgeModel, ParticipantAttributionEvidenceBasisModel, @@ -417,7 +418,8 @@ "ParticipantActionPreconditionResultModel", "ParticipantActionResultModel", "ParticipantAttributionCandidateModel", "ParticipantAttributionEdgeModel", "ParticipantAttributionEvidenceBasisModel", "ParticipantAttributionOrderingBasisModel", - "ParticipantAutonomousExecutionStateModel", "ParticipantBehaviorHistoryEventModel", + "ParticipantActivityOccurrenceProvenanceModel", "ParticipantAutonomousExecutionStateModel", + "ParticipantBehaviorHistoryEventModel", "ParticipantContextViewModel", "ParticipantControlDeclarationModel", "ParticipantControlOccurrenceModel", "ParticipantCrossingOccurrenceModel", "validate_participant_control_occurrence_context", diff --git a/implementations/python/packages/raes_contracts/contracts/manifests.py b/implementations/python/packages/raes_contracts/contracts/manifests.py index 7edf4add3..47239697a 100644 --- a/implementations/python/packages/raes_contracts/contracts/manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/manifests.py @@ -28,6 +28,7 @@ ProvisionerCapabilitiesModel, ) from .experiment_bindings import ConfigurationTargetRegistryModel +from .time_manifest_capabilities import TimeCapabilitiesModel from .trial_cleanup import CleanupActionKind from .validators import ( _validate_canonical_concept_bindings, @@ -166,7 +167,7 @@ class ParticipantRuntimeCapabilitiesModel(ContractModel): ) feature_support: list[ParticipantFeatureSupportModel] = Field(default_factory=list) supports_autonomous_execution: bool = False - supported_autonomous_selection_strategies: list[Literal["ordered_cycle"]] = Field( + supported_autonomous_selection_strategies: list[Literal["ordered_cycle", "weighted"]] = Field( default_factory=list, json_schema_extra={"uniqueItems": True}, ) @@ -182,9 +183,31 @@ class ParticipantRuntimeCapabilitiesModel(ContractModel): default_factory=list, json_schema_extra={"uniqueItems": True}, ) + supported_autonomous_policy_profiles: list[ + Literal["participant-autonomous-execution/v1", "participant-autonomous-execution/v2"] + ] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) + supported_autonomous_activity_features: list[ + Literal[ + "work-windows", + "timing-variation", + "weighted-selection", + "dependencies", + "bounded-retries", + "cooldowns", + "limited-bursts", + "occurrence-provenance", + ] + ] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) + supported_autonomous_random_stream_profiles: list[Literal["blake3-xof-participant-v1"]] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) max_autonomous_participants: int | None = Field(default=None, ge=1) max_autonomous_action_attempts: int | None = Field(default=None, ge=1) max_autonomous_in_flight: int | None = Field(default=None, ge=1) + max_autonomous_occurrences: int | None = Field(default=None, ge=1) + max_autonomous_retries_per_occurrence: int | None = Field(default=None, ge=1) + max_autonomous_burst_size: int | None = Field(default=None, ge=1) constraints: dict[str, str] = Field(default_factory=dict) @model_validator(mode="after") @@ -230,8 +253,18 @@ def _validate_autonomous_configuration(self) -> None: raise ValueError("autonomous_execution feature and support flag must agree") if self.supports_autonomous_execution and not self._has_complete_autonomous_configuration(): raise ValueError( - "autonomous execution requires selection strategies, exact action and observation support, " - "and finite limits" + "autonomous execution requires selection strategies, exact action, observation, and policy-profile " + "support, and finite limits" + ) + if ( + self.supports_autonomous_execution + and "participant-autonomous-execution/v2" in self.supported_autonomous_policy_profiles + and ( + not self.supported_autonomous_activity_features or not self.supported_autonomous_random_stream_profiles + ) + ): + raise ValueError( + "autonomous execution v2 requires exact activity-feature and random-stream-profile support" ) if not self.supports_autonomous_execution and self._has_any_autonomous_configuration(): raise ValueError("autonomous execution limits require autonomous execution support") @@ -241,6 +274,7 @@ def _has_complete_autonomous_configuration(self) -> bool: self.supported_autonomous_selection_strategies and self.supported_autonomous_action_contracts and self.supported_autonomous_observation_boundaries + and self.supported_autonomous_policy_profiles and all(value is not None for value in self._autonomous_limits()) ) @@ -250,14 +284,20 @@ def _has_any_autonomous_configuration(self) -> bool: or self.supported_autonomous_action_contracts or self.supported_autonomous_observation_boundaries or self.supported_autonomous_target_addresses + or self.supported_autonomous_policy_profiles + or self.supported_autonomous_activity_features + or self.supported_autonomous_random_stream_profiles or any(value is not None for value in self._autonomous_limits()) ) - def _autonomous_limits(self) -> tuple[int | None, int | None, int | None]: + def _autonomous_limits(self) -> tuple[int | None, ...]: return ( self.max_autonomous_participants, self.max_autonomous_action_attempts, self.max_autonomous_in_flight, + self.max_autonomous_occurrences, + self.max_autonomous_retries_per_occurrence, + self.max_autonomous_burst_size, ) def _validate_autonomous_addresses(self) -> None: @@ -341,72 +381,6 @@ def _validate_cleanup_capability(self) -> CleanupCapabilitiesModel: return self -_TIME_CAPABILITY_REQUIRED_CONTRACTS = { - "time-model-v1", - "time-runtime-state-v1", - "realized-time-model-v1", - "runtime-snapshot-v1", - "experiment-run-v1", -} -_TIME_CAPABILITY_TERMS = { - "supported_domain_kinds": {"wall_clock", "monotonic", "simulated", "logical", "external"}, - "supported_authority_kinds": {"runtime", "backend", "system", "external"}, - "supported_advancement_modes": { - "real_time", - "dilated", - "stepped", - "event_driven", - "externally_paced", - }, - "supported_synchronization_modes": {"none", "authority", "barrier", "conservative"}, - "supported_mapping_kinds": {"identity", "affine_rational"}, - "supported_constraint_kinds": {"precedence", "duration", "window", "deadline", "cadence"}, - "supported_reset_behaviors": {"unsupported", "new_segment_zero", "new_segment_preserve_value"}, - "supported_replay_behaviors": {"unsupported", "restart_from_anchor", "restore_recorded_advances"}, -} - - -class TimeCapabilitiesModel(ContractModel): - """Backend support for the API-421 portable shared-time contract family.""" - - name: NonEmptyString - supported_contract_versions: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) - supported_domain_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) - supported_authority_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) - supported_advancement_modes: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) - supported_synchronization_modes: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) - supported_mapping_kinds: list[NonEmptyString] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) - supported_constraint_kinds: list[NonEmptyString] = Field( - default_factory=list, json_schema_extra={"uniqueItems": True} - ) - supported_reset_behaviors: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) - supported_replay_behaviors: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) - max_time_domains: int | None = Field(default=None, ge=1) - max_clocks: int | None = Field(default=None, ge=1) - supports_pause: bool = False - supports_jump: bool = False - supports_exact_rational_mappings: bool = False - supports_append_only_history: bool = False - supports_run_provenance: bool = False - supports_coordinated_participant_reset: bool = False - constraints: dict[str, str] = Field(default_factory=dict) - - @model_validator(mode="after") - def _validate_time_capability(self) -> TimeCapabilitiesModel: - if set(self.supported_contract_versions) != _TIME_CAPABILITY_REQUIRED_CONTRACTS: - raise ValueError("time capabilities require the complete time contract family") - validate_backend_supported_contract_versions(self.supported_contract_versions) - for field_name, allowed in _TIME_CAPABILITY_TERMS.items(): - values = getattr(self, field_name) - _validate_unique_string_values(f"time {field_name}", values) - unknown = sorted(set(values) - allowed) - if unknown: - raise ValueError(f"time {field_name} contains unknown values: {', '.join(unknown)}") - if self.supported_mapping_kinds and not self.supports_exact_rational_mappings: - raise ValueError("time mapping support requires exact rational mappings") - return self - - class BackendCapabilitiesV2Model(ContractModel): provisioner: ProvisionerCapabilitiesModel orchestrator: OrchestratorCapabilitiesModel | None = None diff --git a/implementations/python/packages/raes_contracts/contracts/participant_runtime.py b/implementations/python/packages/raes_contracts/contracts/participant_runtime.py index 43ba220fc..681d58a5f 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_runtime.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_runtime.py @@ -4,7 +4,7 @@ from typing import Any, Literal -from pydantic import Field, StrictInt, model_validator +from pydantic import Field, SerializerFunctionWrapHandler, StrictInt, model_serializer, model_validator from raes.participant_attribution_semantics import ( ParticipantAttributionCandidateKind, ParticipantAttributionOrderingBasisKind, @@ -36,6 +36,7 @@ ) from ..versions import PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION from .base import ContractModel, NonEmptyString +from .random_stream import ParticipantStreamAddressModel class ParticipantEpisodeStateModel(ContractModel): @@ -189,6 +190,25 @@ class ParticipantOutcomeInterpretationRecordModel(ContractModel): diagnostics: list[NonEmptyString] = Field(default_factory=list) +class ParticipantActivityOccurrenceProvenanceModel(ContractModel): + """Safe within-run scheduler provenance for one native action attempt.""" + + policy_address: NonEmptyString + policy_profile: Literal["participant-autonomous-execution/v2"] + occurrence_id: NonEmptyString + attempt_id: NonEmptyString + predecessor_attempt_id: NonEmptyString | None = None + candidate_id: NonEmptyString + dependency_candidate_ids: list[NonEmptyString] = Field(default_factory=list) + timing_tick: StrictInt = Field(ge=0) + timing_disposition: Literal["drawn", "next_opening", "retry", "burst"] + burst_position: StrictInt = Field(ge=0) + random_control_id: NonEmptyString + random_profile_id: NonEmptyString + random_address: ParticipantStreamAddressModel + terminal_outcome: NonEmptyString + + class ParticipantBehaviorHistoryEventModel(ContractModel): event_type: ParticipantBehaviorHistoryEventType timestamp: NonEmptyString @@ -215,6 +235,7 @@ class ParticipantBehaviorHistoryEventModel(ContractModel): attribution_edges: list[ParticipantAttributionEdgeModel] = Field(default_factory=list) outcome_interpretations: list[ParticipantOutcomeInterpretationRecordModel] = Field(default_factory=list) temporal_contexts: list[ParticipantTemporalRuntimeContextModel] = Field(default_factory=list) + activity_provenance: ParticipantActivityOccurrenceProvenanceModel | None = None details: ParticipantObservationDetailsModel = Field(default_factory=ParticipantObservationDetailsModel) @model_validator(mode="after") @@ -250,11 +271,59 @@ class ParticipantAutonomousExecutionStateModel(ContractModel): failed_actions: StrictInt = Field(ge=0) in_flight: StrictInt = Field(default=0, ge=0) last_action_instance_id: str | None = None + profile: Literal[ + "participant-autonomous-execution/v1", + "participant-autonomous-execution/v2", + ] = "participant-autonomous-execution/v1" + occurrence_ordinal: StrictInt = Field(default=0, ge=0) + current_retry: StrictInt = Field(default=0, ge=0) + burst_position: StrictInt = Field(default=0, ge=0) + burst_size: StrictInt = Field(default=1, ge=1) + next_timing_disposition: Literal["cadence", "drawn", "next_opening"] = "cadence" + last_candidate_id: str | None = None + completed_candidate_ids: list[NonEmptyString] = Field(default_factory=list) + candidate_cooldown_until: dict[NonEmptyString, StrictInt] = Field(default_factory=dict) + random_control_id: str | None = None + random_profile_id: str | None = None + random_namespace: str | None = None + + @model_serializer(mode="wrap") + def _serialize_profile_state( + self, + handler: SerializerFunctionWrapHandler, + ) -> dict[str, Any]: + payload = handler(self) + if self.profile == "participant-autonomous-execution/v1": + for field_name in ( + "profile", + "occurrence_ordinal", + "current_retry", + "burst_position", + "burst_size", + "next_timing_disposition", + "last_candidate_id", + "completed_candidate_ids", + "candidate_cooldown_until", + "random_control_id", + "random_profile_id", + "random_namespace", + ): + payload.pop(field_name, None) + return payload @model_validator(mode="after") def _validate_counters(self) -> ParticipantAutonomousExecutionStateModel: if self.succeeded_actions + self.failed_actions > self.attempted_actions: raise ValueError("terminal autonomous action counts cannot exceed attempted actions") + if self.profile == "participant-autonomous-execution/v1": + if any((self.random_control_id, self.random_profile_id, self.random_namespace)): + raise ValueError("v1 autonomous execution state cannot carry participant random-control identity") + elif not all((self.random_control_id, self.random_profile_id, self.random_namespace)): + raise ValueError("v2 autonomous execution state requires complete participant random-control identity") + if len(self.completed_candidate_ids) != len(set(self.completed_candidate_ids)): + raise ValueError("completed autonomous activity candidate ids must be unique") + if self.current_retry > self.attempted_actions: + raise ValueError("autonomous activity current_retry cannot exceed attempted actions") return self diff --git a/implementations/python/packages/raes_contracts/contracts/participant_views.py b/implementations/python/packages/raes_contracts/contracts/participant_views.py index 272d3e37f..f3f3baf6d 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_views.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_views.py @@ -23,6 +23,7 @@ from .participant_envelopes import ParticipantRuntimeBaseEnvelopeModel from .participant_runtime import ( ParticipantActionResultModel, + ParticipantActivityOccurrenceProvenanceModel, ParticipantAttributionEdgeModel, ParticipantObservationDetailsModel, ParticipantOutcomeInterpretationRecordModel, @@ -115,6 +116,7 @@ class ParticipantHistoryViewBehaviorEventModel(ContractModel): attribution_edges: list[ParticipantAttributionEdgeModel] = Field(default_factory=list) outcome_interpretations: list[ParticipantOutcomeInterpretationRecordModel] = Field(default_factory=list) temporal_contexts: list[ParticipantTemporalRuntimeContextModel] = Field(default_factory=list) + activity_provenance: ParticipantActivityOccurrenceProvenanceModel | None = None details: ParticipantObservationDetailsModel = Field(default_factory=ParticipantObservationDetailsModel) @model_validator(mode="after") diff --git a/implementations/python/packages/raes_contracts/contracts/random_stream.py b/implementations/python/packages/raes_contracts/contracts/random_stream.py index cfaf03468..92924c4b3 100644 --- a/implementations/python/packages/raes_contracts/contracts/random_stream.py +++ b/implementations/python/packages/raes_contracts/contracts/random_stream.py @@ -157,6 +157,47 @@ def __get_pydantic_json_schema__( return json_schema +class ParticipantStreamAddressModel(ContractModel): + """Closed within-run address for one participant-policy draw.""" + + namespace: PortableIdentifier + policy_address: NonEmptyString + participant_address: NonEmptyString + time_segment: NonNegativeInteger + occurrence_ordinal: NonNegativeInteger + draw_purpose: PortableIdentifier + local_coordinate: NonNegativeInteger + + @model_validator(mode="after") + def _validate_address(self) -> ParticipantStreamAddressModel: + _validate_controlled_vocabulary_terms(RANDOM_STREAM_DRAW_PURPOSE_SCOPE, [self.draw_purpose]) + if not self.policy_address.startswith("participant.autonomous-execution."): + raise ValueError("participant stream policy_address must be a compiled autonomous-execution address") + if not self.participant_address.startswith("participant.behavior."): + raise ValueError("participant stream participant_address must be a compiled participant address") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + _add_aces_invariant( + json_schema, + "participant-random-stream-address-governed", + ( + "draw_purpose must be governed and policy_address/participant_address must be compiled " + "autonomous participant addresses." + ), + validator=("raes_contracts.contracts.random_stream.ParticipantStreamAddressModel._validate_address"), + inputs=[{"contract_id": "controlled-vocabularies-v1", "instance_path": "#"}], + ) + return json_schema + + class RandomStreamControlBindingModel(ContractModel): """Executable binding: profile identity, namespace, and root entropy. @@ -358,7 +399,7 @@ class RandomStreamVectorModel(ContractModel): profile_id: SemanticProfileId root_entropy: PublicSeedModel stream_key_hex: NonEmptyString = Field(pattern=_PUBLIC_SEED_HEX_PATTERN) - address: StreamAddressModel + address: StreamAddressModel | ParticipantStreamAddressModel address_canonical_bytes_hex: NonEmptyString raw_block_hex: NonEmptyString transform: RandomStreamBoundedIntegerVectorCaseModel | None = None diff --git a/implementations/python/packages/raes_contracts/contracts/time_manifest_capabilities.py b/implementations/python/packages/raes_contracts/contracts/time_manifest_capabilities.py new file mode 100644 index 000000000..8842f45b9 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/time_manifest_capabilities.py @@ -0,0 +1,77 @@ +"""Backend manifest capability declarations for shared time.""" + +from __future__ import annotations + +from pydantic import Field, model_validator + +from ..manifest_authority import validate_backend_supported_contract_versions +from .base import ContractModel, NonEmptyString +from .validators import _validate_unique_string_values + +_TIME_CAPABILITY_REQUIRED_CONTRACTS = { + "time-model-v1", + "time-runtime-state-v1", + "realized-time-model-v1", + "runtime-snapshot-v1", + "experiment-run-v1", +} +_TIME_CAPABILITY_TERMS = { + "supported_domain_kinds": {"wall_clock", "monotonic", "simulated", "logical", "external"}, + "supported_authority_kinds": {"runtime", "backend", "system", "external"}, + "supported_advancement_modes": { + "real_time", + "dilated", + "stepped", + "event_driven", + "externally_paced", + }, + "supported_synchronization_modes": {"none", "authority", "barrier", "conservative"}, + "supported_mapping_kinds": {"identity", "affine_rational"}, + "supported_constraint_kinds": {"precedence", "duration", "window", "deadline", "cadence"}, + "supported_reset_behaviors": {"unsupported", "new_segment_zero", "new_segment_preserve_value"}, + "supported_replay_behaviors": {"unsupported", "restart_from_anchor", "restore_recorded_advances"}, +} + + +class TimeCapabilitiesModel(ContractModel): + """Backend support for the API-421 portable shared-time contract family.""" + + name: NonEmptyString + supported_contract_versions: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_domain_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_authority_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_advancement_modes: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_synchronization_modes: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_mapping_kinds: list[NonEmptyString] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) + supported_constraint_kinds: list[NonEmptyString] = Field( + default_factory=list, json_schema_extra={"uniqueItems": True} + ) + supported_reset_behaviors: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_replay_behaviors: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + max_time_domains: int | None = Field(default=None, ge=1) + max_clocks: int | None = Field(default=None, ge=1) + supports_pause: bool = False + supports_jump: bool = False + supports_exact_rational_mappings: bool = False + supports_append_only_history: bool = False + supports_run_provenance: bool = False + supports_coordinated_participant_reset: bool = False + constraints: dict[str, str] = Field(default_factory=dict) + + @model_validator(mode="after") + def _validate_time_capability(self) -> TimeCapabilitiesModel: + if set(self.supported_contract_versions) != _TIME_CAPABILITY_REQUIRED_CONTRACTS: + raise ValueError("time capabilities require the complete time contract family") + validate_backend_supported_contract_versions(self.supported_contract_versions) + for field_name, allowed in _TIME_CAPABILITY_TERMS.items(): + values = getattr(self, field_name) + _validate_unique_string_values(f"time {field_name}", values) + unknown = sorted(set(values) - allowed) + if unknown: + raise ValueError(f"time {field_name} contains unknown values: {', '.join(unknown)}") + if self.supported_mapping_kinds and not self.supports_exact_rational_mappings: + raise ValueError("time mapping support requires exact rational mappings") + return self + + +__all__ = ["TimeCapabilitiesModel"] diff --git a/implementations/python/packages/raes_contracts/random_stream_engine.py b/implementations/python/packages/raes_contracts/random_stream_engine.py index 56c005ed5..08a1a71ac 100644 --- a/implementations/python/packages/raes_contracts/random_stream_engine.py +++ b/implementations/python/packages/raes_contracts/random_stream_engine.py @@ -22,9 +22,10 @@ bias, recording ``rejection_attempts`` and enforcing a bounded max-attempt budget: deterministic exhaustion failure, never fallback or clamping. -Only the ``blake3-xof-v1`` profile id is dispatched. An unknown/unsupported -profile id fails closed (``ValueError``) rather than falling back to a -library default or a dynamic plugin lookup. +Only the published ``blake3-xof-v1`` and ``blake3-xof-participant-v1`` +profile ids are dispatched. An unknown/unsupported profile id fails closed +(``ValueError``) rather than falling back to a library default or a dynamic +plugin lookup. """ from __future__ import annotations @@ -34,7 +35,7 @@ import blake3 import rfc8785 -from .contracts.random_stream import PublicSeedModel, StreamAddressModel +from .contracts.random_stream import ParticipantStreamAddressModel, PublicSeedModel, StreamAddressModel from .diagnostics import Diagnostic, Severity from .random_stream_profiles import SUPPORTED_RANDOM_STREAM_PROFILE_IDS @@ -85,7 +86,7 @@ def derive_stream_key(*, profile_id: str, root_entropy: bytes) -> bytes: return hasher.digest() -def canonical_stream_address_bytes(address: StreamAddressModel) -> bytes: +def canonical_stream_address_bytes(address: StreamAddressModel | ParticipantStreamAddressModel) -> bytes: """Return the RFC 8785/JCS canonical bytes for one closed ``StreamAddressModel``. Uses ``exclude_none=True`` rather than ``exclude_unset=True``: the @@ -107,7 +108,7 @@ def raw_block( *, profile_id: str, stream_key: bytes, - address: StreamAddressModel, + address: StreamAddressModel | ParticipantStreamAddressModel, byte_length: int = BLOCK_BYTES, byte_offset: int = 0, ) -> bytes: @@ -149,7 +150,7 @@ def draw_bounded_integer( *, profile_id: str, stream_key: bytes, - address: StreamAddressModel, + address: StreamAddressModel | ParticipantStreamAddressModel, minimum: int, maximum: int, max_rejection_attempts: int, diff --git a/implementations/python/packages/raes_contracts/random_stream_profiles.py b/implementations/python/packages/raes_contracts/random_stream_profiles.py index cb7037eb0..292434509 100644 --- a/implementations/python/packages/raes_contracts/random_stream_profiles.py +++ b/implementations/python/packages/raes_contracts/random_stream_profiles.py @@ -25,7 +25,7 @@ # ``contracts/profiles/random-stream/`` *and* its id here -- there is no # dynamic plugin, "latest" alias, or version-range fallback (EXP-718 # preflight, "One Profile And One Stateless API"). -SUPPORTED_RANDOM_STREAM_PROFILE_IDS = frozenset({"blake3-xof-v1"}) +SUPPORTED_RANDOM_STREAM_PROFILE_IDS = frozenset({"blake3-xof-participant-v1", "blake3-xof-v1"}) def random_stream_profiles_root() -> Path: diff --git a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py new file mode 100644 index 000000000..e69deaf02 --- /dev/null +++ b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py @@ -0,0 +1,163 @@ +"""Compilation of autonomous participant execution policies.""" + +from raes.scenario import InstantiatedScenario + +from ..models import ParticipantAutonomousExecutionRuntime +from .addresses import ( + _action_contract_address, + _behavior_specification_address, + _objective_address, + _observation_boundary_address, + _section_ref_name, +) +from .alias_index import _runtime_addressable_ref_index, _runtime_addresses_for_refs +from .support import _address, _dump + + +def _compile_autonomous_execution( + *, + scenario: InstantiatedScenario, + spec_name: str, + participant_addresses: tuple[str, ...], + behavior_spec: object, +) -> ParticipantAutonomousExecutionRuntime | None: + policy = behavior_spec.autonomous_execution + if policy is None: + return None + address = _address("participant", "autonomous-execution", spec_name) + authority = policy.evaluation_authority + profile = getattr(policy, "profile", "participant-autonomous-execution/v1") + activity_candidates = getattr(policy, "action_candidates", None) + ordered_candidates = sorted(activity_candidates.items()) if activity_candidates is not None else [] + action_refs = ( + [candidate.action_ref for _, candidate in ordered_candidates] + if ordered_candidates + else list(policy.action_order) + ) + work_window_refs = list(getattr(policy, "work_window_refs", ())) + pause_window_refs = list(getattr(policy, "pause_window_refs", ())) + temporal_constraint_refs = ( + [*work_window_refs, *pause_window_refs] + if profile == "participant-autonomous-execution/v2" + else list(policy.temporal_constraint_refs) + ) + addressable_ref_index = _runtime_addressable_ref_index(scenario) + target_refs = [ + str(ref) + for action_ref in action_refs + for effect in scenario.action_contracts[ + _section_ref_name(action_ref, "action_contracts", scenario.action_contracts) + ].effects + for ref in effect.target_refs + ] + target_refs.extend( + str(ref) + for action_ref in action_refs + for precondition in scenario.action_contracts[ + _section_ref_name(action_ref, "action_contracts", scenario.action_contracts) + ].preconditions + for ref in precondition.support_refs + ) + target_addresses = _runtime_addresses_for_refs( + list(dict.fromkeys(target_refs)), + addressable_ref_index=addressable_ref_index, + ) + return ParticipantAutonomousExecutionRuntime( + address=address, + name=spec_name, + behavior_specification_address=_behavior_specification_address(spec_name), + participant_addresses=participant_addresses, + participant_implementation_ref=policy.participant_implementation_ref, + clock_address=_address("time", "clock", _section_ref_name(policy.clock_ref, "clocks", scenario.clocks)), + progression_policy_address=_address( + "time", + "policy", + _section_ref_name( + policy.progression_policy_ref, + "time_progression_policies", + scenario.time_progression_policies, + ), + ), + temporal_constraint_addresses=tuple( + _address( + "time", + "constraint", + _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), + ) + for ref in temporal_constraint_refs + ), + action_contract_addresses=tuple( + _action_contract_address(_section_ref_name(ref, "action_contracts", scenario.action_contracts)) + for ref in action_refs + ), + target_addresses=target_addresses, + observation_boundary_address=_observation_boundary_address( + _section_ref_name( + policy.observation_boundary_ref, + "observation_boundaries", + scenario.observation_boundaries, + ) + ), + selection_strategy=policy.selection_strategy, + max_action_attempts=policy.max_action_attempts, + max_in_flight=policy.max_in_flight, + failure_policy=policy.failure_policy.value, + evaluation_authority_mode=authority.mode.value, + objective_refs=tuple( + _objective_address(_section_ref_name(ref, "objectives", scenario.objectives)) + for ref in authority.objective_refs + ), + proof_producer_refs=tuple(authority.proof_producer_refs), + score_authority_refs=tuple(authority.score_authority_refs), + receipt_authority_refs=tuple(authority.receipt_authority_refs), + profile=profile, + work_window_addresses=tuple( + _address( + "time", + "constraint", + _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), + ) + for ref in work_window_refs + ), + pause_window_addresses=tuple( + _address( + "time", + "constraint", + _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), + ) + for ref in pause_window_refs + ), + stochastic_control_ref=str(getattr(policy, "stochastic_control_ref", "")), + timing_minimum_ticks=int(getattr(getattr(policy, "timing", None), "minimum_ticks", 0)), + timing_maximum_ticks=int(getattr(getattr(policy, "timing", None), "maximum_ticks", 0)), + outside_window_disposition=str(getattr(policy, "outside_window_disposition", "")), + empty_eligible_disposition=str(getattr(policy, "empty_eligible_disposition", "")), + action_candidate_ids=tuple(str(candidate_id) for candidate_id, _ in ordered_candidates), + action_candidate_weights=tuple(candidate.weight for _, candidate in ordered_candidates), + action_candidate_dependencies=tuple( + tuple(str(ref) for ref in candidate.depends_on) for _, candidate in ordered_candidates + ), + action_candidate_retry_failure_classes=tuple( + tuple(value.value for value in candidate.retryable_failure_classes) for _, candidate in ordered_candidates + ), + action_candidate_max_retries=tuple(candidate.max_retries for _, candidate in ordered_candidates), + action_candidate_cooldown_ticks=tuple(candidate.cooldown_ticks for _, candidate in ordered_candidates), + max_occurrences=int(getattr(policy, "max_occurrences", 0)), + max_burst_size=int(getattr(policy, "max_burst_size", 1)), + refresh_dependencies=( + *participant_addresses, + *tuple( + _action_contract_address(_section_ref_name(ref, "action_contracts", scenario.action_contracts)) + for ref in action_refs + ), + *tuple( + _objective_address(_section_ref_name(ref, "objectives", scenario.objectives)) + for ref in authority.objective_refs + ), + *target_addresses, + ), + spec=_dump(policy), + ) + + +__all__ = ["_compile_autonomous_execution"] diff --git a/implementations/python/packages/raes_processor/compiler/participant_behaviors.py b/implementations/python/packages/raes_processor/compiler/participant_behaviors.py index 78f78bdad..6cf8b2c5c 100644 --- a/implementations/python/packages/raes_processor/compiler/participant_behaviors.py +++ b/implementations/python/packages/raes_processor/compiler/participant_behaviors.py @@ -6,7 +6,6 @@ from ..models import ( Diagnostic, - ParticipantAutonomousExecutionRuntime, ParticipantBehaviorRuntime, ParticipantBehaviorSpecificationRuntime, ParticipantInteractiveAccessRuntime, @@ -18,7 +17,6 @@ _assertion_address, _behavior_specification_address, _content_address, - _objective_address, _observation_boundary_address, _outcome_interpretation_rule_address, _participant_behavior_address, @@ -32,104 +30,8 @@ _runtime_addressable_ref_index, _runtime_addresses_for_refs, ) -from .support import _address, _dedupe, _dump - - -def _compile_autonomous_execution( - *, - scenario: InstantiatedScenario, - spec_name: str, - participant_addresses: tuple[str, ...], - behavior_spec: object, -) -> ParticipantAutonomousExecutionRuntime | None: - policy = behavior_spec.autonomous_execution - if policy is None: - return None - address = _address("participant", "autonomous-execution", spec_name) - authority = policy.evaluation_authority - addressable_ref_index = _runtime_addressable_ref_index(scenario) - target_refs = [ - str(ref) - for action_ref in policy.action_order - for effect in scenario.action_contracts[ - _section_ref_name(action_ref, "action_contracts", scenario.action_contracts) - ].effects - for ref in effect.target_refs - ] - target_refs.extend( - str(ref) - for action_ref in policy.action_order - for precondition in scenario.action_contracts[ - _section_ref_name(action_ref, "action_contracts", scenario.action_contracts) - ].preconditions - for ref in precondition.support_refs - ) - target_addresses = _runtime_addresses_for_refs( - list(dict.fromkeys(target_refs)), - addressable_ref_index=addressable_ref_index, - ) - return ParticipantAutonomousExecutionRuntime( - address=address, - name=spec_name, - behavior_specification_address=_behavior_specification_address(spec_name), - participant_addresses=participant_addresses, - participant_implementation_ref=policy.participant_implementation_ref, - clock_address=_address("time", "clock", _section_ref_name(policy.clock_ref, "clocks", scenario.clocks)), - progression_policy_address=_address( - "time", - "policy", - _section_ref_name( - policy.progression_policy_ref, - "time_progression_policies", - scenario.time_progression_policies, - ), - ), - temporal_constraint_addresses=tuple( - _address( - "time", - "constraint", - _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), - ) - for ref in policy.temporal_constraint_refs - ), - action_contract_addresses=tuple( - _action_contract_address(_section_ref_name(ref, "action_contracts", scenario.action_contracts)) - for ref in policy.action_order - ), - target_addresses=target_addresses, - observation_boundary_address=_observation_boundary_address( - _section_ref_name( - policy.observation_boundary_ref, - "observation_boundaries", - scenario.observation_boundaries, - ) - ), - selection_strategy=policy.selection_strategy, - max_action_attempts=policy.max_action_attempts, - max_in_flight=policy.max_in_flight, - failure_policy=policy.failure_policy.value, - evaluation_authority_mode=authority.mode.value, - objective_refs=tuple( - _objective_address(_section_ref_name(ref, "objectives", scenario.objectives)) - for ref in authority.objective_refs - ), - proof_producer_refs=tuple(authority.proof_producer_refs), - score_authority_refs=tuple(authority.score_authority_refs), - receipt_authority_refs=tuple(authority.receipt_authority_refs), - refresh_dependencies=( - *participant_addresses, - *tuple( - _action_contract_address(_section_ref_name(ref, "action_contracts", scenario.action_contracts)) - for ref in policy.action_order - ), - *tuple( - _objective_address(_section_ref_name(ref, "objectives", scenario.objectives)) - for ref in authority.objective_refs - ), - *target_addresses, - ), - spec=_dump(policy), - ) +from .participant_autonomous_execution import _compile_autonomous_execution +from .support import _dedupe, _dump def _participant_action_addresses( diff --git a/implementations/python/packages/raes_processor/models/behavior_resources.py b/implementations/python/packages/raes_processor/models/behavior_resources.py index d5eb26e8f..bfb66f703 100644 --- a/implementations/python/packages/raes_processor/models/behavior_resources.py +++ b/implementations/python/packages/raes_processor/models/behavior_resources.py @@ -105,6 +105,22 @@ class ParticipantAutonomousExecutionRuntime(ResolvedResource): proof_producer_refs: tuple[str, ...] = () score_authority_refs: tuple[str, ...] = () receipt_authority_refs: tuple[str, ...] = () + profile: str = "participant-autonomous-execution/v1" + work_window_addresses: tuple[str, ...] = () + pause_window_addresses: tuple[str, ...] = () + stochastic_control_ref: str = "" + timing_minimum_ticks: int = 0 + timing_maximum_ticks: int = 0 + outside_window_disposition: str = "" + empty_eligible_disposition: str = "" + action_candidate_ids: tuple[str, ...] = () + action_candidate_weights: tuple[int, ...] = () + action_candidate_dependencies: tuple[tuple[str, ...], ...] = () + action_candidate_retry_failure_classes: tuple[tuple[str, ...], ...] = () + action_candidate_max_retries: tuple[int, ...] = () + action_candidate_cooldown_ticks: tuple[int, ...] = () + max_occurrences: int = 0 + max_burst_size: int = 1 @dataclass(frozen=True) diff --git a/implementations/python/packages/raes_runtime/manager.py b/implementations/python/packages/raes_runtime/manager.py index 3b20fcc4d..98d8ce5ca 100644 --- a/implementations/python/packages/raes_runtime/manager.py +++ b/implementations/python/packages/raes_runtime/manager.py @@ -1,7 +1,9 @@ """Runtime manager for compiled SDL runtime plans.""" +from collections.abc import Iterable from dataclasses import dataclass +from raes_contracts.contracts import ExperimentStochasticControlModel from raes_contracts.contracts.time_model import TimeModelDeclarationModel from raes_contracts.diagnostics import Diagnostic from raes_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp, RuntimeDomain @@ -13,6 +15,7 @@ from .apply_failure import maybe_synthesize_failure, rollback_services from .backend_calls import _call_backend_apply, _call_backend_diagnostics 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 .time_control import RuntimeTimeControlMixin @@ -87,6 +90,7 @@ def __init__( target: RuntimeTarget, *, initial_snapshot: RuntimeSnapshot | None = None, + stochastic_controls: Iterable[ExperimentStochasticControlModel] = (), ) -> None: _validate_runtime_target_shape( manifest=target.manifest, @@ -98,6 +102,7 @@ def __init__( ) self._target = target self._snapshot = initial_snapshot if initial_snapshot is not None else RuntimeSnapshot() + self._participant_activity_controls = resolve_participant_activity_controls(stochastic_controls) self._time_declaration: TimeModelDeclarationModel | None = None self._initialize_participant_scheduler() diff --git a/implementations/python/packages/raes_runtime/participant_activity.py b/implementations/python/packages/raes_runtime/participant_activity.py new file mode 100644 index 000000000..c34c46ec4 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_activity.py @@ -0,0 +1,296 @@ +"""Deterministic governed draws and eligibility for participant activity v2.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from raes_contracts.contracts import ExperimentStochasticControlModel +from raes_contracts.contracts.random_stream import ( + ParticipantStreamAddressModel, + PublicSeedModel, +) +from raes_contracts.random_stream_engine import ( + decode_public_seed, + derive_stream_key, + draw_bounded_integer, +) +from raes_contracts.random_stream_profiles import load_random_stream_profile +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + +_PARTICIPANT_PROFILE = "blake3-xof-participant-v1" + + +@dataclass(frozen=True) +class ParticipantActivityRandomControl: + """Validated safe runtime view of one admitted agent-policy control.""" + + control_id: str + profile_id: str + namespace: str + stream_key: bytes + + +@dataclass(frozen=True) +class ParticipantActivityTimingSelection: + """One deterministic timing draw and its governed window disposition.""" + + tick: int | None + disposition: str + + +def resolve_participant_activity_controls( + controls: Iterable[ExperimentStochasticControlModel], +) -> dict[str, ParticipantActivityRandomControl]: + """Resolve public-seed participant controls exactly once and fail closed.""" + + resolved: dict[str, ParticipantActivityRandomControl] = {} + for control in controls: + if control.control_id in resolved: + raise ValueError(f"duplicate participant activity stochastic control {control.control_id!r}") + if control.role != "agent-policy": + continue + binding = control.executable_binding + if binding is None: + raise ValueError(f"agent-policy stochastic control {control.control_id!r} requires executable_binding") + profile_id = binding.profile_ref.ref_id + if profile_id != _PARTICIPANT_PROFILE: + raise ValueError( + f"agent-policy stochastic control {control.control_id!r} requires profile {_PARTICIPANT_PROFILE!r}" + ) + load_random_stream_profile(profile_id) + if not isinstance(binding.root_entropy, PublicSeedModel): + raise ValueError( + f"agent-policy stochastic control {control.control_id!r} uses governed entropy without a resolver" + ) + root_entropy = decode_public_seed(binding.root_entropy) + resolved[control.control_id] = ParticipantActivityRandomControl( + control_id=control.control_id, + profile_id=profile_id, + namespace=str(binding.namespace), + stream_key=derive_stream_key(profile_id=profile_id, root_entropy=root_entropy), + ) + return resolved + + +def activity_control_for( + policy: ParticipantAutonomousExecutionRuntime, + controls: dict[str, ParticipantActivityRandomControl], +) -> ParticipantActivityRandomControl | None: + """Return the exact control referenced by a v2 policy.""" + + return controls.get(policy.stochastic_control_ref) + + +def activity_draw_address( + *, + policy: ParticipantAutonomousExecutionRuntime, + participant_address: str, + time_segment: int, + occurrence_ordinal: int, + control: ParticipantActivityRandomControl, + local_coordinate: int, +) -> ParticipantStreamAddressModel: + return ParticipantStreamAddressModel( + namespace=control.namespace, + policy_address=policy.address, + participant_address=participant_address, + time_segment=time_segment, + occurrence_ordinal=occurrence_ordinal, + draw_purpose="agent-policy", + local_coordinate=local_coordinate, + ) + + +def draw_activity_integer( + *, + policy: ParticipantAutonomousExecutionRuntime, + participant_address: str, + time_segment: int, + occurrence_ordinal: int, + control: ParticipantActivityRandomControl, + local_coordinate: int, + minimum: int, + maximum: int, +) -> int: + """Draw one bounded value from a stable occurrence-local coordinate.""" + + draw = draw_bounded_integer( + profile_id=control.profile_id, + stream_key=control.stream_key, + address=activity_draw_address( + policy=policy, + participant_address=participant_address, + time_segment=time_segment, + occurrence_ordinal=occurrence_ordinal, + control=control, + local_coordinate=local_coordinate, + ), + minimum=minimum, + maximum=maximum, + max_rejection_attempts=32, + ) + if draw.rejection_exhausted or draw.value is None: + raise ValueError("participant activity bounded random draw exhausted") + return draw.value + + +def _window_ranges( + addresses: tuple[str, ...], + time_model: CompiledTimeModel, +) -> tuple[tuple[tuple[int, int], tuple[int, int]], ...]: + selected = { + constraint.address: constraint for constraint in time_model.constraints if constraint.address in addresses + } + ranges: list[tuple[tuple[int, int], tuple[int, int]]] = [] + for address in addresses: + constraint = selected[address] + if constraint.start_tick is None or constraint.end_tick is None: + raise ValueError("participant activity windows require finite start and end ticks") + ranges.append( + ( + (constraint.start_tick, constraint.start_microstep or 0), + (constraint.end_tick, constraint.end_microstep or 0), + ) + ) + return tuple(sorted(ranges)) + + +def activity_tick_is_eligible( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + tick: int, +) -> bool: + """Evaluate half-open work-union minus pause-union eligibility.""" + + work = _window_ranges(policy.work_window_addresses, time_model) + pauses = _window_ranges(policy.pause_window_addresses, time_model) + coordinate = (tick, 0) + return any(start <= coordinate < end for start, end in work) and not any( + start <= coordinate < end for start, end in pauses + ) + + +def next_activity_timing( + *, + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_address: str, + time_segment: int, + occurrence_ordinal: int, + current_tick: int, + control: ParticipantActivityRandomControl, +) -> ParticipantActivityTimingSelection: + """Draw and normalize the next due tick through a bounded window search.""" + + progression = next( + item for item in time_model.progression_policies if item.address == policy.progression_policy_address + ) + step_ticks = progression.step_ticks if progression.advancement_mode == "stepped" else None + minimum = policy.timing_minimum_ticks // step_ticks if step_ticks is not None else policy.timing_minimum_ticks + maximum = policy.timing_maximum_ticks // step_ticks if step_ticks is not None else policy.timing_maximum_ticks + interval_units = draw_activity_integer( + policy=policy, + participant_address=participant_address, + time_segment=time_segment, + occurrence_ordinal=occurrence_ordinal, + control=control, + local_coordinate=0, + minimum=minimum, + maximum=maximum, + ) + interval = interval_units * step_ticks if step_ticks is not None else interval_units + candidate = current_tick + interval + if activity_tick_is_eligible(policy, time_model, candidate): + return ParticipantActivityTimingSelection(tick=candidate, disposition="drawn") + if policy.outside_window_disposition == "skip": + return ParticipantActivityTimingSelection(tick=None, disposition="drawn") + work = _window_ranges(policy.work_window_addresses, time_model) + for start, end in work: + first_tick = start[0] + int(start[1] > 0) + normalized = max(candidate, first_tick) + if step_ticks is not None and normalized % step_ticks: + normalized += step_ticks - normalized % step_ticks + while (normalized, 0) < end: + if activity_tick_is_eligible(policy, time_model, normalized): + return ParticipantActivityTimingSelection(tick=normalized, disposition="next_opening") + pause_end = max( + ( + pause_end[0] + int(pause_end[1] > 0) + for pause_start, pause_end in _window_ranges(policy.pause_window_addresses, time_model) + if pause_start <= (normalized, 0) < pause_end + ), + default=normalized + 1, + ) + normalized = pause_end + if step_ticks is not None and normalized % step_ticks: + normalized += step_ticks - normalized % step_ticks + return ParticipantActivityTimingSelection(tick=None, disposition="next_opening") + + +def next_activity_tick( + *, + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_address: str, + time_segment: int, + occurrence_ordinal: int, + current_tick: int, + control: ParticipantActivityRandomControl, +) -> int | None: + """Return only the selected tick for callers that do not persist provenance.""" + + return next_activity_timing( + policy=policy, + time_model=time_model, + participant_address=participant_address, + time_segment=time_segment, + occurrence_ordinal=occurrence_ordinal, + current_tick=current_tick, + control=control, + ).tick + + +def select_activity_candidate( + *, + policy: ParticipantAutonomousExecutionRuntime, + participant_address: str, + time_segment: int, + occurrence_ordinal: int, + control: ParticipantActivityRandomControl, + eligible_indices: tuple[int, ...], +) -> int | None: + """Select from canonical candidates using exact positive integer weights.""" + + if not eligible_indices: + return None + total = sum(policy.action_candidate_weights[index] for index in eligible_indices) + selected = draw_activity_integer( + policy=policy, + participant_address=participant_address, + time_segment=time_segment, + occurrence_ordinal=occurrence_ordinal, + control=control, + local_coordinate=1, + minimum=0, + maximum=total - 1, + ) + cursor = 0 + for index in eligible_indices: + cursor += policy.action_candidate_weights[index] + if selected < cursor: + return index + raise ValueError("participant activity weighted selection did not resolve") + + +__all__ = [ + "ParticipantActivityRandomControl", + "activity_draw_address", + "activity_control_for", + "activity_tick_is_eligible", + "draw_activity_integer", + "next_activity_tick", + "next_activity_timing", + "resolve_participant_activity_controls", + "select_activity_candidate", +] diff --git a/implementations/python/packages/raes_runtime/participant_activity_support.py b/implementations/python/packages/raes_runtime/participant_activity_support.py new file mode 100644 index 000000000..fa83d97dc --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_activity_support.py @@ -0,0 +1,154 @@ +"""State and provenance helpers for participant activity execution.""" + +from typing import Protocol + +from raes_contracts.contracts import ( + ParticipantActivityOccurrenceProvenanceModel, + ParticipantAutonomousExecutionStateModel, + ParticipantBehaviorHistoryEventModel, +) +from raes_contracts.participant_binding import ParticipantActionAdmissionRequest +from raes_contracts.runtime_state import RuntimeSnapshot +from raes_processor.models import ParticipantAutonomousExecutionRuntime + +from .participant_activity import ParticipantActivityRandomControl, activity_draw_address + + +class ActivityRunState(Protocol): + working: RuntimeSnapshot + changed: list[str] + + +class ActivityDueContext(Protocol): + policy: ParticipantAutonomousExecutionRuntime + participant_address: str + current_tick: int + activity_control: ParticipantActivityRandomControl | None + + +def activity_attempt_id( + *, + policy_address: str, + participant_address: str, + episode_id: str, + time_segment: int, + occurrence_ordinal: int, + retry_ordinal: int, +) -> str: + """Return a reset-safe identity for one activity attempt.""" + + return ( + f"{policy_address}:{participant_address}:episode={episode_id}:" + f"segment={time_segment}:occurrence={occurrence_ordinal}:retry={retry_ordinal}" + ) + + +def persist_activity_state( + run: ActivityRunState, + key: str, + state: ParticipantAutonomousExecutionStateModel, +) -> None: + states = dict(run.working.participant_autonomous_execution_states) + states[key] = state.model_dump(mode="json") + run.working = run.working.with_entries( + dict(run.working.entries), + participant_autonomous_execution_states=states, + ) + run.changed.append(key) + + +def activity_eligible_indices( + policy: ParticipantAutonomousExecutionRuntime, + state: ParticipantAutonomousExecutionStateModel, + current_tick: int, +) -> tuple[int, ...]: + completed = set(state.completed_candidate_ids) + return tuple( + index + for index, candidate_id in enumerate(policy.action_candidate_ids) + if set(policy.action_candidate_dependencies[index]).issubset(completed) + and state.candidate_cooldown_until.get(candidate_id, 0) <= current_tick + ) + + +def _activity_provenance( + context: ActivityDueContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + terminal_outcome: str, +) -> ParticipantActivityOccurrenceProvenanceModel: + control = context.activity_control + if control is None: + raise ValueError("participant activity provenance requires a random control") + candidate_id = context.policy.action_candidate_ids[state.next_action_index] + occurrence_id = ( + f"{context.policy.address}:{context.participant_address}:" + f"segment={state.time_segment}:occurrence={state.occurrence_ordinal}" + ) + predecessor = None + if state.current_retry: + predecessor = activity_attempt_id( + policy_address=context.policy.address, + participant_address=context.participant_address, + episode_id=state.episode_id, + time_segment=state.time_segment, + occurrence_ordinal=state.occurrence_ordinal, + retry_ordinal=state.current_retry - 1, + ) + disposition = ( + "retry" if state.current_retry else ("burst" if state.burst_position else state.next_timing_disposition) + ) + return ParticipantActivityOccurrenceProvenanceModel( + policy_address=context.policy.address, + policy_profile="participant-autonomous-execution/v2", + occurrence_id=occurrence_id, + attempt_id=request.action_instance_id, + predecessor_attempt_id=predecessor, + candidate_id=candidate_id, + dependency_candidate_ids=list(context.policy.action_candidate_dependencies[state.next_action_index]), + timing_tick=context.current_tick, + timing_disposition=disposition, + burst_position=state.burst_position, + random_control_id=control.control_id, + random_profile_id=control.profile_id, + random_address=activity_draw_address( + policy=context.policy, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=state.occurrence_ordinal, + control=control, + local_coordinate=1, + ), + terminal_outcome=terminal_outcome, + ) + + +def annotate_activity_history( + run: ActivityRunState, + context: ActivityDueContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + terminal_outcome: str, +) -> None: + provenance = _activity_provenance(context, state, request, terminal_outcome) + histories = dict(run.working.participant_behavior_history) + events = list(histories.get(context.participant_address, ())) + for index, event in enumerate(events): + if event.get("action_instance_id") != request.action_instance_id: + continue + enriched = dict(event) + enriched["activity_provenance"] = provenance.model_dump(mode="json") + events[index] = ParticipantBehaviorHistoryEventModel.model_validate(enriched).model_dump(mode="json") + histories[context.participant_address] = events + run.working = run.working.with_entries( + dict(run.working.entries), + participant_behavior_history=histories, + ) + + +__all__ = [ + "activity_attempt_id", + "activity_eligible_indices", + "annotate_activity_history", + "persist_activity_state", +] diff --git a/implementations/python/packages/raes_runtime/participant_execution_control.py b/implementations/python/packages/raes_runtime/participant_execution_control.py index aa323114b..ea7566109 100644 --- a/implementations/python/packages/raes_runtime/participant_execution_control.py +++ b/implementations/python/packages/raes_runtime/participant_execution_control.py @@ -80,6 +80,7 @@ def _apply_participant_execution_phase( execution_plan.model.time_model, self._target.participant_runtime, state.working_snapshot, + self._participant_activity_controls, ) self._record_phase_result(state, result) if not result.success: @@ -97,6 +98,7 @@ def _start_participant_execution_phase( execution_plan.model.time_model, self._target.participant_runtime, state.working_snapshot, + self._participant_activity_controls, ) self._record_phase_result(state, due) return due.success @@ -110,6 +112,7 @@ def run_due_participant_actions(self) -> ApplyResult: self._participant_execution_time_model, self._target.participant_runtime, self._snapshot, + self._participant_activity_controls, ) self._snapshot = result.snapshot return result @@ -181,6 +184,7 @@ def _sync_participant_reset(self, clock_address: str) -> ApplyResult | None: self._snapshot, clock_address, reset_participants=False, + activity_controls=self._participant_activity_controls, ) self._snapshot = result.snapshot return result diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index 169da0c2a..43ad852a1 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -16,11 +16,17 @@ from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime +from .participant_activity import ( + ParticipantActivityRandomControl, + activity_control_for, + draw_activity_integer, + next_activity_timing, +) from .participant_scheduler_operations import ( SchedulerRunState, - clock_coordinate, run_participant_due, ) +from .participant_scheduler_time import clock_coordinate def _state_key(policy_address: str, participant_address: str) -> str: @@ -64,6 +70,27 @@ def _policy_digest( "resolved_progression_policy": asdict(progression), "resolved_temporal_constraints": constraints, } + if policy.profile == "participant-autonomous-execution/v2": + payload.update( + { + "profile": policy.profile, + "work_window_addresses": policy.work_window_addresses, + "pause_window_addresses": policy.pause_window_addresses, + "stochastic_control_ref": policy.stochastic_control_ref, + "timing_minimum_ticks": policy.timing_minimum_ticks, + "timing_maximum_ticks": policy.timing_maximum_ticks, + "outside_window_disposition": policy.outside_window_disposition, + "empty_eligible_disposition": policy.empty_eligible_disposition, + "action_candidate_ids": policy.action_candidate_ids, + "action_candidate_weights": policy.action_candidate_weights, + "action_candidate_dependencies": policy.action_candidate_dependencies, + "action_candidate_retry_failure_classes": (policy.action_candidate_retry_failure_classes), + "action_candidate_max_retries": policy.action_candidate_max_retries, + "action_candidate_cooldown_ticks": policy.action_candidate_cooldown_ticks, + "max_occurrences": policy.max_occurrences, + "max_burst_size": policy.max_burst_size, + } + ) canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest() @@ -98,6 +125,10 @@ def _state_identity(state: ParticipantAutonomousExecutionStateModel) -> tuple[ob state.participant_implementation_ref, state.clock_address, state.time_segment, + state.profile, + state.random_control_id, + state.random_profile_id, + state.random_namespace, ) @@ -107,6 +138,7 @@ def _initialize_participant( participant_runtime: object, snapshot: RuntimeSnapshot, participant_address: str, + activity_controls: dict[str, ParticipantActivityRandomControl], ) -> ApplyResult: working = snapshot changed: list[str] = [] @@ -123,8 +155,51 @@ def _initialize_participant( working = result.snapshot changed.extend(result.changed_addresses) key = _state_key(policy.address, participant_address) - first_tick, _ = _cadence(policy, time_model) segment, _ = clock_coordinate(working, policy.clock_address) + activity_control = activity_control_for(policy, activity_controls) + if policy.profile == "participant-autonomous-execution/v2" and activity_control is None: + return ApplyResult( + success=False, + snapshot=working, + diagnostics=[ + Diagnostic( + code="runtime.participant-activity-control-unbound", + domain="participant", + address=policy.address, + message=( + f"Participant activity policy requires admitted stochastic control " + f"{policy.stochastic_control_ref!r}." + ), + ) + ], + ) + if activity_control is None: + first_tick, _ = _cadence(policy, time_model) + burst_size = 1 + timing_disposition = "cadence" + else: + current_tick = _clock_tick(working, policy.clock_address) + burst_size = draw_activity_integer( + policy=policy, + participant_address=participant_address, + time_segment=segment, + occurrence_ordinal=0, + control=activity_control, + local_coordinate=2, + minimum=1, + maximum=policy.max_burst_size, + ) + timing = next_activity_timing( + policy=policy, + time_model=time_model, + participant_address=participant_address, + time_segment=segment, + occurrence_ordinal=0, + current_tick=current_tick, + control=activity_control, + ) + first_tick = timing.tick + timing_disposition = timing.disposition expected = ParticipantAutonomousExecutionStateModel( policy_address=policy.address, policy_digest=_policy_digest(policy, time_model), @@ -133,12 +208,18 @@ def _initialize_participant( participant_implementation_ref=policy.participant_implementation_ref, clock_address=policy.clock_address, time_segment=segment, - lifecycle_state="running", - next_tick=first_tick, + lifecycle_state="running" if first_tick is not None else "completed", + next_tick=first_tick if first_tick is not None else _clock_tick(working, policy.clock_address), next_action_index=0, attempted_actions=0, succeeded_actions=0, failed_actions=0, + profile=policy.profile, + random_control_id=activity_control.control_id if activity_control is not None else None, + random_profile_id=activity_control.profile_id if activity_control is not None else None, + random_namespace=activity_control.namespace if activity_control is not None else None, + burst_size=burst_size, + next_timing_disposition=timing_disposition, ) states = dict(working.participant_autonomous_execution_states) if key in states and _state_identity(ParticipantAutonomousExecutionStateModel.model_validate(states[key])) != ( @@ -175,8 +256,10 @@ def initialize( time_model: CompiledTimeModel, participant_runtime: object, snapshot: RuntimeSnapshot, + activity_controls: dict[str, ParticipantActivityRandomControl] | None = None, ) -> ApplyResult: working = snapshot + resolved_activity_controls = activity_controls or {} changed: list[str] = [] for policy in policies: for participant_address in policy.participant_addresses: @@ -186,6 +269,7 @@ def initialize( participant_runtime, working, participant_address, + resolved_activity_controls, ) if not result.success: return result @@ -203,10 +287,14 @@ def run_due( time_model: CompiledTimeModel, participant_runtime: object, snapshot: RuntimeSnapshot, + activity_controls: dict[str, ParticipantActivityRandomControl] | None = None, ) -> ApplyResult: run = SchedulerRunState(working=snapshot, diagnostics=[], changed=[]) + resolved_activity_controls = activity_controls or {} for policy in policies: - _, cadence_ticks = _cadence(policy, time_model) + cadence_ticks = ( + _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 + ) current_tick = _clock_tick(run.working, policy.clock_address) for participant_address in policy.participant_addresses: run_participant_due( @@ -217,6 +305,7 @@ def run_due( current_tick, cadence_ticks, run, + resolved_activity_controls, ) if run.failure is not None: return run.result() @@ -231,20 +320,25 @@ def reset_clock( clock_address: str, *, reset_participants: bool = True, + activity_controls: dict[str, ParticipantActivityRandomControl] | None = None, ) -> ApplyResult: """Reset bound episodes and scheduler counters at a shared-clock segment boundary.""" working = snapshot + resolved_activity_controls = activity_controls or {} changed: list[str] = [] segment, _ = clock_coordinate(snapshot, clock_address) for policy in policies: if policy.clock_address != clock_address: continue - first_tick, cadence_ticks = _cadence(policy, time_model) current_tick = _clock_tick(snapshot, clock_address) - next_tick = first_tick - if next_tick < current_tick: - next_tick += ((current_tick - next_tick + cadence_ticks - 1) // cadence_ticks) * cadence_ticks + activity_control = activity_control_for(policy, resolved_activity_controls) + if activity_control is None: + first_tick, cadence_ticks = _cadence(policy, time_model) + next_tick = first_tick + timing_disposition = "cadence" + if next_tick < current_tick: + next_tick += ((current_tick - next_tick + cadence_ticks - 1) // cadence_ticks) * cadence_ticks for participant_address in policy.participant_addresses: result_changed: list[str] = [] if reset_participants: @@ -264,6 +358,29 @@ def reset_clock( state = ParticipantAutonomousExecutionStateModel.model_validate( working.participant_autonomous_execution_states[key] ) + if activity_control is not None: + burst_size = draw_activity_integer( + policy=policy, + participant_address=participant_address, + time_segment=segment, + occurrence_ordinal=0, + control=activity_control, + local_coordinate=2, + minimum=1, + maximum=policy.max_burst_size, + ) + timing = next_activity_timing( + policy=policy, + time_model=time_model, + participant_address=participant_address, + time_segment=segment, + occurrence_ordinal=0, + current_tick=current_tick, + control=activity_control, + ) + selected_tick = timing.tick + timing_disposition = timing.disposition + next_tick = selected_tick if selected_tick is not None else current_tick states = dict(working.participant_autonomous_execution_states) states[key] = state.model_copy( update={ @@ -277,6 +394,14 @@ def reset_clock( "failed_actions": 0, "in_flight": 0, "last_action_instance_id": None, + "occurrence_ordinal": 0, + "current_retry": 0, + "burst_position": 0, + "last_candidate_id": None, + "completed_candidate_ids": [], + "candidate_cooldown_until": {}, + "burst_size": burst_size if activity_control is not None else 1, + "next_timing_disposition": timing_disposition, } ).model_dump(mode="json") working = working.with_entries( diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py index 9e7d03d9f..b0dd14580 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py @@ -15,29 +15,20 @@ from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime from .participant_action_validation import autonomous_action_result_violation - - -def clock_coordinate(snapshot: RuntimeSnapshot, clock_address: str) -> tuple[int, int]: - """Read one shared clock coordinate or fail closed.""" - - if snapshot.time_model_state is None: - raise ValueError("autonomous participant execution requires typed shared-time state") - clock = snapshot.time_model_state.clocks.get(clock_address) - if clock is None: - raise ValueError(f"autonomous participant clock {clock_address!r} has no runtime state") - return clock.coordinate.segment, clock.coordinate.tick - - -def _time_domain(policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel) -> str: - clock = next(item for item in time_model.clocks if item.address == policy.clock_address) - domain = next(item for item in time_model.domains if item.address == clock.time_domain_address) - return { - "wall_clock": "wall_clock_time", - "simulated": "simulation_time", - "logical": "scenario_time", - "monotonic": "scenario_time", - "external": "backend_time", - }[domain.kind] +from .participant_activity import ( + ParticipantActivityRandomControl, + activity_control_for, + draw_activity_integer, + next_activity_timing, + select_activity_candidate, +) +from .participant_activity_support import ( + activity_attempt_id, + activity_eligible_indices, + annotate_activity_history, + persist_activity_state, +) +from .participant_scheduler_time import clock_coordinate, participant_time_domain @dataclass @@ -67,6 +58,7 @@ class _DueActionContext: key: str current_tick: int cadence_ticks: int + activity_control: ParticipantActivityRandomControl | None = None def _cadence_missed_result( @@ -99,12 +91,22 @@ def _bound_action_request( ) -> ParticipantActionAdmissionRequest: policy = context.policy action_address = policy.action_contract_addresses[state.next_action_index % len(policy.action_contract_addresses)] - action_instance_id = f"{policy.address}:{context.participant_address}:{state.attempted_actions}" + if policy.profile == "participant-autonomous-execution/v2": + action_instance_id = activity_attempt_id( + policy_address=policy.address, + participant_address=context.participant_address, + episode_id=state.episode_id, + time_segment=state.time_segment, + occurrence_ordinal=state.occurrence_ordinal, + retry_ordinal=state.current_retry, + ) + else: + action_instance_id = f"{policy.address}:{context.participant_address}:{state.attempted_actions}" segment, _ = clock_coordinate(working, policy.clock_address) temporal_contexts = tuple( ParticipantTemporalRuntimeContextModel( temporal_contract_id=constraint_address, - time_domain=_time_domain(policy, context.time_model), + time_domain=participant_time_domain(policy, context.time_model), clock_authority=policy.clock_address, event_points=["submit", "start", "end", "observed"], observation_point=f"{policy.clock_address}@segment={segment},tick={context.current_tick}", @@ -239,6 +241,216 @@ def _run_one_due_action( return next_state +def _next_activity_occurrence_state( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + *, + action_succeeded: bool, + failure_class: str | None, + protocol_failure: bool, +) -> ParticipantAutonomousExecutionStateModel: + policy = context.policy + control = context.activity_control + if control is None: + raise ValueError("participant activity execution requires a random control") + index = state.next_action_index + candidate_id = policy.action_candidate_ids[index] + attempted = state.attempted_actions + 1 + failed = state.failed_actions + (0 if action_succeeded else 1) + retryable = ( + not protocol_failure + and not action_succeeded + and failure_class in policy.action_candidate_retry_failure_classes[index] + and state.current_retry < policy.action_candidate_max_retries[index] + and attempted < policy.max_action_attempts + ) + if retryable: + return state.model_copy( + update={ + "next_tick": context.current_tick, + "attempted_actions": attempted, + "failed_actions": failed, + "current_retry": state.current_retry + 1, + "last_candidate_id": candidate_id, + "last_action_instance_id": request.action_instance_id, + } + ) + + completed = list(state.completed_candidate_ids) + if action_succeeded and candidate_id not in completed: + completed.append(candidate_id) + cooldowns = dict(state.candidate_cooldown_until) + cooldowns[candidate_id] = context.current_tick + policy.action_candidate_cooldown_ticks[index] + occurrence = state.occurrence_ordinal + 1 + lifecycle = state.lifecycle_state + if protocol_failure or (not action_succeeded and policy.failure_policy == "stop"): + lifecycle = "failed" + elif occurrence >= policy.max_occurrences or attempted >= policy.max_action_attempts: + lifecycle = "completed" + + burst_position = state.burst_position + burst_size = state.burst_size + next_tick = context.current_tick + if lifecycle == "running": + if burst_position + 1 < burst_size: + burst_position += 1 + else: + burst_position = 0 + burst_size = draw_activity_integer( + policy=policy, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=occurrence, + control=control, + local_coordinate=2, + minimum=1, + maximum=policy.max_burst_size, + ) + timing = next_activity_timing( + policy=policy, + time_model=context.time_model, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=occurrence, + current_tick=context.current_tick, + control=control, + ) + selected_tick = timing.tick + if selected_tick is None: + lifecycle = "completed" + else: + next_tick = selected_tick + return state.model_copy( + update={ + "lifecycle_state": lifecycle, + "next_tick": next_tick, + "attempted_actions": attempted, + "succeeded_actions": state.succeeded_actions + (1 if action_succeeded else 0), + "failed_actions": failed, + "occurrence_ordinal": occurrence, + "current_retry": 0, + "burst_position": burst_position, + "burst_size": burst_size, + "last_candidate_id": candidate_id, + "completed_candidate_ids": completed, + "candidate_cooldown_until": cooldowns, + "last_action_instance_id": request.action_instance_id, + "next_timing_disposition": ( + timing.disposition if lifecycle == "running" and burst_position == 0 else state.next_timing_disposition + ), + } + ) + + +def _run_one_activity_action( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + run: SchedulerRunState, +) -> ParticipantAutonomousExecutionStateModel: + request = _bound_action_request(context, run.working, state) + predecessor = run.working + result = context.participant_runtime.admit_action(request, predecessor) + protocol_violation = autonomous_action_result_violation( + request, + result, + episode_id=state.episode_id, + predecessor=predecessor, + ) + protocol_failure = _record_protocol_result(run, context, predecessor, result, protocol_violation) + action_result = getattr(result, "action_result", None) + status = getattr(getattr(action_result, "status", None), "value", getattr(action_result, "status", None)) + action_succeeded = bool(not protocol_failure and result.success and status == "succeeded") + failure_value = getattr( + getattr(action_result, "failure_class", None), + "value", + getattr(action_result, "failure_class", None), + ) + if not protocol_failure: + annotate_activity_history( + run, + context, + state, + request, + str(status or "unknown"), + ) + next_state = _next_activity_occurrence_state( + context, + state, + request, + action_succeeded=action_succeeded, + failure_class=str(failure_value) if failure_value is not None else None, + protocol_failure=protocol_failure, + ) + persist_activity_state(run, context.key, next_state) + if not protocol_failure: + run.changed.extend(result.changed_addresses) + if next_state.lifecycle_state == "failed": + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + return next_state + + +def _run_participant_activity_due( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + run: SchedulerRunState, +) -> None: + while ( + state.lifecycle_state == "running" + and state.next_tick == context.current_tick + and state.attempted_actions < context.policy.max_action_attempts + and run.failure is None + ): + eligible = activity_eligible_indices(context.policy, state, context.current_tick) + control = context.activity_control + if control is None: + raise ValueError("participant activity execution requires a random control") + selected = ( + state.next_action_index + if state.current_retry + else select_activity_candidate( + policy=context.policy, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=state.occurrence_ordinal, + control=control, + eligible_indices=eligible, + ) + ) + if selected is None: + lifecycle = "completed" if context.policy.empty_eligible_disposition == "complete" else "running" + selected_tick = ( + None + if lifecycle == "completed" + else next_activity_timing( + policy=context.policy, + time_model=context.time_model, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=state.occurrence_ordinal, + current_tick=context.current_tick, + control=control, + ).tick + ) + if selected_tick is None: + lifecycle = "completed" + state = state.model_copy( + update={ + "lifecycle_state": lifecycle, + "next_tick": selected_tick if selected_tick is not None else context.current_tick, + } + ) + persist_activity_state(run, context.key, state) + return + state = state.model_copy(update={"next_action_index": selected}) + state = _run_one_activity_action(context, state, run) + + def run_participant_due( policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel, @@ -247,6 +459,7 @@ def run_participant_due( current_tick: int, cadence_ticks: int, run: SchedulerRunState, + activity_controls: dict[str, ParticipantActivityRandomControl] | None = None, ) -> None: """Run one participant at the current governed cadence boundary.""" @@ -265,7 +478,11 @@ def run_participant_due( key=key, current_tick=current_tick, cadence_ticks=cadence_ticks, + activity_control=activity_control_for(policy, activity_controls or {}), ) + if policy.profile == "participant-autonomous-execution/v2": + _run_participant_activity_due(action_context, state, run) + return action_is_due = ( state.lifecycle_state == "running" and state.next_tick == current_tick @@ -280,4 +497,4 @@ def run_participant_due( ) -__all__ = ["SchedulerRunState", "clock_coordinate", "run_participant_due"] +__all__ = ["SchedulerRunState", "run_participant_due"] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_time.py b/implementations/python/packages/raes_runtime/participant_scheduler_time.py new file mode 100644 index 000000000..ce861d412 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_time.py @@ -0,0 +1,33 @@ +"""Shared-time helpers for autonomous participant scheduling.""" + +from raes_contracts.runtime_state import RuntimeSnapshot +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + + +def clock_coordinate(snapshot: RuntimeSnapshot, clock_address: str) -> tuple[int, int]: + """Read one shared clock coordinate or fail closed.""" + + if snapshot.time_model_state is None: + raise ValueError("autonomous participant execution requires typed shared-time state") + clock = snapshot.time_model_state.clocks.get(clock_address) + if clock is None: + raise ValueError(f"autonomous participant clock {clock_address!r} has no runtime state") + return clock.coordinate.segment, clock.coordinate.tick + + +def participant_time_domain( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, +) -> str: + clock = next(item for item in time_model.clocks if item.address == policy.clock_address) + domain = next(item for item in time_model.domains if item.address == clock.time_domain_address) + return { + "wall_clock": "wall_clock_time", + "simulated": "simulation_time", + "logical": "scenario_time", + "monotonic": "scenario_time", + "external": "backend_time", + }[domain.kind] + + +__all__ = ["clock_coordinate", "participant_time_domain"] diff --git a/implementations/python/tests/test_dsl_437_benign_participant_execution.py b/implementations/python/tests/test_dsl_437_benign_participant_execution.py index 096d1010e..35cb407d4 100644 --- a/implementations/python/tests/test_dsl_437_benign_participant_execution.py +++ b/implementations/python/tests/test_dsl_437_benign_participant_execution.py @@ -12,17 +12,26 @@ from raes._errors import SDLValidationError from raes.parser import parse_sdl from raes.participant_behavior import ParticipantFailureClass +from raes.participant_execution import ParticipantAutonomousExecutionPolicyV2 from raes_backend_protocols.capability_admission import participant_autonomous_execution_capability_gaps +from raes_backend_protocols.manifest import backend_manifest_from_v2_model, backend_manifest_v2_model from raes_backend_protocols.participant_runtime_base import BaseParticipantRuntime from raes_backend_stubs.manifest import create_stub_manifest from raes_backend_stubs.stubs import create_stub_target from raes_contracts.contracts import ( + ExperimentStochasticControlModel, ParticipantActionResultModel, ParticipantAutonomousExecutionStateModel, ParticipantImplementationManifestModel, ParticipantImplementationSelectionModel, RuntimeSnapshotEnvelopeModel, ) +from raes_contracts.contracts.random_stream import ( + GovernedEntropyRefModel, + PublicSeedModel, + RandomStreamControlBindingModel, + RandomStreamProfileReferenceModel, +) from raes_contracts.contracts.time_model import TimeRuntimeStateModel from raes_contracts.participant_binding import ( ParticipantActionAdmissionRequest, @@ -38,6 +47,12 @@ from raes_processor.compiler.time_model import time_model_contract_model from raes_processor.planner import plan from raes_runtime.manager import RuntimeManager +from raes_runtime.participant_activity import ( + activity_tick_is_eligible, + next_activity_timing, + resolve_participant_activity_controls, + select_activity_candidate, +) from raes_runtime.participant_clock_driver import ParticipantClockDriver from raes_runtime.participant_scheduler import ParticipantScheduler from raes_runtime.time_coordinator import ReferenceTimeRuntime, TimeCoordinator @@ -122,6 +137,61 @@ def _scenario_yaml(*, role: str = "green") -> str: return yaml.safe_dump(payload, sort_keys=False) +def _activity_policy_yaml() -> str: + payload = yaml.safe_load(_scenario_yaml()) + payload["temporal_constraints"].update( + { + "work-window": { + "constraint_kind": "window", + "clock_ref": "scenario-clock", + "subject_refs": ["participant-agent"], + "start": {"tick": 0}, + "end": {"tick": 100}, + "description": "Participant work availability.", + }, + "pause-window": { + "constraint_kind": "window", + "clock_ref": "scenario-clock", + "subject_refs": ["participant-agent"], + "start": {"tick": 40}, + "end": {"tick": 50}, + "description": "Participant pause interval.", + }, + } + ) + payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] = { + "profile": "participant-autonomous-execution/v2", + "participant_implementation_ref": IMPLEMENTATION_REF, + "clock_ref": "scenario-clock", + "progression_policy_ref": "scenario-progression", + "work_window_refs": ["work-window"], + "pause_window_refs": ["pause-window"], + "observation_boundary_ref": "participant-view", + "stochastic_control_ref": "green-activity-policy", + "selection_strategy": "weighted", + "timing": {"minimum_ticks": 10, "maximum_ticks": 30}, + "outside_window_disposition": "next_opening", + "empty_eligible_disposition": "complete", + "action_candidates": { + "portal_login": { + "action_ref": "probe-customer-portal-login", + "weight": 3, + "depends_on": [], + "retryable_failure_classes": ["target_unavailable", "timeout"], + "max_retries": 2, + "cooldown_ticks": 20, + } + }, + "max_occurrences": 8, + "max_action_attempts": 24, + "max_burst_size": 2, + "max_in_flight": 1, + "failure_policy": "continue", + "evaluation_authority": {"mode": "none"}, + } + return yaml.safe_dump(payload, sort_keys=False) + + def _implementation_manifest() -> ParticipantImplementationManifestModel: return ParticipantImplementationManifestModel.model_validate( { @@ -169,6 +239,26 @@ def _implementation_manifest() -> ParticipantImplementationManifestModel: ) +def _activity_control() -> ExperimentStochasticControlModel: + return ExperimentStochasticControlModel( + control_id="green-activity-policy", + role="agent-policy", + executable_binding=RandomStreamControlBindingModel( + profile_ref=RandomStreamProfileReferenceModel( + ref_id="blake3-xof-participant-v1", + ref_kind="profile", + ref_version="1", + ), + namespace="green-activity", + root_entropy=PublicSeedModel( + kind="public-seed", + encoding="hex-fixed-width", + value="42" * 32, + ), + ), + ) + + def _selection(participant_address: str) -> ParticipantImplementationSelectionModel: return ParticipantImplementationSelectionModel.model_validate( { @@ -473,7 +563,7 @@ def _autonomous_manifest(runtime_model: object) -> object: base_manifest.participant_runtime.supported_behavior_features | {"autonomous_execution"} ), supports_autonomous_execution=True, - supported_autonomous_selection_strategies=frozenset({"ordered_cycle"}), + supported_autonomous_selection_strategies=frozenset(policy.selection_strategy for policy in policies), supported_autonomous_action_contracts=frozenset( address for policy in policies for address in policy.action_contract_addresses ), @@ -483,9 +573,39 @@ def _autonomous_manifest(runtime_model: object) -> object: supported_autonomous_target_addresses=frozenset( address for policy in policies for address in policy.target_addresses ), + supported_autonomous_policy_profiles=frozenset(policy.profile for policy in policies), + supported_autonomous_activity_features=frozenset( + { + "work-windows", + "timing-variation", + "weighted-selection", + "dependencies", + "bounded-retries", + "cooldowns", + "limited-bursts", + "occurrence-provenance", + } + if any(policy.profile == "participant-autonomous-execution/v2" for policy in policies) + else () + ), + supported_autonomous_random_stream_profiles=frozenset( + {"blake3-xof-participant-v1"} + if any(policy.profile == "participant-autonomous-execution/v2" for policy in policies) + else () + ), max_autonomous_participants=8, - max_autonomous_action_attempts=8, + max_autonomous_action_attempts=max(policy.max_action_attempts for policy in policies), max_autonomous_in_flight=1, + max_autonomous_occurrences=max( + (policy.max_occurrences or policy.max_action_attempts for policy in policies), + default=1, + ), + max_autonomous_retries_per_occurrence=max( + (max(policy.action_candidate_max_retries, default=0) for policy in policies), + default=0, + ) + or 1, + max_autonomous_burst_size=max((policy.max_burst_size for policy in policies), default=1), ), ), ) @@ -506,6 +626,86 @@ def test_autonomous_execution_compiles_existing_participant_and_shared_time_refs assert action.shared_state_refs == ("nodes.customer-portal.services.http",) +def test_activity_policy_v2_compiles_weighted_candidates_and_shared_time_windows() -> None: + runtime_model = compile_runtime_model(parse_sdl(_activity_policy_yaml())) + policy = runtime_model.behavior_specifications[ + "participant.behavior-specification.participant-behavior" + ].autonomous_execution + + assert policy is not None + assert policy.profile == "participant-autonomous-execution/v2" + assert policy.selection_strategy == "weighted" + assert policy.work_window_addresses == ("time.constraint.work-window",) + assert policy.pause_window_addresses == ("time.constraint.pause-window",) + assert policy.stochastic_control_ref == "green-activity-policy" + assert policy.timing_minimum_ticks == 10 + assert policy.timing_maximum_ticks == 30 + assert policy.action_candidate_ids == ("portal_login",) + assert policy.action_candidate_weights == (3,) + assert policy.action_contract_addresses == ("participant.action-contract.probe-customer-portal-login",) + assert policy.max_occurrences == 8 + assert policy.max_burst_size == 2 + + +def test_activity_policy_v2_compiles_section_qualified_window_refs_to_canonical_addresses() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + policy["work_window_refs"] = ["temporal_constraints.work-window"] + policy["pause_window_refs"] = ["temporal_constraints.pause-window"] + + runtime_model = compile_runtime_model(parse_sdl(yaml.safe_dump(payload, sort_keys=False))) + compiled = runtime_model.behavior_specifications[ + "participant.behavior-specification.participant-behavior" + ].autonomous_execution + + assert compiled is not None + assert compiled.work_window_addresses == ("time.constraint.work-window",) + assert compiled.pause_window_addresses == ("time.constraint.pause-window",) + assert set(compiled.temporal_constraint_addresses) == { + "time.constraint.work-window", + "time.constraint.pause-window", + } + + +def test_activity_policy_v2_rejects_dependency_cycles() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + policy["action_candidates"]["portal_login"]["depends_on"] = ["second_action"] + policy["action_candidates"]["second_action"] = { + **policy["action_candidates"]["portal_login"], + "depends_on": ["portal_login"], + } + + with pytest.raises(ValueError, match="dependency graph must be acyclic"): + ParticipantAutonomousExecutionPolicyV2.model_validate(policy) + + +def test_activity_policy_v2_rejects_non_window_availability_constraint() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["work_window_refs"] = [ + "green-cadence" + ] + + with pytest.raises(SDLValidationError, match="work and pause refs must resolve to window constraints"): + parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + + +def test_activity_policy_v2_rejects_timing_bounds_unreachable_by_stepped_progression() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["timing"]["minimum_ticks"] = 15 + + with pytest.raises(SDLValidationError, match="activity timing bounds are unreachable by stepped progression"): + parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + + +def test_activity_policy_v2_rejects_window_for_unrelated_subject() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + payload["temporal_constraints"]["work-window"]["subject_refs"] = ["nodes.customer-portal"] + + with pytest.raises(SDLValidationError, match="must name the behavior specification or every governed participant"): + parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + + def test_non_evaluated_autonomous_participant_must_be_green() -> None: scenario_yaml = _scenario_yaml(role="red") @@ -594,6 +794,52 @@ def test_backend_admission_enforces_finite_autonomous_execution_limits() -> None assert "action attempts" in gaps[0] +def test_backend_admission_requires_exact_v2_activity_and_random_profile_support() -> None: + runtime_model = compile_runtime_model(parse_sdl(_activity_policy_yaml())) + policies = tuple( + specification.autonomous_execution + for specification in runtime_model.behavior_specifications.values() + if specification.autonomous_execution is not None + ) + manifest = _autonomous_manifest(runtime_model) + + assert participant_autonomous_execution_capability_gaps(manifest, policies, runtime_model.time_model) == () + restored = backend_manifest_from_v2_model(backend_manifest_v2_model(manifest)) + assert participant_autonomous_execution_capability_gaps(restored, policies, runtime_model.time_model) == () + assert manifest.participant_runtime is not None + unsupported = replace( + manifest, + capabilities=replace( + manifest.capabilities, + participant_runtime=replace( + manifest.participant_runtime, + supported_autonomous_random_stream_profiles=frozenset({"blake3-xof-v1"}), + ), + ), + ) + + gaps = participant_autonomous_execution_capability_gaps(unsupported, policies, runtime_model.time_model) + assert "unsupported autonomous random-stream profiles: blake3-xof-participant-v1" in gaps + missing_dependency_support = replace( + manifest, + capabilities=replace( + manifest.capabilities, + participant_runtime=replace( + manifest.participant_runtime, + supported_autonomous_activity_features=( + manifest.participant_runtime.supported_autonomous_activity_features - {"dependencies"} + ), + ), + ), + ) + gaps = participant_autonomous_execution_capability_gaps( + missing_dependency_support, + policies, + runtime_model.time_model, + ) + assert "unsupported autonomous activity features: dependencies" in gaps + + def test_planner_enforces_required_participant_features_and_exact_targets() -> None: runtime_model, _ = _compiled() manifest = _autonomous_manifest(runtime_model) @@ -667,6 +913,446 @@ def test_runtime_manager_drives_due_actions_from_shared_clock_controls() -> None assert len(participant_runtime.native_actions) == 3 +def test_runtime_manager_executes_v2_activity_from_admitted_random_control() -> None: + scenario = parse_sdl(_activity_policy_yaml()) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _NativeParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + + applied = manager.apply(manager.plan(scenario)) + + assert applied.success + assert participant_runtime.native_actions == [] + state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + assert 10 <= state.next_tick <= 30 + due = manager.advance_time("time.clock.scenario-clock", ticks=state.next_tick) + assert due.success + assert len(participant_runtime.native_actions) == 1 + advanced = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(due.snapshot.participant_autonomous_execution_states.values())) + ) + assert advanced.occurrence_ordinal == 1 + assert advanced.last_candidate_id == "portal_login" + assert advanced.random_control_id == "green-activity-policy" + assert advanced.random_profile_id == "blake3-xof-participant-v1" + history = due.snapshot.participant_behavior_history[advanced.participant_address] + assert len(history) == 3 + provenances = [event["activity_provenance"] for event in history] + assert all(provenance is not None for provenance in provenances) + assert {provenance["occurrence_id"] for provenance in provenances} == { + ( + "participant.autonomous-execution.participant-behavior:" + "participant.behavior.participant-agent:segment=0:occurrence=0" + ) + } + assert all(provenance["candidate_id"] == "portal_login" for provenance in provenances) + assert all( + provenance["random_address"]["time_segment"] == 0 and provenance["random_address"]["occurrence_ordinal"] == 0 + for provenance in provenances + ) + + +def test_runtime_manager_bounds_v2_retries_and_preserves_occurrence_causality() -> None: + scenario = parse_sdl(_activity_policy_yaml()) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _FailedParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + applied = manager.apply(manager.plan(scenario)) + initial = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + + due = manager.advance_time("time.clock.scenario-clock", ticks=initial.next_tick) + + assert due.success + assert participant_runtime.native_actions == [ + ( + "participant.autonomous-execution.participant-behavior:" + "participant.behavior.participant-agent:" + f"episode={initial.episode_id}:segment=0:occurrence=0:retry=0" + ), + ( + "participant.autonomous-execution.participant-behavior:" + "participant.behavior.participant-agent:" + f"episode={initial.episode_id}:segment=0:occurrence=0:retry=1" + ), + ( + "participant.autonomous-execution.participant-behavior:" + "participant.behavior.participant-agent:" + f"episode={initial.episode_id}:segment=0:occurrence=0:retry=2" + ), + ] + state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(due.snapshot.participant_autonomous_execution_states.values())) + ) + assert (state.occurrence_ordinal, state.current_retry) == (1, 0) + assert (state.attempted_actions, state.failed_actions) == (3, 3) + history = due.snapshot.participant_behavior_history[state.participant_address] + attempted = [event for event in history if event["event_type"] == "action_attempted"] + assert [event["activity_provenance"]["timing_disposition"] for event in attempted] == [ + initial.next_timing_disposition, + "retry", + "retry", + ] + assert attempted[1]["activity_provenance"]["predecessor_attempt_id"] == participant_runtime.native_actions[0] + assert attempted[2]["activity_provenance"]["predecessor_attempt_id"] == participant_runtime.native_actions[1] + + +def test_runtime_manager_global_attempt_bound_stops_v2_retry_chain() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + policy.update(max_occurrences=2, max_action_attempts=2, max_burst_size=1) + scenario = parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _FailedParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + applied = manager.apply(manager.plan(scenario)) + initial = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + + due = manager.advance_time("time.clock.scenario-clock", ticks=initial.next_tick) + + assert due.success + assert len(participant_runtime.native_actions) == 2 + state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(due.snapshot.participant_autonomous_execution_states.values())) + ) + assert state.lifecycle_state == "completed" + assert (state.occurrence_ordinal, state.attempted_actions, state.failed_actions) == (1, 2, 2) + + +def test_runtime_manager_normalizes_v2_timing_to_half_open_work_availability() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + payload["temporal_constraints"]["work-window"]["start"] = {"tick": 20, "microstep": 1} + timing = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["timing"] + timing.update(minimum_ticks=10, maximum_ticks=10) + scenario = parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + runtime_model = compile_runtime_model(scenario) + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=_NativeParticipantRuntime(), + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + + applied = manager.apply(manager.plan(scenario)) + + assert applied.success + state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + policy = next( + specification.autonomous_execution + for specification in runtime_model.behavior_specifications.values() + if specification.autonomous_execution is not None + ) + assert state.next_tick == 30 + assert state.next_timing_disposition == "next_opening" + assert not activity_tick_is_eligible(policy, runtime_model.time_model, 20) + assert not activity_tick_is_eligible(policy, runtime_model.time_model, 40) + assert activity_tick_is_eligible(policy, runtime_model.time_model, 50) + assert not activity_tick_is_eligible(policy, runtime_model.time_model, 100) + + +def test_v2_weighted_selection_honors_each_cumulative_weight_boundary( + monkeypatch: pytest.MonkeyPatch, +) -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + authored_policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + authored_policy["action_candidates"]["follow_up"] = { + **authored_policy["action_candidates"]["portal_login"], + "weight": 2, + } + runtime_model = compile_runtime_model(parse_sdl(yaml.safe_dump(payload, sort_keys=False))) + policy = runtime_model.behavior_specifications[ + "participant.behavior-specification.participant-behavior" + ].autonomous_execution + assert policy is not None + control = resolve_participant_activity_controls([_activity_control()])["green-activity-policy"] + first_weight = policy.action_candidate_weights[0] + total_weight = sum(policy.action_candidate_weights) + + for drawn_value, expected_index in ( + (0, 0), + (first_weight - 1, 0), + (first_weight, 1), + (total_weight - 1, 1), + ): + monkeypatch.setattr( + "raes_runtime.participant_activity.draw_activity_integer", + lambda drawn_value=drawn_value, **_kwargs: drawn_value, + ) + selected = select_activity_candidate( + policy=policy, + participant_address=policy.participant_addresses[0], + time_segment=0, + occurrence_ordinal=0, + control=control, + eligible_indices=(0, 1), + ) + assert selected == expected_index + + +def test_v2_timing_search_advances_across_disjoint_work_windows() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + payload["temporal_constraints"]["work-window"]["end"] = {"tick": 20} + payload["temporal_constraints"]["second-work-window"] = { + **payload["temporal_constraints"]["work-window"], + "start": {"tick": 50}, + "end": {"tick": 100}, + } + authored_policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + authored_policy["work_window_refs"] = ["work-window", "second-work-window"] + authored_policy["pause_window_refs"] = [] + authored_policy["timing"] = {"minimum_ticks": 30, "maximum_ticks": 30} + runtime_model = compile_runtime_model(parse_sdl(yaml.safe_dump(payload, sort_keys=False))) + policy = runtime_model.behavior_specifications[ + "participant.behavior-specification.participant-behavior" + ].autonomous_execution + assert policy is not None + control = resolve_participant_activity_controls([_activity_control()])["green-activity-policy"] + + timing = next_activity_timing( + policy=policy, + time_model=runtime_model.time_model, + participant_address=policy.participant_addresses[0], + time_segment=0, + occurrence_ordinal=0, + current_tick=0, + control=control, + ) + + assert timing.tick == 50 + assert timing.disposition == "next_opening" + + +def test_runtime_manager_waits_when_every_v2_candidate_is_cooling_down() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + authored_policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + authored_policy["empty_eligible_disposition"] = "wait" + authored_policy["timing"] = {"minimum_ticks": 10, "maximum_ticks": 10} + authored_policy["max_occurrences"] = 2 + authored_policy["max_burst_size"] = 2 + scenario = parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _NativeParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + applied = manager.apply(manager.plan(scenario)) + initial = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + assert initial.burst_size == 2 + + due = manager.advance_time("time.clock.scenario-clock", ticks=initial.next_tick) + + assert due.success + assert len(participant_runtime.native_actions) == 1 + waiting = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(due.snapshot.participant_autonomous_execution_states.values())) + ) + assert waiting.lifecycle_state == "running" + assert waiting.occurrence_ordinal == 1 + assert waiting.attempted_actions == 1 + assert waiting.next_tick == initial.next_tick + 10 + + +def test_runtime_manager_skips_v2_timing_outside_work_windows() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + payload["temporal_constraints"]["work-window"]["end"] = {"tick": 20} + payload["temporal_constraints"]["second-work-window"] = { + **payload["temporal_constraints"]["work-window"], + "start": {"tick": 50}, + "end": {"tick": 100}, + } + authored_policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + authored_policy["work_window_refs"] = ["work-window", "second-work-window"] + authored_policy["pause_window_refs"] = [] + authored_policy["timing"] = {"minimum_ticks": 30, "maximum_ticks": 30} + authored_policy["outside_window_disposition"] = "skip" + scenario = parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _NativeParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + + applied = manager.apply(manager.plan(scenario)) + + assert applied.success + assert participant_runtime.native_actions == [] + state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + assert state.lifecycle_state == "completed" + assert state.next_tick == 0 + assert state.next_timing_disposition == "drawn" + + +def test_runtime_manager_enforces_v2_dependencies_cooldowns_and_limited_burst() -> None: + payload = yaml.safe_load(_activity_policy_yaml()) + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + policy["action_candidates"]["follow_up"] = { + **policy["action_candidates"]["portal_login"], + "weight": 1, + "depends_on": ["portal_login"], + } + scenario = parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _NativeParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + applied = manager.apply(manager.plan(scenario)) + initial = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + assert initial.burst_size == 2 + + due = manager.advance_time("time.clock.scenario-clock", ticks=initial.next_tick) + + assert due.success + assert len(participant_runtime.native_actions) == 2 + state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(due.snapshot.participant_autonomous_execution_states.values())) + ) + assert state.occurrence_ordinal == 2 + assert state.completed_candidate_ids == ["portal_login", "follow_up"] + assert state.candidate_cooldown_until == { + "portal_login": initial.next_tick + 20, + "follow_up": initial.next_tick + 20, + } + attempted = [ + event + for event in due.snapshot.participant_behavior_history[state.participant_address] + if event["event_type"] == "action_attempted" + ] + assert attempted[1]["activity_provenance"]["candidate_id"] == "follow_up" + assert attempted[1]["activity_provenance"]["dependency_candidate_ids"] == ["portal_login"] + assert attempted[1]["activity_provenance"]["timing_disposition"] == "burst" + + +def test_runtime_manager_resets_v2_continuation_and_random_generation() -> None: + scenario = parse_sdl(_activity_policy_yaml()) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _NativeParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + applied = manager.apply(manager.plan(scenario)) + initial = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + ) + first_due = manager.advance_time("time.clock.scenario-clock", ticks=initial.next_tick) + assert first_due.success + first_attempts = [ + event["activity_provenance"] + for event in first_due.snapshot.participant_behavior_history[initial.participant_address] + if event["event_type"] == "action_attempted" + ] + first_attempt_ids = {provenance["attempt_id"] for provenance in first_attempts} + + reset = manager.reset_time("time.clock.scenario-clock") + + assert reset.success + reset_state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(reset.snapshot.participant_autonomous_execution_states.values())) + ) + assert reset_state.time_segment == 1 + assert reset_state.occurrence_ordinal == 0 + assert reset_state.current_retry == 0 + assert reset_state.completed_candidate_ids == [] + assert reset_state.candidate_cooldown_until == {} + second_due = manager.advance_time("time.clock.scenario-clock", ticks=reset_state.next_tick) + assert second_due.success + state = ParticipantAutonomousExecutionStateModel.model_validate( + next(iter(second_due.snapshot.participant_autonomous_execution_states.values())) + ) + attempted = [ + event + for event in second_due.snapshot.participant_behavior_history[state.participant_address] + if event["event_type"] == "action_attempted" + ] + assert [event["activity_provenance"] for event in attempted[: len(first_attempts)]] == first_attempts + second_attempt_ids = {event["activity_provenance"]["attempt_id"] for event in attempted[len(first_attempts) :]} + assert first_attempt_ids.isdisjoint(second_attempt_ids) + latest = attempted[-1]["activity_provenance"] + assert f"episode={reset_state.episode_id}" in latest["attempt_id"] + assert "segment=1" in latest["attempt_id"] + assert latest["occurrence_id"].endswith("segment=1:occurrence=0") + assert latest["random_address"]["time_segment"] == 1 + assert latest["random_address"]["occurrence_ordinal"] == 0 + + +def test_runtime_manager_rejects_v2_activity_without_admitted_random_control() -> None: + scenario = parse_sdl(_activity_policy_yaml()) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _NativeParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + + manager = RuntimeManager(target) + applied = manager.apply(manager.plan(scenario)) + + assert not applied.success + assert any("stochastic control" in diagnostic.message for diagnostic in applied.diagnostics) + + +def test_runtime_manager_fails_closed_for_unresolved_governed_activity_entropy() -> None: + control = _activity_control() + assert control.executable_binding is not None + governed = control.model_copy( + update={ + "executable_binding": control.executable_binding.model_copy( + update={ + "root_entropy": GovernedEntropyRefModel( + kind="governed-reference", + reference_id="participant-activity-seed", + reference_version="1", + ) + } + ) + } + ) + + with pytest.raises(ValueError, match="governed entropy without a resolver"): + RuntimeManager(create_stub_target(), stochastic_controls=[governed]) + + def test_runtime_manager_rolls_back_clock_when_participant_reset_fails() -> None: scenario = parse_sdl(_scenario_yaml()) runtime_model = compile_runtime_model(scenario) @@ -804,7 +1490,7 @@ def test_scheduler_executes_native_actions_and_persists_shared_time_readback() - ) assert (state.attempted_actions, state.succeeded_actions, state.next_tick) == (1, 1, 10) assert state.time_segment == 0 - assert state.policy_digest.startswith("sha256:") + assert state.policy_digest == "sha256:d7c496e43e5782f4459b4b74b62e8b15b4f6d1ddba0ab4044c7fa3f406f502ef" events = first.snapshot.participant_behavior_history[state.participant_address] assert [event["event_type"] for event in events] == [ "action_attempted", @@ -888,6 +1574,35 @@ def test_scheduler_rejects_reapplication_after_material_policy_change() -> None: assert changed.diagnostics[0].code == "runtime.participant-autonomous-state-conflict" +def test_scheduler_rejects_v2_continuation_after_material_policy_change() -> None: + runtime_model = compile_runtime_model(parse_sdl(_activity_policy_yaml())) + policy = runtime_model.behavior_specifications[ + "participant.behavior-specification.participant-behavior" + ].autonomous_execution + assert policy is not None + scheduler = ParticipantScheduler() + controls = resolve_participant_activity_controls([_activity_control()]) + initialized = scheduler.initialize( + [policy], + runtime_model.time_model, + _NativeParticipantRuntime(), + TimeCoordinator(runtime_model.time_model).initialize(), + controls, + ) + assert initialized.success + + changed = scheduler.initialize( + [replace(policy, timing_maximum_ticks=policy.timing_maximum_ticks + 10)], + runtime_model.time_model, + _NativeParticipantRuntime(), + initialized.snapshot, + controls, + ) + + assert not changed.success + assert changed.diagnostics[0].code == "runtime.participant-autonomous-state-conflict" + + def test_scheduler_rejects_reapplication_after_referenced_cadence_change() -> None: runtime_model, policy = _compiled() scheduler = ParticipantScheduler() diff --git a/implementations/python/tests/test_random_stream_determinism.py b/implementations/python/tests/test_random_stream_determinism.py index f01fd97e0..c66646555 100644 --- a/implementations/python/tests/test_random_stream_determinism.py +++ b/implementations/python/tests/test_random_stream_determinism.py @@ -32,7 +32,12 @@ import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st -from raes_contracts.contracts.random_stream import StreamAddressModel, TrialCoordinateModel +from pydantic import ValidationError +from raes_contracts.contracts.random_stream import ( + ParticipantStreamAddressModel, + StreamAddressModel, + TrialCoordinateModel, +) from raes_contracts.random_stream_engine import ( BLOCK_BYTES, _bounded_integer_byte_width, @@ -178,6 +183,81 @@ def test_cross_process_and_hash_seed_independence() -> None: assert digest_seed_0 == digest_seed_1 == in_process +def test_participant_occurrence_address_is_deterministic_and_reset_scoped() -> None: + profile_id = "blake3-xof-participant-v1" + stream_key = derive_stream_key(profile_id=profile_id, root_entropy=ROOT_ENTROPY) + address = ParticipantStreamAddressModel( + namespace="run-policy", + policy_address="participant.autonomous-execution.participant-behavior", + participant_address="participant.behavior.participant-agent", + time_segment=2, + occurrence_ordinal=7, + draw_purpose="agent-policy", + local_coordinate=0, + ) + + first = draw_bounded_integer( + profile_id=profile_id, + stream_key=stream_key, + address=address, + minimum=0, + maximum=9, + max_rejection_attempts=32, + ) + repeated = draw_bounded_integer( + profile_id=profile_id, + stream_key=stream_key, + address=address, + minimum=0, + maximum=9, + max_rejection_attempts=32, + ) + reset = draw_bounded_integer( + profile_id=profile_id, + stream_key=stream_key, + address=address.model_copy(update={"time_segment": 3}), + minimum=0, + maximum=9, + max_rejection_attempts=32, + ) + + assert first == repeated + assert first.value is not None + assert reset.value is not None + assert raw_block(profile_id=profile_id, stream_key=stream_key, address=address) != raw_block( + profile_id=profile_id, + stream_key=stream_key, + address=address.model_copy(update={"time_segment": 3}), + ) + + +@pytest.mark.parametrize( + ("field_name", "invalid_value"), + [ + ("policy_address", "participant.behavior.not-a-policy"), + ("participant_address", "participant.agent.not-a-behavior"), + ("draw_purpose", "ungoverned-purpose"), + ], +) +def test_participant_occurrence_address_rejects_ungoverned_identity_fields( + field_name: str, + invalid_value: str, +) -> None: + payload = { + "namespace": "run-policy", + "policy_address": "participant.autonomous-execution.participant-behavior", + "participant_address": "participant.behavior.participant-agent", + "time_segment": 2, + "occurrence_ordinal": 7, + "draw_purpose": "agent-policy", + "local_coordinate": 0, + } + payload[field_name] = invalid_value + + with pytest.raises(ValidationError): + ParticipantStreamAddressModel.model_validate(payload) + + class TestNonInterferenceProperty: """SVR-015: an unrelated draw or an unrelated address field never perturbs another address's output.""" diff --git a/implementations/python/tests/test_random_stream_profile.py b/implementations/python/tests/test_random_stream_profile.py index 61b1006b6..e3788f0be 100644 --- a/implementations/python/tests/test_random_stream_profile.py +++ b/implementations/python/tests/test_random_stream_profile.py @@ -24,6 +24,7 @@ REPO_ROOT = Path(__file__).resolve().parents[3] PROFILE_PATH = REPO_ROOT / "contracts" / "profiles" / "random-stream" / "blake3-xof-v1.json" +PARTICIPANT_PROFILE_PATH = REPO_ROOT / "contracts" / "profiles" / "random-stream" / "blake3-xof-participant-v1.json" FIXTURES_ROOT = REPO_ROOT / "contracts" / "fixtures" / "profiles" / "random-stream-profile-v1" VALID_DIR = FIXTURES_ROOT / "valid" INVALID_DIR = FIXTURES_ROOT / "invalid" @@ -36,9 +37,15 @@ def test_load_reference_random_stream_profile() -> None: assert profile.generator.family == "blake3" assert "bounded-integer" in profile.transforms + participant_profile = load_random_stream_profile("blake3-xof-participant-v1") + assert participant_profile.profile_id == "blake3-xof-participant-v1" + assert participant_profile.generator.family == "blake3" + assert "bounded-integer" in participant_profile.transforms + def test_reference_profile_path_resolves() -> None: assert random_stream_profile_path("blake3-xof-v1") == PROFILE_PATH + assert random_stream_profile_path("blake3-xof-participant-v1") == PARTICIPANT_PROFILE_PATH def test_reference_profile_matches_valid_fixture() -> None: @@ -90,7 +97,10 @@ def test_rejects_unsupported_but_syntactically_valid_id(self) -> None: random_stream_profile_path("nonexistent-profile-v1") def test_supported_profile_ids_contains_blake3_xof(self) -> None: - assert "blake3-xof-v1" in SUPPORTED_RANDOM_STREAM_PROFILE_IDS + assert { + "blake3-xof-participant-v1", + "blake3-xof-v1", + } == SUPPORTED_RANDOM_STREAM_PROFILE_IDS def test_load_unsupported_profile_fails_closed_without_file_probe(self) -> None: with pytest.raises(ValueError, match="unsupported"): diff --git a/implementations/python/tests/test_random_stream_vectors.py b/implementations/python/tests/test_random_stream_vectors.py index 8d6167786..f4f4e7877 100644 --- a/implementations/python/tests/test_random_stream_vectors.py +++ b/implementations/python/tests/test_random_stream_vectors.py @@ -1,6 +1,6 @@ -"""Canonical-vector conformance tests for the EXP-718 ``blake3-xof-v1`` engine. +"""Canonical-vector conformance tests for published EXP-718 BLAKE3 profiles. -Every vector under ``contracts/fixtures/random-stream-vectors/blake3-xof-v1/`` +Every vector under ``contracts/fixtures/random-stream-vectors/`` was computed independently of ``raes_contracts.random_stream_engine`` (a one-off script that calls the ``blake3`` library directly -- see the EXP-718 implementation notes); these tests run the same inputs through the public @@ -28,12 +28,12 @@ ) REPO_ROOT = Path(__file__).resolve().parents[3] -VECTORS_ROOT = REPO_ROOT / "contracts" / "fixtures" / "random-stream-vectors" / "blake3-xof-v1" +VECTORS_ROOT = REPO_ROOT / "contracts" / "fixtures" / "random-stream-vectors" def _load_vectors() -> list[RandomStreamVectorModel]: vectors = [] - for path in sorted(VECTORS_ROOT.glob("*.json")): + for path in sorted(VECTORS_ROOT.glob("*/*.json")): payload = json.loads(path.read_text(encoding="utf-8")) vectors.append(RandomStreamVectorModel.model_validate(payload)) return vectors @@ -43,7 +43,8 @@ def _load_vectors() -> list[RandomStreamVectorModel]: def test_vector_corpus_is_non_empty() -> None: - assert len(VECTORS) >= 6 + assert len(VECTORS) >= 7 + assert any(vector.profile_id == "blake3-xof-participant-v1" for vector in VECTORS) @pytest.mark.parametrize("vector", VECTORS, ids=lambda v: v.vector_id) diff --git a/specs/formal/participant-semantics/autonomous-execution.md b/specs/formal/participant-semantics/autonomous-execution.md index a37cee88e..d58db080b 100644 --- a/specs/formal/participant-semantics/autonomous-execution.md +++ b/specs/formal/participant-semantics/autonomous-execution.md @@ -4,7 +4,7 @@ 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. -## Authored Policy +## V1 Authored Policy For behavior specification \(B\), autonomous policy \(P\) contains: @@ -31,6 +31,36 @@ system, and external clock authorities are never advanced by the participant driver. Stepped and event-driven clocks advance only through shared-time control. +## V2 Activity Policy + +`participant-autonomous-execution/v2` preserves the same participant, action, +observation, implementation, clock, progression, authority, and native +execution boundaries. It replaces v1 cadence/order fields with: + +- a non-empty work-window set \(W_P\) and optional pause-window set \(H_P\); +- inclusive positive timing bounds \([d_{min}, d_{max}]\); +- stable keyed candidates \(q_i=(action_i, weight_i, dependencies_i, + retryClasses_i, maxRetries_i, cooldown_i)\); +- an `agent-policy` stochastic-control reference; and +- positive finite occurrence, attempt, burst, and in-flight bounds. + +Every availability reference is a finite shared-time `window` on \(C_P\) that +names the governed behavior specification or every governed participant. +Eligibility at tick \(t\) is: + +\[ +eligibleTime(P,t) = + (\exists [s,e) \in W_P : s \le t < e) + \land + \neg(\exists [s,e) \in H_P : s \le t < e) +\] + +Declaration order is not semantic. For stepped progression, both timing bounds +are integer multiples of `step_ticks`. A drawn tick outside eligibility follows +the authored disposition exactly: `skip` terminates that scheduling path; +`next_opening` performs a finite forward search over declared work windows. It +never clamps, redraws, sleeps on wall time, or consults host calendar state. + ## Non-Evaluated Authority Invariant When `evaluation_authority.mode = none`: @@ -63,6 +93,32 @@ apparatus contracts; it does not change this selection relation. The digest covers the resolved clock, time domain, progression policy, and temporal constraints, not only their addresses. +For v2, let \(E(S,t)\) be candidate ids whose dependencies are present in the +typed completed-candidate set and whose cooldown is not later than \(t\). +Candidate ids are sorted canonically before compilation. With exact positive +integer weights and an addressed bounded draw: + +\[ +r \in [0,\sum_{i \in E} weight_i - 1] +\] + +the selected candidate is the first canonical prefix interval containing +\(r\). Dependency filtering precedes the draw. An empty set follows the +authored `complete` or `wait` disposition and never falls back to all +candidates. Timing, selection, and burst-size draws use the immutable +`blake3-xof-participant-v1` profile and the closed address: + +\[ +(namespace, policyAddress, participantAddress, segment, + occurrenceOrdinal, purpose, localCoordinate) +\] + +Retries retain the occurrence ordinal, selected candidate, timing tick, and +selection draw. Retry number, worker identity, call order, wall time, and +backend availability are not stream coordinates. Activity draws are within-run +policy execution and are separate from scenario-family variation and trial +compilation. + ## Execution And Evidence An action may commit only in this order: @@ -90,6 +146,21 @@ match the selected participant implementation. `stop` marks the scheduler failed; `continue` advances the bounded attempt counter and cadence. +For v2, a retry is admitted only when the typed terminal failure class is in +the selected candidate's declared retry set and the per-occurrence retry and +global attempt bounds both remain. Each retry has a distinct attempt id and +names its predecessor. Protocol-invalid or indeterminate work is not retried. +A terminal occurrence updates dependency completion and cooldown state before +the next selection. A burst performs at most `max_burst_size` serialized +occurrences at one due tick; each remains a distinct occurrence and action +attempt. + +Every committed v2 behavior-history event carries safe occurrence provenance: +policy/profile, occurrence and attempt identity, predecessor and dependency +ids, candidate, timing tick/disposition, burst position, terminal outcome, and +the safe control/profile/address identity. It never carries root entropy, +derived keys, raw blocks, or backend-private objects. + ## Lifecycle Pause changes non-terminal scheduler states on the governed clock to `paused`; @@ -114,6 +185,12 @@ atomic batch implementation rather than inherit the reference in-memory transaction. Durable and conformance validation require scheduler segment/lifecycle and episode identity to agree with the bound shared clock and live episode. +V2 reset also clears occurrence, retry, dependency, cooldown, and burst +continuation, then derives the next timing and burst values under the new +shared-time segment. The segment is part of every activity address, so reset +generations cannot alias prior draws. Participant/service state changes remain +owned by native action results and existing episode/reset contracts; scheduler +timestamps alone make no causal or rollback claim. ## Backend Admission @@ -152,6 +229,18 @@ feature set. A reset-capable policy additionally requires the coordinated participant-reset capability and runtime method. Runtime state, typed native action outcome, history, and backend evidence establish what occurred. +V2 additionally requires exact admission of: + +- `participant-autonomous-execution/v2`; +- all governed activity features; +- `weighted`; +- `blake3-xof-participant-v1`; +- shared-time `window`; and +- occurrence, retry-per-occurrence, and burst-size maxima. + +Missing or differently named support fails admission; no compatible-profile, +transform, or strategy fallback is inferred. + ## Nonclaims This contract does not prove participant intelligence, human realism, service From 13f67129474e5691347bd0b7447364034411fe01 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 22:24:17 +0200 Subject: [PATCH 19/55] Complete RAES identity cutover --- .claude/agents/completion-verifier.md | 6 +- .claude/hooks/verify-extra.sh | 8 +- .../raes-asset-inventory-capture/SKILL.md | 38 +- .../agents/openai.yaml | 6 +- .../scripts/normalize-syft-cyclonedx.jq | 2 +- .../raes-gap-remediation-implement/SKILL.md | 14 +- .../agents/openai.yaml | 4 +- .codex | 4 +- .../raes-asset-inventory-capture/SKILL.md | 38 +- .../agents/openai.yaml | 6 +- .../scripts/normalize-syft-cyclonedx.jq | 2 +- .../raes-gap-remediation-implement/SKILL.md | 14 +- .../agents/openai.yaml | 4 +- .gc/plan-rules.md | 8 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/dependabot.yml | 2 +- .github/workflows/ci.yml | 2 +- .gitleaks.toml | 4 +- .ground-control.yaml | 4 +- .mcp.json | 2 +- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- contracts/README.md | 2 +- .../behavioral-relations-v1.json | 10 +- .../concept-families-v1.json | 18 +- .../controlled-vocabularies-v1.json | 8 +- .../concept-authority/uco-alignment-v1.json | 10 +- .../authenticity-without-threshold.json | 2 +- .../invalid/duplicate-evidence-class.json | 2 +- .../invalid/missing-family.json | 2 +- .../invalid/missing-integrity.json | 2 +- .../invalid/secret-bearing.json | 2 +- .../invalid/unknown-family.json | 2 +- .../vocabulary-missing-governance-source.json | 2 +- .../valid/reference.json | 12 +- .../invalid/duplicate-binding-scope.json | 2 +- .../feature-support-duplicate-feature.json | 2 +- .../feature-support-missing-disclosure.json | 2 +- ...eature-support-unguarded-feature-term.json | 2 +- ...-support-unsupported-declared-feature.json | 2 +- .../invalid/hollow-provisioner.json | 2 +- .../invalid/hollow-realization-support.json | 2 +- .../invalid/invalid-binding-family.json | 2 +- .../invalid/malformed-compatibility.json | 2 +- .../malformed-realization-support.json | 2 +- .../invalid/missing-concept-bindings.json | 2 +- .../invalid/missing-version.json | 2 +- .../invalid/non-backend-contract-version.json | 2 +- .../non-processor-compatibility-surface.json | 2 +- .../invalid/unknown-workflow-feature.json | 2 +- .../unknown-workflow-state-predicate.json | 2 +- .../valid/feature-support-bounded.json | 2 +- .../backend-manifest-v2/valid/stub.json | 2 +- .../valid/reference.json | 2 +- .../invalid/native-with-authority.json | 4 +- .../concept-families-v1/valid/reference.json | 16 +- .../valid/reference.json | 8 +- .../invalid/adapted-without-divergence.json | 4 +- .../invalid/empty-divergence.json | 4 +- .../invalid/iri-mismatch.json | 10 +- .../invalid/missing-family.json | 10 +- .../invalid/native-family.json | 10 +- .../invalid/provenance-mismatch.json | 10 +- .../invalid/unknown-family.json | 10 +- .../uco-alignment-v1/valid/reference.json | 10 +- .../valid/reference.json | 10 +- .../invalid/invalid-generated-at.json | 2 +- .../invalid/non-reported-with-value.json | 2 +- .../invalid/reported-without-value.json | 2 +- .../valid/reference.json | 2 +- .../augmentation-without-affected-refs.json | 10 +- .../realized-form-authority-mismatch.json | 14 +- .../invalid/realized-form-missing-target.json | 14 +- ...zed-form-processor-authority-mismatch.json | 14 +- .../traceability-empty-capture-specs.json | 14 +- .../experiment-run-v1/valid/reference.json | 14 +- .../invalid/empty-membership.json | 2 +- .../experiment-study-v1/valid/reference.json | 4 +- .../experiment-task-v1/valid/reference.json | 6 +- .../invalid/cross-outcome-payload.json | 32 +- .../valid/valid-path.json | 32 +- .../valid/reference.json | 2 +- .../valid/reference.json | 2 +- .../invalid/unknown-phase.json | 4 +- .../valid/opaque-llm-execution-attempt.json | 4 +- .../valid/opaque-llm-selection.json | 4 +- .../missing-visibility-projection.json | 4 +- .../valid/rl-observation-only.json | 4 +- .../invalid/empty-outcome-sources.json | 4 +- .../invalid/empty-state-relationships.json | 4 +- .../invalid/inline-score-field.json | 4 +- .../valid/exfiltration-outcome.json | 4 +- .../invalid/unknown-access-kind.json | 4 +- .../serialized-service-state-commit.json | 4 +- .../invalid/empty-compatibility.json | 2 +- .../invalid/hollow-capabilities.json | 2 +- .../invalid/hollow-realization-support.json | 2 +- .../malformed-realization-support.json | 2 +- .../non-backend-compatibility-surface.json | 2 +- .../invalid/unknown-feature.json | 2 +- .../valid/reference.json | 2 +- .../invalid/bad-profile-id-pattern.json | 2 +- .../invalid/empty-transforms.json | 2 +- .../invalid/missing-generator.json | 2 +- .../invalid/transform-key-mismatch.json | 2 +- .../invalid/unknown-field.json | 2 +- .../valid/blake3-xof-v1.json | 2 +- .../valid/minimal.json | 2 +- .../invalid/missing-required-gate.json | 2 +- .../invalid/run-missing-ref-version.json | 4 +- .../scenario-snapshot-missing-digest.json | 2 +- .../invalid/unknown-profile.json | 2 +- .../valid/evidence-backed-run.json | 4 +- .../valid/scenario-snapshot.json | 2 +- .../valid/structural-scenario.json | 2 +- .../invalid/dangling-reference.json | 6 +- .../valid/minimal.json | 6 +- .../sdl-lineage-ledger-v1/valid/minimal.json | 4 +- .../basic-address-and-block.json | 4 +- .../bounded-integer-basic-draw.json | 6 +- ...bounded-integer-boundary-single-value.json | 4 +- .../bounded-integer-exhaustion.json | 8 +- ...bounded-integer-rejection-then-accept.json | 10 +- .../zero-heavy-seed-and-address.json | 4 +- .../invalid/cross-outcome-payload.json | 18 +- .../valid/unsupported-target.json | 20 +- .../invalid/wrong-profile.json | 4 +- .../valid/minimal.json | 4 +- .../invalid/imports-empty.json | 2 +- .../invalid/module-null.json | 2 +- .../invalid/unresolved-variable.json | 2 +- .../invalid/variables-empty.json | 2 +- .../valid/minimal.json | 2 +- .../composition/module.yaml | 2 +- .../authoring-binding-scope-not-governed.json | 4 +- .../missing-concept-catalog-version.json | 4 +- ...processing-binding-scope-not-governed.json | 4 +- .../valid/reference-stack-v1.json | 4 +- .../runtime-snapshot-v1/valid/reference.json | 2 +- .../profiles/random-stream/blake3-xof-v1.json | 2 +- ...scientific-scenario-completeness-rev1.json | 10 +- .../profiles/semantic/reference-stack-v1.json | 4 +- .../validation-profile-catalog-v1.json | 26 +- .../entries/aces-semantic-invariants-v1.json | 10 - .../associated-artifact-manifest-v1.json | 6 +- .../entries/atlas-tactics-source-v1.json | 6 +- .../attack-enterprise-tactics-source-v1.json | 6 +- .../entries/backend-manifest-v2.json | 6 +- .../entries/backend-profile-v1.json | 6 +- .../entries/behavioral-relations-v1.json | 6 +- .../entries/concept-families-v1.json | 6 +- .../entries/controlled-vocabularies-v1.json | 6 +- .../evaluation-history-event-stream-v1.json | 6 +- .../entries/evaluation-plan-v1.json | 6 +- .../evaluation-result-envelope-v1.json | 6 +- .../experiment-apparatus-context-v1.json | 6 +- .../experiment-authoring-input-v1.json | 6 +- .../experiment-binding-descriptors-v1.json | 6 +- .../entries/experiment-capture-spec-v1.json | 6 +- .../experiment-derived-measure-v1.json | 6 +- .../experiment-evidence-record-v1.json | 6 +- .../entries/experiment-run-v1.json | 6 +- .../entries/experiment-study-v1.json | 6 +- .../entries/experiment-task-v1.json | 6 +- .../exploit-path-analysis-evidence-v1.json | 6 +- .../instantiated-scenario-snapshot-v1.json | 6 +- .../entries/instantiated-scenario-v1.json | 6 +- ...st-csf-defensive-categories-source-v1.json | 6 +- .../entries/operation-receipt-v1.json | 6 +- .../entries/operation-status-v1.json | 6 +- .../entries/orchestration-plan-v1.json | 6 +- ...pant-behavior-history-event-stream-v1.json | 6 +- .../participant-configuration-result-v1.json | 6 +- .../entries/participant-context-view-v1.json | 6 +- .../participant-control-occurrence-v1.json | 6 +- .../participant-crossing-occurrence-v1.json | 6 +- .../participant-decision-surface-v1.json | 6 +- ...ipant-episode-history-event-stream-v1.json | 6 +- ...participant-episode-state-envelope-v1.json | 6 +- .../entries/participant-history-view-v1.json | 6 +- ...articipant-implementation-manifest-v1.json | 6 +- ...ticipant-implementation-provenance-v1.json | 6 +- .../participant-joint-action-record-v1.json | 6 +- .../participant-lifecycle-event-v1.json | 6 +- .../participant-observation-envelope-v1.json | 6 +- .../participant-outcome-report-v1.json | 6 +- .../participant-shared-state-record-v1.json | 6 +- .../entries/participant-status-view-v1.json | 6 +- ...articipant-time-management-context-v1.json | 6 +- .../entries/processor-manifest-v2.json | 6 +- .../entries/proposition-truth-result-v1.json | 6 +- .../entries/provisioning-plan-v1.json | 6 +- .../entries/raes-semantic-invariants-v1.json | 10 + .../entries/random-stream-profile-v1.json | 6 +- .../entries/random-stream-vector-v1.json | 6 +- .../entries/realization-envelope-v1.json | 6 +- .../entries/realized-time-model-v1.json | 6 +- .../entries/reference-models-v1.json | 6 +- .../reusable-asset-trust-policy-v1.json | 6 +- .../runtime-fact-binding-plane-v1.json | 6 +- .../entries/runtime-snapshot-v1.json | 6 +- .../scenario-instantiation-request-v1.json | 6 +- .../scenario-satisfiability-evidence-v1.json | 6 +- .../entries/scheduler-isolation-proof-v1.json | 6 +- ...scientific-completeness-assessment-v1.json | 6 +- .../scientific-completeness-taxonomy-v1.json | 6 +- .../entries/sdl-authoring-input-v1.json | 6 +- .../entries/sdl-lineage-ledger-v1.json | 6 +- .../entries/semantic-profile-v1.json | 6 +- .../entries/time-model-v1.json | 6 +- .../entries/time-runtime-state-v1.json | 6 +- .../entries/trial-cleanup-plan-v1.json | 6 +- .../entries/trial-cleanup-receipt-v1.json | 6 +- .../entries/uco-alignment-v1.json | 6 +- .../validation-basis-disclosure-v1.json | 6 +- .../validation-profile-catalog-v1.json | 6 +- .../workflow-cancellation-request-v1.json | 6 +- .../workflow-history-event-stream-v1.json | 6 +- .../entries/workflow-result-envelope-v1.json | 6 +- .../aces-semantic-invariants-v1.json | 4 + contracts/schemas/README.md | 10 +- .../reusable-asset-trust-policy-v1.json | 4 +- .../associated-artifact-manifest-v1.json | 16 +- .../backend-manifest/backend-manifest-v2.json | 2 +- .../atlas-tactics-source-v1.json | 2 +- .../attack-enterprise-tactics-source-v1.json | 2 +- .../behavioral-relations-v1.json | 20 +- .../concept-families-v1.json | 2 +- .../controlled-vocabularies-v1.json | 2 +- ...st-csf-defensive-categories-source-v1.json | 2 +- .../reference-models-v1.json | 2 +- .../concept-authority/uco-alignment-v1.json | 2 +- .../evaluation-history-event-stream-v1.json | 2 +- .../evaluation-result-envelope-v1.json | 2 +- .../control-plane/operation-receipt-v1.json | 2 +- .../control-plane/operation-status-v1.json | 2 +- ...pant-behavior-history-event-stream-v1.json | 2 +- .../participant-context-view-v1.json | 16 +- .../participant-decision-surface-v1.json | 16 +- ...ipant-episode-history-event-stream-v1.json | 2 +- ...participant-episode-state-envelope-v1.json | 2 +- .../participant-history-view-v1.json | 2 +- .../participant-status-view-v1.json | 2 +- .../proposition-truth-result-v1.json | 2 +- .../scheduler-isolation-proof-v1.json | 16 +- .../trial-cleanup-receipt-v1.json | 16 +- .../workflow-cancellation-request-v1.json | 2 +- .../workflow-history-event-stream-v1.json | 2 +- .../workflow-result-envelope-v1.json | 2 +- .../experiment-apparatus-context-v1.json | 18 +- .../experiment-authoring-input-v1.json | 26 +- .../experiment-binding-descriptors-v1.json | 16 +- .../experiment-capture-spec-v1.json | 20 +- .../experiment-derived-measure-v1.json | 18 +- .../experiment-evidence-record-v1.json | 18 +- .../experiment-core/experiment-run-v1.json | 32 +- .../experiment-core/experiment-study-v1.json | 22 +- .../experiment-core/experiment-task-v1.json | 24 +- .../exploit-path-analysis-evidence-v1.json | 50 +- .../participant-configuration-result-v1.json | 16 +- ...articipant-implementation-manifest-v1.json | 2 +- ...ticipant-implementation-provenance-v1.json | 2 +- .../participant-control-occurrence-v1.json | 16 +- .../participant-crossing-occurrence-v1.json | 16 +- .../participant-joint-action-record-v1.json | 2 +- .../participant-lifecycle-event-v1.json | 2 +- .../participant-observation-envelope-v1.json | 2 +- .../participant-outcome-report-v1.json | 2 +- .../participant-shared-state-record-v1.json | 2 +- ...articipant-time-management-context-v1.json | 2 +- .../runtime-fact-binding-plane-v1.json | 16 +- .../schemas/plans/evaluation-plan-v1.json | 2 +- .../schemas/plans/orchestration-plan-v1.json | 2 +- .../schemas/plans/provisioning-plan-v1.json | 2 +- .../schemas/plans/trial-cleanup-plan-v1.json | 16 +- .../processor-manifest-v2.json | 2 +- .../schemas/profiles/backend-profile-v1.json | 2 +- ....json => raes-semantic-invariants-v1.json} | 42 +- .../profiles/random-stream-profile-v1.json | 16 +- .../profiles/random-stream-vector-v1.json | 16 +- ...scientific-completeness-assessment-v1.json | 16 +- .../scientific-completeness-taxonomy-v1.json | 16 +- .../schemas/profiles/semantic-profile-v1.json | 2 +- .../validation-basis-disclosure-v1.json | 16 +- .../validation-profile-catalog-v1.json | 20 +- .../provenance/sdl-lineage-ledger-v1.json | 24 +- .../realization-envelope-v1.json | 16 +- .../scenario-satisfiability-evidence-v1.json | 74 +- .../instantiated-scenario-snapshot-v1.json | 28 +- .../schemas/sdl/instantiated-scenario-v1.json | 24 +- .../scenario-instantiation-request-v1.json | 2 +- .../schemas/sdl/sdl-authoring-input-v1.json | 54 +- .../snapshots/runtime-snapshot-v1.json | 16 +- .../schemas/time/realized-time-model-v1.json | 2 +- contracts/schemas/time/time-model-v1.json | 2 +- .../schemas/time/time-runtime-state-v1.json | 2 +- docs/decisions/adrs/README.md | 6 +- ...icipant-backend-facing-contract-surface.md | 2 +- .../adr-067-participant-behavior-model.md | 14 +- ...r-072-validation-and-admission-profiles.md | 12 +- ...ng-deprecation-and-migration-governance.md | 2 +- ...aes-rename-and-compatibility-boundaries.md | 2 +- ...-cutover-and-historical-record-boundary.md | 131 ++ docs/decisions/adrs/adr-index.yaml | 11 +- .../issue-908-aces-naming-audit-preflight.md | 206 -- ...sue-908-raes-identity-cutover-preflight.md | 143 ++ .../reference/normative-artifact-authority.md | 3 +- .../reference/realization-envelopes.md | 2 +- .../reference/reference-emulation-backend.md | 2 +- docs/explain/sdl/lineage.md | 6 +- docs/explain/sdl/parser.md | 4 +- docs/explain/sdl/runtime-architecture.md | 2 +- .../sdl/scientific-scenario-completeness.md | 2 +- docs/explain/sdl/sections.md | 2 +- docs/lessons/README.md | 2 +- docs/migration/README.md | 7 +- docs/migration/raes-rename.md | 137 +- docs/raes/inventory/ad-preflight.md | 2 +- .../asset-inventory-issue-template.md | 10 +- .../inventory/asset-inventory-methodology.md | 12 +- .../issue-516-redaction-boundary-preflight.md | 2 +- docs/raes/inventory/kali-preflight.md | 8 +- .../inventory/methodology-assurance-report.md | 12 +- .../scn010-expressivity-gap-analysis.md | 14 +- docs/raes/inventory/webapp-preflight.md | 8 +- examples/library/catalog.yaml | 2 +- .../patterns/observable-study-conditions.yaml | 2 +- ...participant-behavior-contract-binding.yaml | 2 +- .../patterns/run-window-with-evidence.yaml | 2 +- .../scenario-reference-integrity.yaml | 2 +- .../patterns/task-as-objective-contract.yaml | 2 +- .../workflow-explicit-control-graph.yaml | 2 +- .../action-contract-observation-boundary.yaml | 2 +- .../templates/run/timed-run-control.yaml | 6 +- .../scenario/minimal-validated-scenario.yaml | 4 +- .../study/observational-study-protocol.yaml | 6 +- .../templates/task/single-objective-task.yaml | 6 +- .../workflow/parallel-objective-workflow.yaml | 8 +- ...rprise-participant-evidence-loop.README.md | 4 +- ...erprise-participant-evidence-loop.sdl.yaml | 8 +- .../hospital-ransomware-surgery-day.sdl.yaml | 20 +- .../scenarios/initial-service-state.sdl.yaml | 2 +- .../port-authority-surge-response.sdl.yaml | 12 +- .../satcom-release-poisoning.sdl.yaml | 12 +- .../techvault-guest-certified.sdl.yaml | 2 +- implementations/python/packages/raes/_base.py | 2 +- .../raes/_realization_envelope_engine.py | 2 +- .../python/packages/raes/_source_profile.py | 2 +- .../python/packages/raes/canonical.py | 6 +- .../python/packages/raes/module_registry.py | 20 +- .../raes/observability_plane_semantics.py | 4 +- .../python/packages/raes/phase_contracts.py | 2 +- .../python/packages/raes/scenario.py | 16 +- .../raes_backend_libvirt/cloudinit.py | 4 +- .../packages/raes_backend_libvirt/dialects.py | 8 +- .../raes_backend_libvirt/drivers/libvirt.py | 8 +- .../raes_backend_libvirt/guest_appliance.py | 44 +- .../raes_backend_libvirt/guest_transport.py | 2 +- .../packages/raes_backend_libvirt/manifest.py | 2 +- .../raes_backend_libvirt/provisioner.py | 2 +- .../raes_backend_libvirt/realization.py | 10 +- .../techvault_appliance.py | 6 +- .../raes_backend_libvirt/techvault_matrix.py | 4 +- .../raes_backend_libvirt/techvault_native.py | 4 +- .../packages/raes_backend_stubs/manifest.py | 2 +- .../python/packages/raes_cli/corpus.py | 2 +- .../python/packages/raes_cli/libvirt.py | 6 +- .../raes_conformance/conformance/report.py | 2 +- .../raes_conformance/conformance/target.py | 2 +- .../conformance/target_probes.py | 2 +- .../packages/raes_conformance/realization.py | 2 +- .../raes_contracts/behavioral_relations.py | 2 +- .../raes_contracts/contracts/__init__.py | 16 +- .../contracts/associated_artifacts.py | 4 +- .../packages/raes_contracts/contracts/base.py | 2 +- .../raes_contracts/contracts/bundle.py | 30 +- .../contracts/experiment_apparatus.py | 6 +- .../contracts/experiment_bindings.py | 6 +- .../contracts/experiment_capture.py | 8 +- .../contracts/experiment_disclosure.py | 8 +- .../contracts/experiment_evidence.py | 20 +- .../experiment_manifest_references.py | 4 +- .../contracts/experiment_run.py | 18 +- .../contracts/experiment_spec.py | 20 +- .../contracts/experiment_study.py | 6 +- .../contracts/participant_context.py | 10 +- .../contracts/participant_control.py | 6 +- .../contracts/participant_crossing.py | 6 +- .../contracts/participant_decision_surface.py | 8 +- .../raes_contracts/contracts/random_stream.py | 10 +- .../contracts/reusable_assets.py | 2 +- .../contracts/schema_constraints.py | 76 +- .../contracts/schema_invariants.py | 48 +- .../raes_contracts/contracts/trial_cleanup.py | 8 +- .../contracts/validation_disclosure.py | 4 +- .../packages/raes_contracts/exploit_path.py | 26 +- .../packages/raes_contracts/provenance.py | 12 +- .../raes_contracts/random_stream_engine.py | 4 +- .../realization_envelope_carrier.py | 2 +- .../packages/raes_contracts/satisfiability.py | 16 +- .../raes_contracts/validation_profiles.py | 2 +- .../packages/raes_mcp/tools/authoring.py | 4 +- .../packages/raes_mcp/tools/reference.py | 2 +- .../_cross_backend_corpus_backend_runs.py | 6 +- .../_cross_backend_corpus_validation.py | 2 +- .../raes_operations/_evidence_run_artifact.py | 8 +- .../raes_operations/_evidence_run_native.py | 4 +- .../raes_operations/cross_backend_corpus.py | 4 +- .../deterministic_participant_fixtures.py | 2 +- .../raes_operations/libvirt_evidence_run.py | 2 +- .../raes_operations/techvault_live.py | 10 +- .../raes_processor/exploit_path/_preflight.py | 4 +- .../raes_processor/exploit_path/_search.py | 6 +- .../raes_processor/exploit_path/_service.py | 6 +- .../packages/raes_processor/manifest.py | 2 +- .../raes_processor/satisfiability/_service.py | 10 +- .../raes_processor/satisfiability/_solver.py | 2 +- .../satisfiability/_translation.py | 6 +- .../raes_reference_backend/drivers/oci.py | 16 +- .../raes_reference_backend/manifest.py | 2 +- .../raes_reference_backend/realization.py | 6 +- .../raes_runtime/control_plane_security.py | 4 +- .../raes_runtime/participant_clock_driver.py | 2 +- implementations/python/pyproject.toml | 2 +- .../tests/libvirt_conformance_fixtures.py | 2 +- .../tests/test_agent_inventory_skill.py | 8 +- ...api_409_participant_control_occurrences.py | 2 +- ..._api_423_participant_crossing_contracts.py | 2 +- .../tests/test_authored_domain_topology.py | 10 +- .../tests/test_backend_conformance_cli.py | 6 +- .../python/tests/test_backend_manifest.py | 14 +- .../tests/test_behavioral_relation_claims.py | 2 +- .../python/tests/test_behavioral_relations.py | 8 +- .../python/tests/test_concept_authority.py | 4 +- .../test_concept_authority_governance.py | 2 +- .../python/tests/test_corpus_packaging.py | 15 +- .../python/tests/test_cross_backend_corpus.py | 2 +- .../tests/test_deprecation_lifecycle.py | 16 +- .../test_dsl_115_authoring_specificity.py | 2 +- ...t_dsl_123_scenario_native_observability.py | 2 +- ..._dsl_124_authored_evidence_requirements.py | 2 +- .../tests/test_dsl_language_evaluation.py | 14 +- .../test_enterprise_deployment_tenancy.py | 6 +- .../tests/test_example_library_policy.py | 8 +- .../python/tests/test_experiment_bindings.py | 12 +- .../tests/test_exploit_path_analysis.py | 36 +- .../python/tests/test_exploit_path_cli.py | 2 +- .../python/tests/test_fm2_semantics.py | 16 +- .../tests/test_formal_semantic_validation.py | 4 +- .../tests/test_identity_cutover_policy.py | 152 ++ .../tests/test_initial_service_state.py | 4 +- .../test_instantiated_scenario_schema.py | 2 +- .../python/tests/test_libvirt_backend_cli.py | 2 +- .../tests/test_libvirt_backend_cloudinit.py | 4 +- .../tests/test_libvirt_backend_dialects.py | 4 +- .../tests/test_libvirt_backend_driver.py | 62 +- .../test_libvirt_backend_guest_certified.py | 18 +- ...rt_backend_guest_certified_real_libvirt.py | 6 +- .../tests/test_libvirt_backend_realization.py | 14 +- .../test_libvirt_backend_techvault_native.py | 10 +- ..._libvirt_backend_techvault_real_libvirt.py | 4 +- .../python/tests/test_libvirt_conformance.py | 2 +- .../python/tests/test_mcp_server.py | 19 +- .../tests/test_network_namespace_sharing.py | 2 +- ...test_observability_evidence_conformance.py | 2 +- .../test_participant_backend_contracts.py | 2 +- ...est_participant_implementation_manifest.py | 4 +- .../test_participant_interactive_access.py | 4 +- .../python/tests/test_plan_inspection_cli.py | 2 +- .../python/tests/test_processor_manifest.py | 2 +- .../tests/test_proposition_semantics.py | 22 +- .../test_realization_envelope_contract.py | 6 +- .../test_reference_backend_components.py | 2 +- ...st_reference_backend_docker_integration.py | 15 +- .../test_reference_backend_oci_driver.py | 26 +- .../tests/test_reference_field_pruning.py | 2 +- .../python/tests/test_reference_processor.py | 6 +- .../tests/test_related_work_comparison.py | 14 +- .../python/tests/test_repo_policy_tools.py | 62 +- .../tests/test_reusable_asset_trust_policy.py | 2 +- .../python/tests/test_run_300_lifecycle.py | 2 +- ...n_305_participant_runtime_state_history.py | 4 +- .../test_run_307_shared_operational_state.py | 4 +- .../python/tests/test_runtime_conformance.py | 6 +- .../python/tests/test_runtime_contracts.py | 58 +- .../tests/test_runtime_control_plane.py | 4 +- .../tests/test_runtime_control_plane_api.py | 60 +- .../tests/test_runtime_fact_bindings.py | 2 +- .../tests/test_runtime_family_invariants.py | 4 +- .../python/tests/test_runtime_manager.py | 6 +- .../python/tests/test_runtime_models.py | 16 +- .../python/tests/test_runtime_planner.py | 46 +- .../python/tests/test_runtime_registry.py | 2 +- .../python/tests/test_runtime_ssh_server.py | 2 +- .../python/tests/test_satisfiability_cli.py | 4 +- .../tests/test_sce_004_goal_oriented_steps.py | 12 +- .../test_scientific_scenario_completeness.py | 4 +- .../python/tests/test_sdl_canonicalization.py | 14 +- .../python/tests/test_sdl_identifiers.py | 10 +- .../python/tests/test_sdl_lineage.py | 21 +- .../python/tests/test_sdl_models.py | 2 +- .../python/tests/test_sdl_module_registry.py | 28 +- .../python/tests/test_sdl_parser.py | 34 +- .../python/tests/test_sdl_phase_contracts.py | 14 +- .../python/tests/test_sdl_realworld.py | 4 +- .../python/tests/test_sdl_stress.py | 10 +- .../python/tests/test_sdl_validator.py | 10 +- .../python/tests/test_sdl_variation_points.py | 4 +- .../test_sem_208_participant_behavior.py | 12 +- ..._215_participant_outcome_interpretation.py | 2 +- .../tests/test_sem_216_boundary_semantics.py | 4 +- .../python/tests/test_sem_218_realization.py | 2 +- .../test_sem_218_realization_designation.py | 2 +- ...st_sem_220_participant_decision_surface.py | 2 +- ...t_sem_224_observability_plane_semantics.py | 4 +- .../test_sem_225_augmentation_semantics.py | 4 +- .../tests/test_sem_227_shared_time_model.py | 2 +- .../python/tests/test_semantics_objectives.py | 4 +- .../test_stateful_realization_resources.py | 4 +- .../tests/test_validation_disclosure.py | 24 +- .../python/tests/test_validation_profiles.py | 10 +- .../tests/test_version_classification.py | 22 - noxfile.py | 16 +- sonar-project.properties | 6 +- specs/concept-authority/concept-authority.md | 2 +- .../controlled-vocabularies.md | 2 +- specs/concept-authority/reference-models.md | 4 +- specs/concept-authority/semantic-profiles.md | 4 +- specs/evolution/deprecation-records.yaml | 12 - .../versioning-deprecation-and-migration.md | 1 - specs/formal/behavioral-relations/README.md | 2 +- specs/formal/experiment-core/README.md | 6 +- specs/formal/exploit-path-analysis/README.md | 10 +- specs/formal/observability-evidence-plane.md | 4 +- specs/formal/participant-runtime/README.md | 44 +- specs/formal/participant-semantics/README.md | 6 +- .../information-flow-control.md | 2 +- .../formal/realization/envelope-semantics.md | 2 +- .../formal/scenario-satisfiability/README.md | 8 +- .../cleanup-contracts.md | 2 +- specs/formal/sdl-phases/README.md | 2 +- .../validation-admission-profiles/README.md | 4 +- specs/sdl/document-model.md | 10 +- specs/sdl/scientific-scenario-completeness.md | 2 +- specs/sdl/stateful-resources.md | 2 +- specs/sdl/variables-and-instantiation.md | 2 +- .../reusable-asset-trust-integrity.md | 2 +- tools/check_deprecation_lifecycle.py | 1 - tools/check_dsl_language_evaluation.py | 17 +- tools/check_example_library.py | 6 +- tools/check_formal_semantic_validation.py | 91 +- tools/check_identity_cutover.py | 250 +++ tools/check_related_work_comparison.py | 29 +- tools/check_requirement_governance.py | 4 +- tools/check_sdl_lineage.py | 31 +- tools/check_specification_coverage.py | 32 +- tools/generate_contract_schemas.py | 2 +- tools/gitleaks_tool.py | 4 +- tools/osv_scanner_tool.py | 2 +- tools/policy/adr_policy.yaml | 8 +- tools/policy/conftest_tool.py | 8 +- tools/policy/historical_identity_records.json | 1784 +++++++++++++++++ tools/policy/repo_policy.py | 51 - tools/policy/requirement_governance.py | 2 +- tools/policy/requirement_order.yaml | 2 +- tools/real-daemon/README.md | 4 +- ...est-certified-asr519-20260712T031842Z.json | 20 +- tools/real-daemon/libvirt_smoke.py | 68 +- tools/real-daemon/run_aws_guest_certify.sh | 28 +- tools/real-daemon/run_aws_smoke.sh | 18 +- 571 files changed, 4873 insertions(+), 2630 deletions(-) delete mode 100644 contracts/schema-publication/entries/aces-semantic-invariants-v1.json create mode 100644 contracts/schema-publication/entries/raes-semantic-invariants-v1.json create mode 100644 contracts/schema-publication/tombstones/aces-semantic-invariants-v1.json rename contracts/schemas/profiles/{aces-semantic-invariants-v1.json => raes-semantic-invariants-v1.json} (76%) create mode 100644 docs/decisions/adrs/adr-095-identity-cutover-and-historical-record-boundary.md delete mode 100644 docs/decisions/issue-908-aces-naming-audit-preflight.md create mode 100644 docs/decisions/issue-908-raes-identity-cutover-preflight.md create mode 100644 implementations/python/tests/test_identity_cutover_policy.py create mode 100644 tools/check_identity_cutover.py create mode 100644 tools/policy/historical_identity_records.json diff --git a/.claude/agents/completion-verifier.md b/.claude/agents/completion-verifier.md index e2065752f..f6b4e12e3 100644 --- a/.claude/agents/completion-verifier.md +++ b/.claude/agents/completion-verifier.md @@ -1,6 +1,6 @@ --- name: completion-verifier -description: Verifies ACES SDL repo-policy, requirement-governance, changelog, and ADR-index completion before Claude stops. +description: Verifies RAES SDL repo-policy, requirement-governance, changelog, and ADR-index completion before Claude stops. tools: Read, Grep, Glob, Bash model: sonnet maxTurns: 20 @@ -8,7 +8,7 @@ maxTurns: 20 # Completion Verifier -You verify that implementation work is complete for ACES SDL. Prefer the +You verify that implementation work is complete for RAES SDL. Prefer the repo-owned policy scripts over ad hoc reasoning. Run these checks and return your verdict: @@ -18,7 +18,7 @@ Run these checks and return your verdict: - Run `git diff --name-only HEAD` to see what files changed. - Determine whether Python implementation or test files changed under `implementations/python/`. -- Determine the active requirement UID from `ACES_REQUIREMENT_UID` or the branch +- Determine the active requirement UID from `RAES_REQUIREMENT_UID` or the branch name. ## 2. Run repo policy checks diff --git a/.claude/hooks/verify-extra.sh b/.claude/hooks/verify-extra.sh index 21dbcf264..b4a1592b0 100755 --- a/.claude/hooks/verify-extra.sh +++ b/.claude/hooks/verify-extra.sh @@ -12,13 +12,13 @@ if [[ ! -x "$PY" ]]; then exit 0 fi -if ! "$PY" "$ROOT/tools/check_repo_policy.py" >/tmp/aces-sdl-policy.out 2>&1; then - tr '\n' ' ' /tmp/raes-sdl-policy.out 2>&1; then + tr '\n' ' ' /tmp/aces-sdl-gc.out 2>&1; then - tr '\n' ' ' /tmp/raes-sdl-gc.out 2>&1; then + tr '\n' ' ' -aptl aces-inventory gaps +aptl raes-inventory schema +aptl raes-inventory validate +aptl raes-inventory gaps ``` No `needs_gap_triage` row may remain at review time. @@ -144,8 +144,8 @@ No `needs_gap_triage` row may remain at review time. 6. Build the ledger fact-by-fact. - Each captured fact needs evidence and an ACES/APTL mapping disposition: - `encoded`, `encoded_with_caveat`, `blocked_by_aces_gap`, + Each captured fact needs evidence and an RAES/APTL mapping disposition: + `encoded`, `encoded_with_caveat`, `blocked_by_raes_gap`, `blocked_by_aptl_gap`, or temporary `needs_gap_triage`. Before review, replace every `needs_gap_triage` with an encoded mapping or a linked gap. @@ -176,7 +176,7 @@ CLI commands as fake URL paths, collapse flags into schema-passing representation. A fact that requires reinterpretation of the surface is a gap. -**Lineage-before-gap gate.** Before reporting an ACES expressivity gap, read +**Lineage-before-gap gate.** Before reporting an RAES expressivity gap, read `docs/explain/sdl/lineage.md`, `docs/explain/sdl/precedents.md`, relevant ADRs, and primary literature for the affected semantic family. The gap report must state the captured fact, checked surfaces, lineage/prior-art treatment, @@ -189,18 +189,18 @@ explicitly asks for upstream design work. ## Gap Handling -When current ACES cannot express a participant-discoverable fact: +When current RAES cannot express a participant-discoverable fact: -1. Search existing ACES and APTL issues. +1. Search existing RAES and APTL issues. 2. If a matching issue exists, link it in `mapping-ledger.yaml`. 3. If none exists, file or draft a gap with evidence path, discovery vantage, - checked ACES surfaces, lineage/prior-art notes, and why the surfaces are + checked RAES surfaces, lineage/prior-art notes, and why the surfaces are insufficient. 4. Stop for discussion before continuing through additional gaps when the methodology or issue asks for that pause. -Do not use APTL backend consumption gaps as a substitute for ACES SDL -expression. Do not use ACES schema shape as the authority for what was +Do not use APTL backend consumption gaps as a substitute for RAES SDL +expression. Do not use RAES schema shape as the authority for what was discovered. ## Completion Checklist @@ -219,8 +219,8 @@ Before returning: limits; - scenario-target secrets are preserved as source capture facts, while operator/out-of-scenario material is excluded or recorded as a capture limit; -- `aptl aces-inventory validate ` passes; -- `aptl aces-inventory gaps ` has no unresolved +- `aptl raes-inventory validate ` passes; +- `aptl raes-inventory gaps ` has no unresolved `needs_gap_triage`; - any methodology document in the target checkout references this skill as the canonical agent entry point; diff --git a/.claude/skills/raes-asset-inventory-capture/agents/openai.yaml b/.claude/skills/raes-asset-inventory-capture/agents/openai.yaml index 9d6cf8b6c..ef6ce6f78 100644 --- a/.claude/skills/raes-asset-inventory-capture/agents/openai.yaml +++ b/.claude/skills/raes-asset-inventory-capture/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "ACES Asset Inventory Capture" - short_description: "Run the ACES asset inventory methodology." - default_prompt: "Use $raes-asset-inventory-capture to capture a methodology-conformant ACES asset inventory bundle for a target image or container." + display_name: "RAES Asset Inventory Capture" + short_description: "Run the RAES asset inventory methodology." + default_prompt: "Use $raes-asset-inventory-capture to capture a methodology-conformant RAES asset inventory bundle for a target image or container." diff --git a/.claude/skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq b/.claude/skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq index 4cdb0e091..e851d3f75 100644 --- a/.claude/skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq +++ b/.claude/skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq @@ -1,4 +1,4 @@ -# Deterministic ACES inventory Syft CycloneDX normalization. +# Deterministic RAES inventory Syft CycloneDX normalization. # Allowed transform: remove Syft file-location component properties only. def strip_syft_location_properties: walk( diff --git a/.claude/skills/raes-gap-remediation-implement/SKILL.md b/.claude/skills/raes-gap-remediation-implement/SKILL.md index 3ddf83ff8..9c282b4db 100644 --- a/.claude/skills/raes-gap-remediation-implement/SKILL.md +++ b/.claude/skills/raes-gap-remediation-implement/SKILL.md @@ -1,9 +1,9 @@ --- name: raes-gap-remediation-implement -description: Architecture-first overlay for remediating ACES/APTL gaps, especially gaps found by the ACES asset inventory capture methodology. Use before normal Ground Control /implement work when a gap may require new or changed SDL/runtime semantics, schemas, validators, ADRs, docs, capture mappings, or cross-repo ACES/APTL behavior. Forces whole-surface review, lineage and primary-literature grounding, peer-review-grade justification, and then delegates to the repo's standard Ground Control implement workflow. +description: Architecture-first overlay for remediating RAES/APTL gaps, especially gaps found by the RAES asset inventory capture methodology. Use before normal Ground Control /implement work when a gap may require new or changed SDL/runtime semantics, schemas, validators, ADRs, docs, capture mappings, or cross-repo RAES/APTL behavior. Forces whole-surface review, lineage and primary-literature grounding, peer-review-grade justification, and then delegates to the repo's standard Ground Control implement workflow. --- -# ACES Gap Remediation Implement +# RAES Gap Remediation Implement Use this skill as an overlay before the normal Ground Control implementation lane. It does not replace `/implement`; it supplies the architecture and @@ -12,7 +12,7 @@ documentation obligations that must be carried into `/implement`. The expected review bar is academic peer review for tier-1 publication: a reviewer must be able to reconstruct why the remediation exists, why existing surfaces were insufficient, why the chosen design is not duplicative, and how -the implementation follows established ACES lineage and relevant external +the implementation follows established RAES lineage and relevant external practice. ## Operating Rule @@ -35,11 +35,11 @@ Before invoking `/implement`, produce or add to the issue a brief with these sections: 1. **Gap Claim**: captured fact, discovery vantage, evidence paths, affected - scenario/asset, and the assurance claim that current ACES/APTL cannot make. + scenario/asset, and the assurance claim that current RAES/APTL cannot make. 2. **Existing Surface Audit**: SDL sections, runtime subdomains, schemas, validators, parser aliases, module refs, docs, examples, and related issues checked. State why each near match is insufficient. -3. **Lineage and Precedent**: relevant ACES lineage, precedents, ADRs, prior +3. **Lineage and Precedent**: relevant RAES lineage, precedents, ADRs, prior issues/PRs, and downstream APTL usage. Explain whether the gap extends an existing family or requires a new one. 4. **Literature and Practice**: primary literature and core/adjacent practice @@ -67,7 +67,7 @@ Run this gate before choosing a design. service, sensor, manager, content, evidence, and source/provenance families as applicable. - Check whether the fact is scenario state, delivery infrastructure, evidence, - control-plane machinery, downstream APTL consumption, or an ACES SDL + control-plane machinery, downstream APTL consumption, or an RAES SDL expression gap. - Treat duplicate parallel surfaces as a design failure. Extend the established family when the semantics belong there; create a new family only when the @@ -81,7 +81,7 @@ Reject an implementation plan if any gate fails. generic relationships, or arbitrary key/value fields. - **No shallow schema pass**: do not choose the representation that validates while changing the meaning of the captured fact. -- **No downstream-only fix**: do not patch APTL consumption when ACES lacks the +- **No downstream-only fix**: do not patch APTL consumption when RAES lacks the needed expression surface. - **No docs-free surface**: every new or changed semantic surface needs public rationale in ADR/docs, not just tests. diff --git a/.claude/skills/raes-gap-remediation-implement/agents/openai.yaml b/.claude/skills/raes-gap-remediation-implement/agents/openai.yaml index 65d458527..73b1a5c0d 100644 --- a/.claude/skills/raes-gap-remediation-implement/agents/openai.yaml +++ b/.claude/skills/raes-gap-remediation-implement/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "ACES Gap Remediation" + display_name: "RAES Gap Remediation" short_description: "Architecture-first gap remediation overlay" - default_prompt: "Use $raes-gap-remediation-implement to remediate an ACES/APTL gap through the Ground Control implement workflow." + default_prompt: "Use $raes-gap-remediation-implement to remediate an RAES/APTL gap through the Ground Control implement workflow." diff --git a/.codex b/.codex index 66eef9268..9b1105e7d 100644 --- a/.codex +++ b/.codex @@ -8,7 +8,7 @@ Use the repo policy tooling before and after implementation work. - `implementations/python/.venv/bin/python tools/check_requirement_governance.py` - `implementations/python/.venv/bin/python tools/verify_all.py` -Set `ACES_REQUIREMENT_UID` when the branch name does not already contain a UID +Set `RAES_REQUIREMENT_UID` when the branch name does not already contain a UID such as `GOV-918`. ## Hard rules @@ -17,7 +17,7 @@ such as `GOV-918`. `docs/`, and `implementations/`. - Do not edit `contracts/schemas/` directly; change generator inputs and regenerate. -- Do not recreate the retired `implementations/python/src/aces/` namespace. +- Do not recreate retired compatibility namespaces. - Import only the owning `raes` or `raes_*` packages under `implementations/python/packages/`. - Keep concept-authority artifacts in the approved concept-authority surfaces. diff --git a/.codex-skills/raes-asset-inventory-capture/SKILL.md b/.codex-skills/raes-asset-inventory-capture/SKILL.md index 613216161..b30c4cb62 100644 --- a/.codex-skills/raes-asset-inventory-capture/SKILL.md +++ b/.codex-skills/raes-asset-inventory-capture/SKILL.md @@ -1,18 +1,18 @@ --- name: raes-asset-inventory-capture -description: Run the ACES asset inventory methodology as an agent-runnable capture workflow from either Claude Code or Codex. Use when asked to inventory a target image, container, host, or asset into a methodology-conformant evidence bundle with mapping-ledger.yaml, explicit capture-limits.txt declinations, scanner/SBOM evidence, and ACES/APTL gap triage. +description: Run the RAES asset inventory methodology as an agent-runnable capture workflow from either Claude Code or Codex. Use when asked to inventory a target image, container, host, or asset into a methodology-conformant evidence bundle with mapping-ledger.yaml, explicit capture-limits.txt declinations, scanner/SBOM evidence, and RAES/APTL gap triage. --- -# ACES Asset Inventory Capture +# RAES Asset Inventory Capture This skill runs from either Claude Code or Codex. Use it as the canonical agent entry point for the participant-discoverable asset inventory -methodology: capture evidence first, map facts to ACES only when the mapping is +methodology: capture evidence first, map facts to RAES only when the mapping is semantically correct, and leave every omission as durable evidence. -The ACES methodology remains the authority. Read -`docs/raes/inventory/asset-inventory-methodology.md` from the ACES repo before -capture; when working in a downstream checkout, use that ACES document as the +The RAES methodology remains the authority. Read +`docs/raes/inventory/asset-inventory-methodology.md` from the RAES repo before +capture; when working in a downstream checkout, use that RAES document as the canonical source. This skill operationalizes that prose; it does not define a second ledger schema or a second secret taxonomy. @@ -67,9 +67,9 @@ it. Run these before returning: ```shell -aptl aces-inventory schema -aptl aces-inventory validate -aptl aces-inventory gaps +aptl raes-inventory schema +aptl raes-inventory validate +aptl raes-inventory gaps ``` No `needs_gap_triage` row may remain at review time. @@ -144,8 +144,8 @@ No `needs_gap_triage` row may remain at review time. 6. Build the ledger fact-by-fact. - Each captured fact needs evidence and an ACES/APTL mapping disposition: - `encoded`, `encoded_with_caveat`, `blocked_by_aces_gap`, + Each captured fact needs evidence and an RAES/APTL mapping disposition: + `encoded`, `encoded_with_caveat`, `blocked_by_raes_gap`, `blocked_by_aptl_gap`, or temporary `needs_gap_triage`. Before review, replace every `needs_gap_triage` with an encoded mapping or a linked gap. @@ -176,7 +176,7 @@ CLI commands as fake URL paths, collapse flags into schema-passing representation. A fact that requires reinterpretation of the surface is a gap. -**Lineage-before-gap gate.** Before reporting an ACES expressivity gap, read +**Lineage-before-gap gate.** Before reporting an RAES expressivity gap, read `docs/explain/sdl/lineage.md`, `docs/explain/sdl/precedents.md`, relevant ADRs, and primary literature for the affected semantic family. The gap report must state the captured fact, checked surfaces, lineage/prior-art treatment, @@ -189,18 +189,18 @@ explicitly asks for upstream design work. ## Gap Handling -When current ACES cannot express a participant-discoverable fact: +When current RAES cannot express a participant-discoverable fact: -1. Search existing ACES and APTL issues. +1. Search existing RAES and APTL issues. 2. If a matching issue exists, link it in `mapping-ledger.yaml`. 3. If none exists, file or draft a gap with evidence path, discovery vantage, - checked ACES surfaces, lineage/prior-art notes, and why the surfaces are + checked RAES surfaces, lineage/prior-art notes, and why the surfaces are insufficient. 4. Stop for discussion before continuing through additional gaps when the methodology or issue asks for that pause. -Do not use APTL backend consumption gaps as a substitute for ACES SDL -expression. Do not use ACES schema shape as the authority for what was +Do not use APTL backend consumption gaps as a substitute for RAES SDL +expression. Do not use RAES schema shape as the authority for what was discovered. ## Completion Checklist @@ -219,8 +219,8 @@ Before returning: limits; - scenario-target secrets are preserved as source capture facts, while operator/out-of-scenario material is excluded or recorded as a capture limit; -- `aptl aces-inventory validate ` passes; -- `aptl aces-inventory gaps ` has no unresolved +- `aptl raes-inventory validate ` passes; +- `aptl raes-inventory gaps ` has no unresolved `needs_gap_triage`; - any methodology document in the target checkout references this skill as the canonical agent entry point; diff --git a/.codex-skills/raes-asset-inventory-capture/agents/openai.yaml b/.codex-skills/raes-asset-inventory-capture/agents/openai.yaml index 9d6cf8b6c..ef6ce6f78 100644 --- a/.codex-skills/raes-asset-inventory-capture/agents/openai.yaml +++ b/.codex-skills/raes-asset-inventory-capture/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "ACES Asset Inventory Capture" - short_description: "Run the ACES asset inventory methodology." - default_prompt: "Use $raes-asset-inventory-capture to capture a methodology-conformant ACES asset inventory bundle for a target image or container." + display_name: "RAES Asset Inventory Capture" + short_description: "Run the RAES asset inventory methodology." + default_prompt: "Use $raes-asset-inventory-capture to capture a methodology-conformant RAES asset inventory bundle for a target image or container." diff --git a/.codex-skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq b/.codex-skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq index 4cdb0e091..e851d3f75 100644 --- a/.codex-skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq +++ b/.codex-skills/raes-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq @@ -1,4 +1,4 @@ -# Deterministic ACES inventory Syft CycloneDX normalization. +# Deterministic RAES inventory Syft CycloneDX normalization. # Allowed transform: remove Syft file-location component properties only. def strip_syft_location_properties: walk( diff --git a/.codex-skills/raes-gap-remediation-implement/SKILL.md b/.codex-skills/raes-gap-remediation-implement/SKILL.md index 3ddf83ff8..9c282b4db 100644 --- a/.codex-skills/raes-gap-remediation-implement/SKILL.md +++ b/.codex-skills/raes-gap-remediation-implement/SKILL.md @@ -1,9 +1,9 @@ --- name: raes-gap-remediation-implement -description: Architecture-first overlay for remediating ACES/APTL gaps, especially gaps found by the ACES asset inventory capture methodology. Use before normal Ground Control /implement work when a gap may require new or changed SDL/runtime semantics, schemas, validators, ADRs, docs, capture mappings, or cross-repo ACES/APTL behavior. Forces whole-surface review, lineage and primary-literature grounding, peer-review-grade justification, and then delegates to the repo's standard Ground Control implement workflow. +description: Architecture-first overlay for remediating RAES/APTL gaps, especially gaps found by the RAES asset inventory capture methodology. Use before normal Ground Control /implement work when a gap may require new or changed SDL/runtime semantics, schemas, validators, ADRs, docs, capture mappings, or cross-repo RAES/APTL behavior. Forces whole-surface review, lineage and primary-literature grounding, peer-review-grade justification, and then delegates to the repo's standard Ground Control implement workflow. --- -# ACES Gap Remediation Implement +# RAES Gap Remediation Implement Use this skill as an overlay before the normal Ground Control implementation lane. It does not replace `/implement`; it supplies the architecture and @@ -12,7 +12,7 @@ documentation obligations that must be carried into `/implement`. The expected review bar is academic peer review for tier-1 publication: a reviewer must be able to reconstruct why the remediation exists, why existing surfaces were insufficient, why the chosen design is not duplicative, and how -the implementation follows established ACES lineage and relevant external +the implementation follows established RAES lineage and relevant external practice. ## Operating Rule @@ -35,11 +35,11 @@ Before invoking `/implement`, produce or add to the issue a brief with these sections: 1. **Gap Claim**: captured fact, discovery vantage, evidence paths, affected - scenario/asset, and the assurance claim that current ACES/APTL cannot make. + scenario/asset, and the assurance claim that current RAES/APTL cannot make. 2. **Existing Surface Audit**: SDL sections, runtime subdomains, schemas, validators, parser aliases, module refs, docs, examples, and related issues checked. State why each near match is insufficient. -3. **Lineage and Precedent**: relevant ACES lineage, precedents, ADRs, prior +3. **Lineage and Precedent**: relevant RAES lineage, precedents, ADRs, prior issues/PRs, and downstream APTL usage. Explain whether the gap extends an existing family or requires a new one. 4. **Literature and Practice**: primary literature and core/adjacent practice @@ -67,7 +67,7 @@ Run this gate before choosing a design. service, sensor, manager, content, evidence, and source/provenance families as applicable. - Check whether the fact is scenario state, delivery infrastructure, evidence, - control-plane machinery, downstream APTL consumption, or an ACES SDL + control-plane machinery, downstream APTL consumption, or an RAES SDL expression gap. - Treat duplicate parallel surfaces as a design failure. Extend the established family when the semantics belong there; create a new family only when the @@ -81,7 +81,7 @@ Reject an implementation plan if any gate fails. generic relationships, or arbitrary key/value fields. - **No shallow schema pass**: do not choose the representation that validates while changing the meaning of the captured fact. -- **No downstream-only fix**: do not patch APTL consumption when ACES lacks the +- **No downstream-only fix**: do not patch APTL consumption when RAES lacks the needed expression surface. - **No docs-free surface**: every new or changed semantic surface needs public rationale in ADR/docs, not just tests. diff --git a/.codex-skills/raes-gap-remediation-implement/agents/openai.yaml b/.codex-skills/raes-gap-remediation-implement/agents/openai.yaml index 65d458527..73b1a5c0d 100644 --- a/.codex-skills/raes-gap-remediation-implement/agents/openai.yaml +++ b/.codex-skills/raes-gap-remediation-implement/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "ACES Gap Remediation" + display_name: "RAES Gap Remediation" short_description: "Architecture-first gap remediation overlay" - default_prompt: "Use $raes-gap-remediation-implement to remediate an ACES/APTL gap through the Ground Control implement workflow." + default_prompt: "Use $raes-gap-remediation-implement to remediate an RAES/APTL gap through the Ground Control implement workflow." diff --git a/.gc/plan-rules.md b/.gc/plan-rules.md index f8f2a29a8..51eeb6766 100644 --- a/.gc/plan-rules.md +++ b/.gc/plan-rules.md @@ -1,4 +1,4 @@ -# aces-sdl plan rules +# raes-sdl plan rules Mandatory constraints the `/implement` skill applies during plan phase. These encode the hard rules previously in `AGENTS.md` prose. @@ -9,7 +9,7 @@ These encode the hard rules previously in `AGENTS.md` prose. before declaring completion. - Plans MUST run `implementations/python/.venv/bin/python tools/verify_all.py` before declaring completion. -- Plans MUST set `ACES_REQUIREMENT_UID` when the branch name does not +- Plans MUST set `RAES_REQUIREMENT_UID` when the branch name does not already contain a UID such as `GOV-918`. - Plans MUST NOT add new authority-bearing artifacts outside `specs/`, `contracts/`, `docs/`, and `implementations/`. @@ -23,10 +23,6 @@ These encode the hard rules previously in `AGENTS.md` prose. `tools/check_generated_schemas.py` passes. The published schema is the authority; a generator/Python edit alone is NOT authorization for a schema change. -- Plans MUST NOT recreate the retired `implementations/python/src/aces/` - namespace. -- Plans MUST NOT import retired `aces` or `aces_*` modules from owning packages - under `implementations/python/packages/`. - Plans MUST keep concept-authority artifacts in the approved concept-authority surfaces. - Plans MUST keep IMPLEMENTS and TESTS traceability in Ground Control diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 15a717b8c..ded0d8e0e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,7 +10,7 @@ - Requirement UID: - ADRs touched: -- Ground Control project: `aces-sdl` +- Ground Control project: `raes-sdl` ## Changes diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0cba9dcdc..cea3a8fbc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,7 +33,7 @@ updates: update-types: ["minor", "patch"] ignore: # z3-solver is pinned by a governed contract, not by ordinary dependency - # policy: the aces-z3-finite-domain/v1 profile fixes package_version + # policy: the raes-z3-finite-domain/v1 profile fixes package_version # 4.16.0.0 / engine_version 4.16.0 in SolverConfigurationModel, in the # published scenario-satisfiability-evidence-v1 schema, and in its # fixtures, so satisfiability evidence stays reproducible. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1978168f..5a06f0736 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: echo "uid=$REQ_UID" >> "$GITHUB_OUTPUT" - name: Run canonical verification graph env: - ACES_REQUIREMENT_UID: ${{ steps.requirement.outputs.uid }} + RAES_REQUIREMENT_UID: ${{ steps.requirement.outputs.uid }} GC_BASE_URL: ${{ vars.GC_BASE_URL }} run: | verify_args=(--base-rev "${{ steps.base.outputs.base_rev }}") diff --git a/.gitleaks.toml b/.gitleaks.toml index 9129dd2bf..9b0ee39f2 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,4 +1,4 @@ -title = "aces gitleaks config" +title = "raes gitleaks config" [extend] useDefault = true @@ -35,7 +35,7 @@ non-target-ruled allowlists to `c.Allowlists`) so it is evaluated solely at the The `paths` pattern is deliberately not anchored to the string start (`^`): the repo's own `noxfile.py` `hygiene` session scans through `--follow-symlinks`, which makes gitleaks match `paths` against each file's *resolved absolute* filesystem path, not its repository-relative path. Anchoring -on `^contracts/...` would never match `/home/.../aces2/contracts/...` and silently stop exempting the +on `^contracts/...` would never match `/home/.../worktree/contracts/...` and silently stop exempting the legitimate vectors under a symlink-following scan. Matching on a `/`-or-start-bounded segment instead works for both a plain repository-relative scan and the real resolved-symlink scan, while still requiring a full path-segment boundary (rejecting an unrelated path that merely contains this text as diff --git a/.ground-control.yaml b/.ground-control.yaml index 56179c5bf..c5bab9e4e 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -1,5 +1,5 @@ schema_version: 1 -project: aces-sdl +project: raes-sdl github_repo: RAESystem/rae workflow: test_command: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify @@ -31,7 +31,7 @@ routing: default_provider: claude stages: {} sonarcloud: - project_key: Brad-Edwards_aces + project_key: Brad-Edwards_raes organization: brad-edwards rules: plan_rules: .gc/plan-rules.md diff --git a/.mcp.json b/.mcp.json index ab5241406..3a4f58f6b 100644 --- a/.mcp.json +++ b/.mcp.json @@ -6,7 +6,7 @@ "args": ["/home/atomik/src/Ground-Control/mcp/ground-control/index.js"], "env": { "GC_BASE_URL": "http://red-dragon:8000", - "GH_REPO": "aces-framework/aces-sdl" + "GH_REPO": "RAESystem/rae" } }, "sonarqube": { diff --git a/AGENTS.md b/AGENTS.md index 9e0e535bc..356897495 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ plan rules set under `.gc/plan-rules.md`). Agents read it via the `gc_get_repo_ground_control_context` MCP tool, which returns the full workflow config in a single call. -Set `ACES_REQUIREMENT_UID` when the branch name does not already +Set `RAES_REQUIREMENT_UID` when the branch name does not already contain a UID such as `GOV-918`. The required repo-policy checks and hard rules are enforced by the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 466a572cd..e1e807613 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ Set up the Python implementation: ```shell git clone https://github.com/RAESystem/rae.git -cd aces/implementations/python +cd raes/implementations/python uv sync --all-extras ``` diff --git a/README.md b/README.md index b15aec37f..9d447182b 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ or provisioning a scenario: ```shell uv run --project implementations/python raes processor satisfiability \ path/to/scenario.sdl.yaml \ - --profile aces-finite-domain-satisfiability-v1 + --profile raes-finite-domain-satisfiability-v1 ``` The command emits the published replayable evidence envelope. Exit `0` is a diff --git a/contracts/README.md b/contracts/README.md index 294bdd63c..d9a23ebf4 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -84,7 +84,7 @@ and derived measure schemas under `contracts/schemas/experiment-core/`. These contracts are archival design artifacts for scientific experiment records; they do not add runtime execution, capture, storage, scheduling, statistical engines, or API behavior by themselves. Claim-bearing study and benchmark records bind -their conclusions to `aces-behavioral-relations@rev1`, including population or +their conclusions to `raes-behavioral-relations@rev1`, including population or case scope, measurement projection, evidence boundary, limitations, and explicit nonclaims. diff --git a/contracts/concept-authority/behavioral-relations-v1.json b/contracts/concept-authority/behavioral-relations-v1.json index 762f91ec1..e9b88e5bd 100644 --- a/contracts/concept-authority/behavioral-relations-v1.json +++ b/contracts/concept-authority/behavioral-relations-v1.json @@ -1,6 +1,6 @@ { "schema_version": "behavioral-relations/v1", - "taxonomy_id": "aces-behavioral-relations", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "bibliography": [ { @@ -290,7 +290,7 @@ "display_name": "Semantic validity", "relation_class": "predicate", "definition": "A structurally admitted artifact satisfies the named cross-reference and domain invariants.", - "left_carrier": "A parsed ACES artifact.", + "left_carrier": "A parsed RAES artifact.", "right_carrier": "The named semantic invariant set.", "initial_states": "Not applicable; this is a unary predicate.", "transition_signature": { @@ -886,7 +886,7 @@ "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 ACES transition system.", + "right_carrier": "Abstract RAES transition system.", "initial_states": "Related concrete and abstract initial states.", "transition_signature": { "applicability": "applicable", @@ -2300,7 +2300,7 @@ "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 ACES backend." + "This toy counterexample is not evidence about any RAES backend." ] }, "hidden-action-counterexample": { @@ -2348,7 +2348,7 @@ "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 ACES backend relation." + "The example does not declare arbitrary backend-internal work hidden and does not prove an RAES backend relation." ] } } diff --git a/contracts/concept-authority/concept-families-v1.json b/contracts/concept-authority/concept-families-v1.json index cdea96168..a6922fa04 100644 --- a/contracts/concept-authority/concept-families-v1.json +++ b/contracts/concept-authority/concept-families-v1.json @@ -61,7 +61,7 @@ "title": "Tasks, Runs, and Studies", "description": "Execution lifecycle, run records, and study organization.", "provenance": "native", - "extension_scope": "ACES experiment workflow concepts for task framing, run lifecycle, study grouping, and analysis organization.", + "extension_scope": "RAES experiment workflow concepts for task framing, run lifecycle, study grouping, and analysis organization.", "relation_rules": [ "May reference `scenarios` as the authoring input under evaluation and `provenance-and-evidence` concepts as recorded outputs.", "Must describe experiment organization and lifecycle rather than the cyber-domain assets or actions inside a scenario." @@ -75,7 +75,7 @@ "title": "Behavioral Relations", "description": "Revisioned predicates and relations used to state validation, conformance, comparison, refinement, equivalence, and empirical claims.", "provenance": "native", - "extension_scope": "ACES claim semantics that bind a named relation to carriers, transition and observation policies, quantifier scope, evidence boundary, assurance status, and explicit nonclaims.", + "extension_scope": "RAES claim semantics that bind a named relation to carriers, transition and observation policies, quantifier scope, evidence boundary, assurance status, and explicit nonclaims.", "relation_rules": [ "May classify claims about `scenarios`, `apparatus-declarations`, `realization-and-disclosure`, `tasks-runs-studies`, participant histories, and `provenance-and-evidence` without reclassifying the underlying artifacts.", "Must bind participant-visible comparisons to the governed observation projection and must keep finite evidence separate from universal trace, simulation, refinement, or bisimulation obligations.", @@ -91,7 +91,7 @@ "title": "Episodes", "description": "Participant runtime episode identity, lifecycle state, and history boundaries.", "provenance": "native", - "extension_scope": "ACES participant runtime episode identity, lifecycle state, state/history contracts, reset/restart/terminate semantics, and participant-scoped behavior sequencing.", + "extension_scope": "RAES participant runtime episode identity, lifecycle state, state/history contracts, reset/restart/terminate semantics, and participant-scoped behavior sequencing.", "relation_rules": [ "May relate to `tasks-runs-studies` as the participant-level runtime segment that occurs within a task, run, or study without becoming that task, run, or study.", "May relate to `scenarios` as the runtime execution instance of a participant admitted from scenario context, while scenario authoring remains governed by `scenarios`.", @@ -107,7 +107,7 @@ "title": "Runtime Inventory", "description": "Observed and declared runtime configuration state attached to scenario nodes.", "provenance": "native", - "extension_scope": "ACES observed and declared runtime inventory of scenario node state under nodes.*.runtime: services, platforms, packages and software components, controls and security posture, filesystem and mounts, processes and scheduled jobs, and other node-scoped apparatus-state facts.", + "extension_scope": "RAES observed and declared runtime inventory of scenario node state under nodes.*.runtime: services, platforms, packages and software components, controls and security posture, filesystem and mounts, processes and scheduled jobs, and other node-scoped apparatus-state facts.", "relation_rules": [ "May relate to `assets` as the observed runtime configuration state of an asset-bearing scenario node without becoming the node or its asset identity.", "May relate to `observables` when an inventory fact is surfaced as a condition, metric, or telemetry signal, while the structured inventory record itself remains `runtime-inventory`.", @@ -123,7 +123,7 @@ "title": "Apparatus Declarations", "description": "Processor, backend, and participant-implementation manifests.", "provenance": "native", - "extension_scope": "ACES apparatus identity, capability, compatibility, and declaration surfaces for processors, backends, and participant implementations.", + "extension_scope": "RAES apparatus identity, capability, compatibility, and declaration surfaces for processors, backends, and participant implementations.", "relation_rules": [ "May bind declared capability vocabularies to adopted or adapted cyber-domain families through concept_bindings.", "Must keep apparatus identity and compatibility separate from the cyber-domain objects that an apparatus can process or realize." @@ -137,7 +137,7 @@ "title": "Realization and Disclosure", "description": "Instantiation, planning, compilation, and realization artifacts.", "provenance": "native", - "extension_scope": "ACES realization semantics for turning underspecified authoring inputs into executable, planned, or disclosed artifacts.", + "extension_scope": "RAES realization semantics for turning underspecified authoring inputs into executable, planned, or disclosed artifacts.", "relation_rules": [ "May relate `scenarios` to `apparatus-declarations` by describing what an apparatus can realize or disclose.", "Must distinguish realization support from the cyber-domain objects being realized." @@ -151,7 +151,7 @@ "title": "Provenance and Evidence", "description": "Run provenance records, evidence expectations, and audit artifacts.", "provenance": "native", - "extension_scope": "ACES provenance, evidence expectation, evidence collection, and audit-record semantics for experiments and execution.", + "extension_scope": "RAES provenance, evidence expectation, evidence collection, and audit-record semantics for experiments and execution.", "relation_rules": [ "May record observations about `scenarios`, tasks, runs, studies, apparatus, and cyber-domain objects without redefining those objects.", "Must separate evidence/provenance records from the observed asset, identity, observable, action, relationship, or artifact." @@ -165,14 +165,14 @@ "title": "Time and Apparatus", "description": "Clocks, timing constraints, and apparatus-level concerns.", "provenance": "native", - "extension_scope": "ACES timing, clock, scheduling, synchronization, and apparatus-time semantics that govern execution rather than cyber-domain object identity.", + "extension_scope": "RAES timing, clock, scheduling, synchronization, and apparatus-time semantics that govern execution rather than cyber-domain object identity.", "relation_rules": [ "May constrain `scenarios`, tasks, runs, realization, and apparatus behavior through time or clock declarations.", "Must keep timing and clock semantics separate from the cyber-domain event or action whose occurrence is being timed." ], "non_ambiguity_constraints": [ "Must not be used as a synonym for actions-and-events or observables.", - "Temporal fields that describe an event meaning must bind to actions-and-events or observables; fields that describe ACES clock policy may bind here." + "Temporal fields that describe an event meaning must bind to actions-and-events or observables; fields that describe RAES clock policy may bind here." ] } } diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index 24ca22534..b6d73a4c5 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -146,7 +146,7 @@ }, "participant-defensive-behavior-activities": { "title": "Participant Defensive Behavior Activities", - "description": "ACES adaptations of NIST CSF 2.0 Detect, Respond, and Recover categories as governed defensive behavior classifications for behavior specifications.", + "description": "RAES adaptations of NIST CSF 2.0 Detect, Respond, and Recover categories as governed defensive behavior classifications for behavior specifications.", "source": { "provenance": "adapted", "authority": "NIST Cybersecurity Framework", @@ -673,11 +673,11 @@ }, "adapted": { "title": "Adapted", - "description": "Concept family adapts an external authority for ACES use." + "description": "Concept family adapts an external authority for RAES use." }, "native": { "title": "Native", - "description": "Concept family is native to ACES." + "description": "Concept family is native to RAES." } } }, @@ -933,7 +933,7 @@ }, "autonomous_execution": { "title": "Autonomous Execution", - "description": "Backend supports ACES-clock-governed autonomous execution of ordinary participant actions." + "description": "Backend supports RAES-clock-governed autonomous execution of ordinary participant actions." }, "attribution_support": { "title": "Attribution Support", diff --git a/contracts/concept-authority/uco-alignment-v1.json b/contracts/concept-authority/uco-alignment-v1.json index 3426debb3..c40b4b810 100644 --- a/contracts/concept-authority/uco-alignment-v1.json +++ b/contracts/concept-authority/uco-alignment-v1.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,13 +85,13 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ - "ACES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", - "ACES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", - "ACES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." + "RAES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", + "RAES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", + "RAES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." ] }, "observables": { diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/authenticity-without-threshold.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/authenticity-without-threshold.json index 8b36ac6ba..8eab0441d 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/authenticity-without-threshold.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/authenticity-without-threshold.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/duplicate-evidence-class.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/duplicate-evidence-class.json index bc870ed47..b69a5e224 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/duplicate-evidence-class.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/duplicate-evidence-class.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-family.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-family.json index 1bbe57186..f85567da3 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-family.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-family.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-integrity.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-integrity.json index 430baa968..2a1793878 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-integrity.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/missing-integrity.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/secret-bearing.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/secret-bearing.json index 6bc062369..42e14fa7f 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/secret-bearing.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/secret-bearing.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/unknown-family.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/unknown-family.json index 460d425d1..417182d26 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/unknown-family.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/unknown-family.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/vocabulary-missing-governance-source.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/vocabulary-missing-governance-source.json index 054455210..752b4c303 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/vocabulary-missing-governance-source.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/invalid/vocabulary-missing-governance-source.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", diff --git a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/valid/reference.json b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/valid/reference.json index 157aed6b4..84555ca0a 100644 --- a/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/valid/reference.json +++ b/contracts/fixtures/asset-trust/reusable-asset-trust-policy-v1/valid/reference.json @@ -1,6 +1,6 @@ { "schema_version": "reusable-asset-trust-policy/v1", - "policy_id": "aces-reusable-asset-trust-policy", + "policy_id": "raes-reusable-asset-trust-policy", "families": [ { "asset_family": "associated_artifact_set", @@ -39,7 +39,7 @@ { "evidence_class": "provenance_lock_record", "enforcement": "required", - "mechanism_ref": "aces.lock.json module lock records for every composed module", + "mechanism_ref": "raes.lock.json module lock records for every composed module", "description": "The scenario's composed modules are pinned by digest via the lockfile, giving SLSA-style resolved-dependency provenance over sub-assets." }, { @@ -50,7 +50,7 @@ } ], "authenticity_policy": { - "trusted_signer_set_ref": "aces-trust.yaml:trusted_signers", + "trusted_signer_set_ref": "raes-trust.yaml:trusted_signers", "threshold": 1 } }, @@ -61,7 +61,7 @@ { "evidence_class": "integrity_digest", "enforcement": "required", - "mechanism_ref": "aces.lock.json digest pin (_validate_digest_pin)", + "mechanism_ref": "raes.lock.json digest pin (_validate_digest_pin)", "description": "A resolved module import is pinned to a content digest recorded in the lockfile." }, { @@ -78,7 +78,7 @@ } ], "authenticity_policy": { - "trusted_signer_set_ref": "aces-trust.yaml:trusted_signers", + "trusted_signer_set_ref": "raes-trust.yaml:trusted_signers", "threshold": 1 } }, @@ -172,7 +172,7 @@ } ], "authenticity_policy": { - "trusted_signer_set_ref": "aces-trust.yaml:evidence_signers", + "trusted_signer_set_ref": "raes-trust.yaml:evidence_signers", "threshold": 1 } } diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/duplicate-binding-scope.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/duplicate-binding-scope.json index 16294898c..8c44839fb 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/duplicate-binding-scope.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/duplicate-binding-scope.json @@ -2,7 +2,7 @@ "schema_version": "backend-manifest/v2", "identity": {"name": "stub", "version": "0.2.0"}, "supported_contract_versions": ["backend-manifest-v2"], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-duplicate-feature.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-duplicate-feature.json index 3afe181e0..1a23357ff 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-duplicate-feature.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-duplicate-feature.json @@ -28,7 +28,7 @@ ], "compatibility": { "processors": [ - "aces-reference-processor" + "raes-reference-processor" ] }, "realization_support": [ diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-missing-disclosure.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-missing-disclosure.json index 9f2491705..cd624c9c2 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-missing-disclosure.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-missing-disclosure.json @@ -28,7 +28,7 @@ ], "compatibility": { "processors": [ - "aces-reference-processor" + "raes-reference-processor" ] }, "realization_support": [ diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unguarded-feature-term.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unguarded-feature-term.json index cd0241b07..0fb54bcb7 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unguarded-feature-term.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unguarded-feature-term.json @@ -28,7 +28,7 @@ ], "compatibility": { "processors": [ - "aces-reference-processor" + "raes-reference-processor" ] }, "realization_support": [ diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unsupported-declared-feature.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unsupported-declared-feature.json index 485f3a493..c3f2b2d8c 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unsupported-declared-feature.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/feature-support-unsupported-declared-feature.json @@ -28,7 +28,7 @@ ], "compatibility": { "processors": [ - "aces-reference-processor" + "raes-reference-processor" ] }, "realization_support": [ diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-provisioner.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-provisioner.json index fee8d4974..4857cd9d1 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-provisioner.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-provisioner.json @@ -6,7 +6,7 @@ }, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": ["aces-reference-processor"] + "processors": ["raes-reference-processor"] }, "realization_support": [ { diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-realization-support.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-realization-support.json index 15ce78ac7..442cb07bd 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-realization-support.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/hollow-realization-support.json @@ -6,7 +6,7 @@ }, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": ["aces-reference-processor"] + "processors": ["raes-reference-processor"] }, "realization_support": [ { diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/invalid-binding-family.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/invalid-binding-family.json index fcb11dca0..e63da626d 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/invalid-binding-family.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/invalid-binding-family.json @@ -2,7 +2,7 @@ "schema_version": "backend-manifest/v2", "identity": {"name": "stub", "version": "0.2.0"}, "supported_contract_versions": ["backend-manifest-v2"], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-compatibility.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-compatibility.json index 244daad48..938a30f26 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-compatibility.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-compatibility.json @@ -6,7 +6,7 @@ }, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": "aces-reference-processor" + "processors": "raes-reference-processor" }, "concept_bindings": [ {"scope": "capabilities.provisioner.supported_node_types", "family": "assets"} diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-realization-support.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-realization-support.json index e837603dc..8190436c8 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-realization-support.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/malformed-realization-support.json @@ -6,7 +6,7 @@ }, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": ["aces-reference-processor"] + "processors": ["raes-reference-processor"] }, "realization_support": [ { diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-concept-bindings.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-concept-bindings.json index 8d5cda47b..bf1fc80df 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-concept-bindings.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-concept-bindings.json @@ -2,7 +2,7 @@ "schema_version": "backend-manifest/v2", "identity": {"name": "stub", "version": "0.2.0"}, "supported_contract_versions": ["backend-manifest-v2"], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-version.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-version.json index 96b142168..de0e1dd21 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-version.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/missing-version.json @@ -5,7 +5,7 @@ }, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": ["aces-reference-processor"] + "processors": ["raes-reference-processor"] }, "concept_bindings": [ {"scope": "capabilities.provisioner.supported_node_types", "family": "assets"} diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-backend-contract-version.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-backend-contract-version.json index 4ea06980f..d3bd225e0 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-backend-contract-version.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-backend-contract-version.json @@ -2,7 +2,7 @@ "schema_version": "backend-manifest/v2", "identity": {"name": "stub", "version": "0.2.0"}, "supported_contract_versions": ["semantic-profile-v1"], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-processor-compatibility-surface.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-processor-compatibility-surface.json index 76f5a3d9b..93dd355db 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-processor-compatibility-surface.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/non-processor-compatibility-surface.json @@ -3,7 +3,7 @@ "identity": {"name": "stub", "version": "0.2.0"}, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": ["aces-reference-processor"], + "processors": ["raes-reference-processor"], "backends": ["peer-backend"] }, "realization_support": [ diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-feature.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-feature.json index e1c53ce5a..e3d1dda71 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-feature.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-feature.json @@ -6,7 +6,7 @@ }, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": ["aces-reference-processor"] + "processors": ["raes-reference-processor"] }, "concept_bindings": [ {"scope": "capabilities.provisioner.supported_node_types", "family": "assets"} diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-state-predicate.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-state-predicate.json index 766a3f135..7dbe08928 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-state-predicate.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/invalid/unknown-workflow-state-predicate.json @@ -6,7 +6,7 @@ }, "supported_contract_versions": ["backend-manifest-v2"], "compatibility": { - "processors": ["aces-reference-processor"] + "processors": ["raes-reference-processor"] }, "concept_bindings": [ {"scope": "capabilities.provisioner.supported_node_types", "family": "assets"} 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 f2ee0191f..4f73b7766 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 @@ -28,7 +28,7 @@ ], "compatibility": { "processors": [ - "aces-reference-processor" + "raes-reference-processor" ] }, "realization_support": [ 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 21cc7cbb6..68ecfb390 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -201,7 +201,7 @@ }, "compatibility": { "processors": [ - "aces-reference-processor" + "raes-reference-processor" ] }, "concept_bindings": [ 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 efc61063d..0db9ff993 100644 --- a/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json @@ -1,6 +1,6 @@ { "schema_version": "behavioral-relations/v1", - "taxonomy_id": "aces-behavioral-relations", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "bibliography": [ { diff --git a/contracts/fixtures/concept-authority/concept-families-v1/invalid/native-with-authority.json b/contracts/fixtures/concept-authority/concept-families-v1/invalid/native-with-authority.json index 8b40e4a1d..ff03553ec 100644 --- a/contracts/fixtures/concept-authority/concept-families-v1/invalid/native-with-authority.json +++ b/contracts/fixtures/concept-authority/concept-families-v1/invalid/native-with-authority.json @@ -5,8 +5,8 @@ "title": "Scenarios", "description": "SDL scenarios, compositions, modules, and authoring constructs.", "provenance": "native", - "authority": "ACES", - "authority_reference": "https://aces-framework.org/concepts", + "authority": "RAES", + "authority_reference": "https://raes.dev/concepts", "extension_scope": "SDL-native scenario authoring constructs.", "relation_rules": [ "Must remain the scenario authoring layer." diff --git a/contracts/fixtures/concept-authority/concept-families-v1/valid/reference.json b/contracts/fixtures/concept-authority/concept-families-v1/valid/reference.json index c60aaf1a8..95be2a299 100644 --- a/contracts/fixtures/concept-authority/concept-families-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/concept-families-v1/valid/reference.json @@ -61,7 +61,7 @@ "title": "Tasks, Runs, and Studies", "description": "Execution lifecycle, run records, and study organization.", "provenance": "native", - "extension_scope": "ACES experiment workflow concepts for task framing, run lifecycle, study grouping, and analysis organization.", + "extension_scope": "RAES experiment workflow concepts for task framing, run lifecycle, study grouping, and analysis organization.", "relation_rules": [ "May reference scenarios as the authoring input under evaluation and provenance-and-evidence concepts as recorded outputs.", "Must describe experiment organization and lifecycle rather than the cyber-domain assets or actions inside a scenario." @@ -75,7 +75,7 @@ "title": "Episodes", "description": "Participant runtime episode identity, lifecycle state, and history boundaries.", "provenance": "native", - "extension_scope": "ACES participant runtime episode identity, lifecycle state, state/history contracts, reset/restart/terminate semantics, and participant-scoped behavior sequencing.", + "extension_scope": "RAES participant runtime episode identity, lifecycle state, state/history contracts, reset/restart/terminate semantics, and participant-scoped behavior sequencing.", "relation_rules": [ "May relate to tasks-runs-studies as the participant-level runtime segment that occurs within a task, run, or study without becoming that task, run, or study.", "May relate to scenarios as the runtime execution instance of a participant admitted from scenario context, while scenario authoring remains governed by scenarios.", @@ -91,7 +91,7 @@ "title": "Runtime Inventory", "description": "Observed and declared runtime configuration state attached to scenario nodes.", "provenance": "native", - "extension_scope": "ACES observed and declared runtime inventory of scenario node state under nodes.*.runtime: services, platforms, packages and software components, controls and security posture, filesystem and mounts, processes and scheduled jobs, and other node-scoped apparatus-state facts.", + "extension_scope": "RAES observed and declared runtime inventory of scenario node state under nodes.*.runtime: services, platforms, packages and software components, controls and security posture, filesystem and mounts, processes and scheduled jobs, and other node-scoped apparatus-state facts.", "relation_rules": [ "May relate to assets as the observed runtime configuration state of an asset-bearing scenario node without becoming the node or its asset identity.", "May relate to observables when an inventory fact is surfaced as a condition, metric, or telemetry signal, while the structured inventory record itself remains runtime-inventory.", @@ -107,7 +107,7 @@ "title": "Apparatus Declarations", "description": "Processor, backend, and participant-implementation manifests.", "provenance": "native", - "extension_scope": "ACES apparatus identity, capability, compatibility, and declaration surfaces for processors, backends, and participant implementations.", + "extension_scope": "RAES apparatus identity, capability, compatibility, and declaration surfaces for processors, backends, and participant implementations.", "relation_rules": [ "May bind declared capability vocabularies to adopted or adapted cyber-domain families through concept_bindings.", "Must keep apparatus identity and compatibility separate from the cyber-domain objects that an apparatus can process or realize." @@ -121,7 +121,7 @@ "title": "Realization and Disclosure", "description": "Instantiation, planning, compilation, and realization artifacts.", "provenance": "native", - "extension_scope": "ACES realization semantics for turning underspecified authoring inputs into executable, planned, or disclosed artifacts.", + "extension_scope": "RAES realization semantics for turning underspecified authoring inputs into executable, planned, or disclosed artifacts.", "relation_rules": [ "May relate scenarios to apparatus-declarations by describing what an apparatus can realize or disclose.", "Must distinguish realization support from the cyber-domain objects being realized." @@ -135,7 +135,7 @@ "title": "Provenance and Evidence", "description": "Run provenance records, evidence expectations, and audit artifacts.", "provenance": "native", - "extension_scope": "ACES provenance, evidence expectation, evidence collection, and audit-record semantics for experiments and execution.", + "extension_scope": "RAES provenance, evidence expectation, evidence collection, and audit-record semantics for experiments and execution.", "relation_rules": [ "May record observations about scenarios, tasks, runs, studies, apparatus, and cyber-domain objects without redefining those objects.", "Must separate evidence/provenance records from the observed asset, identity, observable, action, relationship, or artifact." @@ -149,14 +149,14 @@ "title": "Time and Apparatus", "description": "Clocks, timing constraints, and apparatus-level concerns.", "provenance": "native", - "extension_scope": "ACES timing, clock, scheduling, synchronization, and apparatus-time semantics that govern execution rather than cyber-domain object identity.", + "extension_scope": "RAES timing, clock, scheduling, synchronization, and apparatus-time semantics that govern execution rather than cyber-domain object identity.", "relation_rules": [ "May constrain scenarios, tasks, runs, realization, and apparatus behavior through time or clock declarations.", "Must keep timing and clock semantics separate from the cyber-domain event or action whose occurrence is being timed." ], "non_ambiguity_constraints": [ "Must not be used as a synonym for actions-and-events or observables.", - "Temporal fields that describe an event meaning must bind to actions-and-events or observables; fields that describe ACES clock policy may bind here." + "Temporal fields that describe an event meaning must bind to actions-and-events or observables; fields that describe RAES clock policy may bind here." ] } } 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 24ca22534..b6d73a4c5 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -146,7 +146,7 @@ }, "participant-defensive-behavior-activities": { "title": "Participant Defensive Behavior Activities", - "description": "ACES adaptations of NIST CSF 2.0 Detect, Respond, and Recover categories as governed defensive behavior classifications for behavior specifications.", + "description": "RAES adaptations of NIST CSF 2.0 Detect, Respond, and Recover categories as governed defensive behavior classifications for behavior specifications.", "source": { "provenance": "adapted", "authority": "NIST Cybersecurity Framework", @@ -673,11 +673,11 @@ }, "adapted": { "title": "Adapted", - "description": "Concept family adapts an external authority for ACES use." + "description": "Concept family adapts an external authority for RAES use." }, "native": { "title": "Native", - "description": "Concept family is native to ACES." + "description": "Concept family is native to RAES." } } }, @@ -933,7 +933,7 @@ }, "autonomous_execution": { "title": "Autonomous Execution", - "description": "Backend supports ACES-clock-governed autonomous execution of ordinary participant actions." + "description": "Backend supports RAES-clock-governed autonomous execution of ordinary participant actions." }, "attribution_support": { "title": "Attribution Support", diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/adapted-without-divergence.json b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/adapted-without-divergence.json index 01fa8c5c3..32fd9b4d5 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/adapted-without-divergence.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/adapted-without-divergence.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,7 +85,7 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [] diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/empty-divergence.json b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/empty-divergence.json index 8ed376b74..a11661102 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/empty-divergence.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/empty-divergence.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,7 +85,7 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/iri-mismatch.json b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/iri-mismatch.json index bc695b48c..a65989357 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/iri-mismatch.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/iri-mismatch.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,13 +85,13 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ - "ACES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", - "ACES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", - "ACES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." + "RAES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", + "RAES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", + "RAES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." ] }, "observables": { diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/missing-family.json b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/missing-family.json index 6fe752d21..715762e5c 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/missing-family.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/missing-family.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,13 +85,13 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ - "ACES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", - "ACES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", - "ACES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." + "RAES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", + "RAES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", + "RAES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." ] }, "observables": { diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/native-family.json b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/native-family.json index 909ea4219..8f9e53ae7 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/native-family.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/native-family.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,13 +85,13 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ - "ACES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", - "ACES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", - "ACES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." + "RAES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", + "RAES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", + "RAES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." ] }, "observables": { diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/provenance-mismatch.json b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/provenance-mismatch.json index b714afe72..ff4b499dd 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/provenance-mismatch.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/provenance-mismatch.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -87,13 +87,13 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ - "ACES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", - "ACES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", - "ACES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." + "RAES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", + "RAES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", + "RAES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." ] }, "observables": { diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/unknown-family.json b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/unknown-family.json index a7c0a76f1..3fa30a748 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/unknown-family.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/invalid/unknown-family.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,13 +85,13 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ - "ACES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", - "ACES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", - "ACES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." + "RAES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", + "RAES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", + "RAES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." ] }, "observables": { diff --git a/contracts/fixtures/concept-authority/uco-alignment-v1/valid/reference.json b/contracts/fixtures/concept-authority/uco-alignment-v1/valid/reference.json index 3426debb3..c40b4b810 100644 --- a/contracts/fixtures/concept-authority/uco-alignment-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/uco-alignment-v1/valid/reference.json @@ -11,7 +11,7 @@ { "uco_class": "observable:ObservableObject", "iri": "https://ontology.unifiedcyberontology.org/uco/observable/ObservableObject", - "note": "Base observable cyber object; ACES asset-bearing scenario nodes and deployable resources are represented as observable objects." + "note": "Base observable cyber object; RAES asset-bearing scenario nodes and deployable resources are represented as observable objects." }, { "uco_class": "observable:Computer", @@ -85,13 +85,13 @@ { "uco_class": "core:Relationship", "iri": "https://ontology.unifiedcyberontology.org/uco/core/Relationship", - "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for ACES typed scenario relationships." + "note": "UCO directed/typed assertion that objects relate to one another; the closest UCO anchor for RAES typed scenario relationships." } ], "divergences": [ - "ACES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", - "ACES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", - "ACES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." + "RAES relationship `type` values derive from STIX 2.1 Relationship SRO vocabulary and OCR dependency patterns, not from the UCO core:kindOfRelationship controlled vocabulary.", + "RAES relationships carry an open `properties` map instead of UCO facet-based characterization; relationship attributes are not modeled as UCO Facets.", + "RAES relationship source and target reference SDL scenario elements (nodes, accounts, content, conditions, events), not exclusively UCO observable objects." ] }, "observables": { diff --git a/contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json index 6ede01a5b..929636c21 100644 --- a/contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json @@ -7,16 +7,16 @@ "processor": { "component_kind": "processor", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.1.0" }, "manifest_ref": { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -62,11 +62,11 @@ "selected_manifests": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/invalid-generated-at.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/invalid-generated-at.json index a378ddb93..560be5a0f 100644 --- a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/invalid-generated-at.json +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/invalid-generated-at.json @@ -38,7 +38,7 @@ "provenance_refs": [ { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } ] diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/non-reported-with-value.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/non-reported-with-value.json index 348e6db7a..e9874f5ee 100644 --- a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/non-reported-with-value.json +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/non-reported-with-value.json @@ -38,7 +38,7 @@ "provenance_refs": [ { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } ] diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/reported-without-value.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/reported-without-value.json index 102288713..46965ca0d 100644 --- a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/reported-without-value.json +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/reported-without-value.json @@ -37,7 +37,7 @@ "provenance_refs": [ { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } ] diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json index 81115f617..96a3298a9 100644 --- a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json @@ -38,7 +38,7 @@ "provenance_refs": [ { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } ] diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/augmentation-without-affected-refs.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/augmentation-without-affected-refs.json index 200b4603d..de4c7cbd0 100644 --- a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/augmentation-without-affected-refs.json +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/augmentation-without-affected-refs.json @@ -21,16 +21,16 @@ "processor": { "component_kind": "processor", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.1.0" }, "manifest_ref": { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -58,11 +58,11 @@ "selected_manifests": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-authority-mismatch.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-authority-mismatch.json index c14338bf8..d0cc29767 100644 --- a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-authority-mismatch.json +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-authority-mismatch.json @@ -22,16 +22,16 @@ "processor": { "component_kind": "processor", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.1.0" }, "manifest_ref": { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -77,11 +77,11 @@ "selected_manifests": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -214,7 +214,7 @@ } } ], - "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "processor_manifest_ref": "processor-manifest-v2:raes-reference-processor", "backend_manifest_ref": "backend-manifest-v2:stub-backend" }, "parameter_set": [ @@ -279,7 +279,7 @@ "basis": "backend-realized", "realized_by_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" }, "authored_ref": { diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-missing-target.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-missing-target.json index 6f48cbeb1..f0c116f8e 100644 --- a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-missing-target.json +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-missing-target.json @@ -22,16 +22,16 @@ "processor": { "component_kind": "processor", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.1.0" }, "manifest_ref": { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -77,11 +77,11 @@ "selected_manifests": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -214,7 +214,7 @@ } } ], - "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "processor_manifest_ref": "processor-manifest-v2:raes-reference-processor", "backend_manifest_ref": "backend-manifest-v2:stub-backend" }, "parameter_set": [ @@ -279,7 +279,7 @@ "basis": "processor-realized", "realized_by_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" }, "authored_ref": { diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-processor-authority-mismatch.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-processor-authority-mismatch.json index 0fecb50c9..9d5d9b00d 100644 --- a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-processor-authority-mismatch.json +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-processor-authority-mismatch.json @@ -22,16 +22,16 @@ "processor": { "component_kind": "processor", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.1.0" }, "manifest_ref": { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -77,11 +77,11 @@ "selected_manifests": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -214,7 +214,7 @@ } } ], - "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "processor_manifest_ref": "processor-manifest-v2:raes-reference-processor", "backend_manifest_ref": "backend-manifest-v2:stub-backend" }, "parameter_set": [ @@ -279,7 +279,7 @@ "basis": "processor-realized", "realized_by_ref": { "ref_kind": "backend", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" }, "authored_ref": { diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/traceability-empty-capture-specs.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/traceability-empty-capture-specs.json index e53297caa..adf71994a 100644 --- a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/traceability-empty-capture-specs.json +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/traceability-empty-capture-specs.json @@ -22,16 +22,16 @@ "processor": { "component_kind": "processor", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.1.0" }, "manifest_ref": { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -77,11 +77,11 @@ "selected_manifests": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -214,7 +214,7 @@ } } ], - "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "processor_manifest_ref": "processor-manifest-v2:raes-reference-processor", "backend_manifest_ref": "backend-manifest-v2:stub-backend" }, "parameter_set": [ @@ -273,7 +273,7 @@ "basis": "processor-realized", "realized_by_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" }, "authored_ref": { diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json index d2897fe27..217d6bca7 100644 --- a/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json @@ -22,16 +22,16 @@ "processor": { "component_kind": "processor", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.1.0" }, "manifest_ref": { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -77,11 +77,11 @@ "selected_manifests": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, @@ -214,7 +214,7 @@ } } ], - "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "processor_manifest_ref": "processor-manifest-v2:raes-reference-processor", "backend_manifest_ref": "backend-manifest-v2:stub-backend" }, "parameter_set": [ @@ -279,7 +279,7 @@ "basis": "processor-realized", "realized_by_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" }, "authored_ref": { diff --git a/contracts/fixtures/experiment-core/experiment-study-v1/invalid/empty-membership.json b/contracts/fixtures/experiment-core/experiment-study-v1/invalid/empty-membership.json index 482ae456e..3a78a28f1 100644 --- a/contracts/fixtures/experiment-core/experiment-study-v1/invalid/empty-membership.json +++ b/contracts/fixtures/experiment-core/experiment-study-v1/invalid/empty-membership.json @@ -4,7 +4,7 @@ "study_version": "1.0.0", "study_kind": "collection", "title": "Bad study", - "owner": "aces-experiment-core", + "owner": "raes-experiment-core", "description": "Invalid because a study must contain at least one typed member.", "purpose": "Invalid fixture.", "membership": {}, diff --git a/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json index 247de33ab..e3fef3702 100644 --- a/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json @@ -4,7 +4,7 @@ "study_version": "1.0.0", "study_kind": "study", "title": "TechVault baseline study", - "owner": "aces-experiment-core", + "owner": "raes-experiment-core", "description": "Archival baseline study for TechVault red-team runs.", "purpose": "Estimate baseline participant policy outcomes for the fixed task protocol.", "research_questions": [ @@ -12,7 +12,7 @@ ], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "empirical-adequacy", "subject": "TechVault baseline study", diff --git a/contracts/fixtures/experiment-core/experiment-task-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-task-v1/valid/reference.json index 8998737a5..735b7d34e 100644 --- a/contracts/fixtures/experiment-core/experiment-task-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-task-v1/valid/reference.json @@ -56,7 +56,7 @@ "allowed_processor_refs": [ { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } ], @@ -70,11 +70,11 @@ "required_manifest_refs": [ { "ref_kind": "manifest", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2", "subject_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0" } }, diff --git a/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/invalid/cross-outcome-payload.json b/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/invalid/cross-outcome-payload.json index e4ecd6868..98524285a 100644 --- a/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/invalid/cross-outcome-payload.json +++ b/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/invalid/cross-outcome-payload.json @@ -1,52 +1,52 @@ { - "analysis_profile": "aces-exploit-path-analysis-v1", + "analysis_profile": "raes-exploit-path-analysis-v1", "authored_digest": { "algorithm": "sha256", - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "value": "sha256:1dd6f51fcf01986275edb0c8fa6f75d163a9e5e097c6729d1f5447ec03cbfc10" }, "diagnostics": [], "failure": null, "normalized_graph": { - "binding_profile": "aces-sdl-snapshot-attack-binding/v1", + "binding_profile": "raes-sdl-snapshot-attack-binding/v1", "bindings": [ { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-attacker-foothold", "concept_kind": "scenario-node", "target_id": "attacker-foothold", "target_kind": "state-fact" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-data-access", "concept_kind": "scenario-node", "target_id": "data-access", "target_kind": "state-fact" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-exploit-web", "concept_kind": "participant-action", "target_id": "exploit-web", "target_kind": "transition" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-read-data", "concept_kind": "participant-action", "target_id": "read-data", "target_kind": "transition" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-web-shell", "concept_kind": "scenario-node", "target_id": "web-shell", "target_kind": "state-fact" } ], - "profile": "aces-attack-graph/v1", + "profile": "raes-attack-graph/v1", "snapshot_digest": "sha256:4d49022a2c6f04058552ea9ef79b05a26b2ee3ffc3a91f85d977b5b0d6e3127b", "state_facts": [ { @@ -68,7 +68,7 @@ "subject": "attacker" } ], - "transition_semantics_profile": "aces-monotonic-attack-transition/v1", + "transition_semantics_profile": "raes-monotonic-attack-transition/v1", "transitions": [ { "effects": [ @@ -102,7 +102,7 @@ } ] }, - "normalized_graph_digest": "sha256:8d5735dbed3247af815b54f8f763c59ee2c0b1823361d121ee6e8ff53cbd54d0", + "normalized_graph_digest": "sha256:bf22cd24bdb751ae3b2657243274a9ddf96691eed4cc43ce26dee483a25bdeb2", "outcome": "invalid-path", "profile": "exploit-path-analysis-evidence/v1", "query": { @@ -115,21 +115,21 @@ ], "max_depth": 4, "participant_perspective": "attacker", - "profile": "aces-exploit-path-query/v1", + "profile": "raes-exploit-path-query/v1", "query_id": "attacker-data-path", "start_facts": [ "attacker-foothold" ] }, - "query_digest": "sha256:a9483aa194eed2ca368b372866d437de88262f126f77f58c9db67a650e4eeac3", + "query_digest": "sha256:68eb8c83b4d8805287159376f7a272ecbbe9dc31a6903805a22ae8ef01f5179a", "search_configuration": { "goal_check": "before-and-after-transition", "max_depth": 4, - "profile": "aces-deterministic-attack-graph-search/v1", + "profile": "raes-deterministic-attack-graph-search/v1", "strategy": "breadth-first-canonical", "transition_semantics": "monotonic-additive/v1" }, - "search_configuration_digest": "sha256:fca04e68d5277c7d4b2e939bee3e0d43f607cccc4e63bddfce2bcb0977626a86", + "search_configuration_digest": "sha256:7f8876d81feb77d3a3239be2fb8337de8885e2744f8786728ba23e4e6027bc0a", "snapshot_digest": "sha256:4d49022a2c6f04058552ea9ef79b05a26b2ee3ffc3a91f85d977b5b0d6e3127b", "source": { "byte_digest": "sha256:0f7e0fb521139d81f28dcbf9d6423f5f194487d8cbe36679d200c6a34199397b", @@ -149,7 +149,7 @@ "initial_state": [ "attacker-foothold" ], - "profile": "aces-exploit-path-witness/v1", + "profile": "raes-exploit-path-witness/v1", "steps": [ { "applied_effects": [ diff --git a/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/valid/valid-path.json b/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/valid/valid-path.json index 4b64d6d35..6263f8dfd 100644 --- a/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/valid/valid-path.json +++ b/contracts/fixtures/exploit-path-analysis/exploit-path-analysis-evidence-v1/valid/valid-path.json @@ -1,52 +1,52 @@ { - "analysis_profile": "aces-exploit-path-analysis-v1", + "analysis_profile": "raes-exploit-path-analysis-v1", "authored_digest": { "algorithm": "sha256", - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "value": "sha256:1dd6f51fcf01986275edb0c8fa6f75d163a9e5e097c6729d1f5447ec03cbfc10" }, "diagnostics": [], "failure": null, "normalized_graph": { - "binding_profile": "aces-sdl-snapshot-attack-binding/v1", + "binding_profile": "raes-sdl-snapshot-attack-binding/v1", "bindings": [ { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-attacker-foothold", "concept_kind": "scenario-node", "target_id": "attacker-foothold", "target_kind": "state-fact" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-data-access", "concept_kind": "scenario-node", "target_id": "data-access", "target_kind": "state-fact" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-exploit-web", "concept_kind": "participant-action", "target_id": "exploit-web", "target_kind": "transition" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-read-data", "concept_kind": "participant-action", "target_id": "read-data", "target_kind": "transition" }, { - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "binding_id": "bind-web-shell", "concept_kind": "scenario-node", "target_id": "web-shell", "target_kind": "state-fact" } ], - "profile": "aces-attack-graph/v1", + "profile": "raes-attack-graph/v1", "snapshot_digest": "sha256:4d49022a2c6f04058552ea9ef79b05a26b2ee3ffc3a91f85d977b5b0d6e3127b", "state_facts": [ { @@ -68,7 +68,7 @@ "subject": "attacker" } ], - "transition_semantics_profile": "aces-monotonic-attack-transition/v1", + "transition_semantics_profile": "raes-monotonic-attack-transition/v1", "transitions": [ { "effects": [ @@ -102,7 +102,7 @@ } ] }, - "normalized_graph_digest": "sha256:8d5735dbed3247af815b54f8f763c59ee2c0b1823361d121ee6e8ff53cbd54d0", + "normalized_graph_digest": "sha256:bf22cd24bdb751ae3b2657243274a9ddf96691eed4cc43ce26dee483a25bdeb2", "outcome": "valid-path", "profile": "exploit-path-analysis-evidence/v1", "query": { @@ -115,21 +115,21 @@ ], "max_depth": 4, "participant_perspective": "attacker", - "profile": "aces-exploit-path-query/v1", + "profile": "raes-exploit-path-query/v1", "query_id": "attacker-data-path", "start_facts": [ "attacker-foothold" ] }, - "query_digest": "sha256:a9483aa194eed2ca368b372866d437de88262f126f77f58c9db67a650e4eeac3", + "query_digest": "sha256:68eb8c83b4d8805287159376f7a272ecbbe9dc31a6903805a22ae8ef01f5179a", "search_configuration": { "goal_check": "before-and-after-transition", "max_depth": 4, - "profile": "aces-deterministic-attack-graph-search/v1", + "profile": "raes-deterministic-attack-graph-search/v1", "strategy": "breadth-first-canonical", "transition_semantics": "monotonic-additive/v1" }, - "search_configuration_digest": "sha256:fca04e68d5277c7d4b2e939bee3e0d43f607cccc4e63bddfce2bcb0977626a86", + "search_configuration_digest": "sha256:7f8876d81feb77d3a3239be2fb8337de8885e2744f8786728ba23e4e6027bc0a", "snapshot_digest": "sha256:4d49022a2c6f04058552ea9ef79b05a26b2ee3ffc3a91f85d977b5b0d6e3127b", "source": { "byte_digest": "sha256:0f7e0fb521139d81f28dcbf9d6423f5f194487d8cbe36679d200c6a34199397b", @@ -149,7 +149,7 @@ "initial_state": [ "attacker-foothold" ], - "profile": "aces-exploit-path-witness/v1", + "profile": "raes-exploit-path-witness/v1", "steps": [ { "applied_effects": [ diff --git a/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json b/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json index 0b70db930..3e8cac9e3 100644 --- a/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json +++ b/contracts/fixtures/participant-implementation-manifest/participant-implementation-manifest-v1/valid/reference.json @@ -19,7 +19,7 @@ "stub-participant-runtime" ], "processors": [ - "aces-reference-processor" + "raes-reference-processor" ], "backends": [ "stub" diff --git a/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json b/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json index f9c4c26b1..f694fe1da 100644 --- a/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json +++ b/contracts/fixtures/participant-implementation-provenance/participant-implementation-provenance-v1/valid/reference.json @@ -44,7 +44,7 @@ } } ], - "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "processor_manifest_ref": "processor-manifest-v2:raes-reference-processor", "backend_manifest_ref": "backend-manifest-v2:stub", "metadata": { "apparatus_record": "participant-implementation-selection" diff --git a/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/invalid/unknown-phase.json b/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/invalid/unknown-phase.json index b0c730ecf..60174fa4a 100644 --- a/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/invalid/unknown-phase.json +++ b/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/invalid/unknown-phase.json @@ -1,6 +1,6 @@ { "event_id": "evt-llm-17-exec", - "schema_name": "aces.participant_runtime.lifecycle", + "schema_name": "raes.participant_runtime.lifecycle", "schema_version": "1.0.0", "event_type": "execution_attempt", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "tool_call_completed", "status_detail": "tool gateway accepted and completed the command", "source_status_label": "tool_call_completed", - "source_status_mapping": "aces.lifecycle.operation_state.completed" + "source_status_mapping": "raes.lifecycle.operation_state.completed" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-execution-attempt.json b/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-execution-attempt.json index d65a5205b..3659b128b 100644 --- a/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-execution-attempt.json +++ b/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-execution-attempt.json @@ -1,6 +1,6 @@ { "event_id": "evt-llm-17-exec", - "schema_name": "aces.participant_runtime.lifecycle", + "schema_name": "raes.participant_runtime.lifecycle", "schema_version": "1.0.0", "event_type": "execution_attempt", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "tool_call_completed", "status_detail": "tool gateway accepted and completed the command", "source_status_label": "tool_call_completed", - "source_status_mapping": "aces.lifecycle.operation_state.completed" + "source_status_mapping": "raes.lifecycle.operation_state.completed" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-selection.json b/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-selection.json index 7ae5cf1e8..7afff94e0 100644 --- a/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-selection.json +++ b/contracts/fixtures/participant-runtime/participant-lifecycle-event-v1/valid/opaque-llm-selection.json @@ -1,6 +1,6 @@ { "event_id": "evt-llm-16-selection", - "schema_name": "aces.participant_runtime.lifecycle", + "schema_name": "raes.participant_runtime.lifecycle", "schema_version": "1.0.0", "event_type": "selection_or_admission", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "model_private_choice", "status_detail": "selection existed inside opaque model apparatus", "source_status_label": "model_private_choice", - "source_status_mapping": "aces.lifecycle.phase_realization.opaque" + "source_status_mapping": "raes.lifecycle.phase_realization.opaque" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/invalid/missing-visibility-projection.json b/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/invalid/missing-visibility-projection.json index c06be7e7b..3513758c8 100644 --- a/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/invalid/missing-visibility-projection.json +++ b/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/invalid/missing-visibility-projection.json @@ -1,6 +1,6 @@ { "event_id": "obs-blue-43", - "schema_name": "aces.participant_runtime.observation", + "schema_name": "raes.participant_runtime.observation", "schema_version": "1.0.0", "event_type": "observation_emission", "extension_policy": "reject_unknown_required", @@ -22,7 +22,7 @@ "status_code": "observation_emitted", "status_detail": "simulator emitted the blue local telemetry observation", "source_status_label": "cyborg_observation", - "source_status_mapping": "aces.observation.emitted" + "source_status_mapping": "raes.observation.emitted" }, "participant_address": "participants.blue.rl", "episode_id": "ep-blue-002", diff --git a/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/valid/rl-observation-only.json b/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/valid/rl-observation-only.json index 2264656fc..113c1d2ca 100644 --- a/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/valid/rl-observation-only.json +++ b/contracts/fixtures/participant-runtime/participant-observation-envelope-v1/valid/rl-observation-only.json @@ -1,6 +1,6 @@ { "event_id": "obs-blue-43", - "schema_name": "aces.participant_runtime.observation", + "schema_name": "raes.participant_runtime.observation", "schema_version": "1.0.0", "event_type": "observation_emission", "extension_policy": "reject_unknown_required", @@ -22,7 +22,7 @@ "status_code": "observation_emitted", "status_detail": "simulator emitted the blue local telemetry observation", "source_status_label": "cyborg_observation", - "source_status_mapping": "aces.observation.emitted" + "source_status_mapping": "raes.observation.emitted" }, "participant_address": "participants.blue.rl", "episode_id": "ep-blue-002", diff --git a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-outcome-sources.json b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-outcome-sources.json index cd4021067..9bf65e53c 100644 --- a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-outcome-sources.json +++ b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-outcome-sources.json @@ -1,6 +1,6 @@ { "event_id": "outcome-red-17", - "schema_name": "aces.participant_runtime.outcome_report", + "schema_name": "raes.participant_runtime.outcome_report", "schema_version": "1.0.0", "event_type": "outcome_report", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "outcome_interpreted", "status_detail": "interpretation rule grounded the outcome in recorded sources", "source_status_label": "outcome_interpreted", - "source_status_mapping": "aces.outcome.interpreted" + "source_status_mapping": "raes.outcome.interpreted" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-state-relationships.json b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-state-relationships.json index d954303f8..498e00d06 100644 --- a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-state-relationships.json +++ b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-state-relationships.json @@ -1,6 +1,6 @@ { "event_id": "outcome-red-17", - "schema_name": "aces.participant_runtime.outcome_report", + "schema_name": "raes.participant_runtime.outcome_report", "schema_version": "1.0.0", "event_type": "outcome_report", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "outcome_interpreted", "status_detail": "interpretation rule grounded the outcome in recorded sources", "source_status_label": "outcome_interpreted", - "source_status_mapping": "aces.outcome.interpreted" + "source_status_mapping": "raes.outcome.interpreted" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/inline-score-field.json b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/inline-score-field.json index aeec88472..3300174e4 100644 --- a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/inline-score-field.json +++ b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/inline-score-field.json @@ -1,6 +1,6 @@ { "event_id": "outcome-red-17", - "schema_name": "aces.participant_runtime.outcome_report", + "schema_name": "raes.participant_runtime.outcome_report", "schema_version": "1.0.0", "event_type": "outcome_report", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "outcome_interpreted", "status_detail": "interpretation rule grounded the outcome in recorded sources", "source_status_label": "outcome_interpreted", - "source_status_mapping": "aces.outcome.interpreted" + "source_status_mapping": "raes.outcome.interpreted" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/valid/exfiltration-outcome.json b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/valid/exfiltration-outcome.json index aaee4a487..36b775c03 100644 --- a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/valid/exfiltration-outcome.json +++ b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/valid/exfiltration-outcome.json @@ -1,6 +1,6 @@ { "event_id": "outcome-red-17", - "schema_name": "aces.participant_runtime.outcome_report", + "schema_name": "raes.participant_runtime.outcome_report", "schema_version": "1.0.0", "event_type": "outcome_report", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "outcome_interpreted", "status_detail": "interpretation rule grounded the outcome in recorded sources", "source_status_label": "outcome_interpreted", - "source_status_mapping": "aces.outcome.interpreted" + "source_status_mapping": "raes.outcome.interpreted" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/invalid/unknown-access-kind.json b/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/invalid/unknown-access-kind.json index 459fd2679..5956abdfb 100644 --- a/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/invalid/unknown-access-kind.json +++ b/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/invalid/unknown-access-kind.json @@ -1,6 +1,6 @@ { "event_id": "state-web01-http-rev8", - "schema_name": "aces.participant_runtime.shared_state", + "schema_name": "raes.participant_runtime.shared_state", "schema_version": "1.0.0", "event_type": "state_update_commit", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "state_committed", "status_detail": "runtime committed the serialized shared-state write", "source_status_label": "state_committed", - "source_status_mapping": "aces.shared_state.commit" + "source_status_mapping": "raes.shared_state.commit" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/valid/serialized-service-state-commit.json b/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/valid/serialized-service-state-commit.json index 826e6252e..af7bc2802 100644 --- a/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/valid/serialized-service-state-commit.json +++ b/contracts/fixtures/participant-runtime/participant-shared-state-record-v1/valid/serialized-service-state-commit.json @@ -1,6 +1,6 @@ { "event_id": "state-web01-http-rev8", - "schema_name": "aces.participant_runtime.shared_state", + "schema_name": "raes.participant_runtime.shared_state", "schema_version": "1.0.0", "event_type": "state_update_commit", "extension_policy": "reject_unknown_required", @@ -11,7 +11,7 @@ "status_code": "state_committed", "status_detail": "runtime committed the serialized shared-state write", "source_status_label": "state_committed", - "source_status_mapping": "aces.shared_state.commit" + "source_status_mapping": "raes.shared_state.commit" }, "participant_address": "participants.red.llm", "episode_id": "ep-red-004", diff --git a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/empty-compatibility.json b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/empty-compatibility.json index e7a2c74f6..c380bc9c3 100644 --- a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/empty-compatibility.json +++ b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/empty-compatibility.json @@ -1,7 +1,7 @@ { "schema_version": "processor-manifest/v2", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.2.0" }, "supported_contract_versions": ["processor-manifest-v2"], diff --git a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-capabilities.json b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-capabilities.json index 1b8566bcd..03a9f5f87 100644 --- a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-capabilities.json +++ b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-capabilities.json @@ -1,7 +1,7 @@ { "schema_version": "processor-manifest/v2", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.2.0" }, "supported_contract_versions": ["processor-manifest-v2"], diff --git a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-realization-support.json b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-realization-support.json index 44db20fe7..d43b832e8 100644 --- a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-realization-support.json +++ b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/hollow-realization-support.json @@ -1,7 +1,7 @@ { "schema_version": "processor-manifest/v2", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.2.0" }, "supported_contract_versions": ["processor-manifest-v2"], diff --git a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/malformed-realization-support.json b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/malformed-realization-support.json index 81ae33dc1..8bef6d8c3 100644 --- a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/malformed-realization-support.json +++ b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/malformed-realization-support.json @@ -1,7 +1,7 @@ { "schema_version": "processor-manifest/v2", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.2.0" }, "supported_contract_versions": ["processor-manifest-v2"], diff --git a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/non-backend-compatibility-surface.json b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/non-backend-compatibility-surface.json index 66f871bf9..260f9f47a 100644 --- a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/non-backend-compatibility-surface.json +++ b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/non-backend-compatibility-surface.json @@ -1,7 +1,7 @@ { "schema_version": "processor-manifest/v2", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.2.0" }, "supported_contract_versions": ["processor-manifest-v2"], diff --git a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/unknown-feature.json b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/unknown-feature.json index f98cd3099..67c510db1 100644 --- a/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/unknown-feature.json +++ b/contracts/fixtures/processor-manifest/processor-manifest-v2/invalid/unknown-feature.json @@ -1,7 +1,7 @@ { "schema_version": "processor-manifest/v2", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.2.0" }, "supported_contract_versions": ["processor-manifest-v2"], diff --git a/contracts/fixtures/processor-manifest/processor-manifest-v2/valid/reference.json b/contracts/fixtures/processor-manifest/processor-manifest-v2/valid/reference.json index b5fcdafa4..fb995127e 100644 --- a/contracts/fixtures/processor-manifest/processor-manifest-v2/valid/reference.json +++ b/contracts/fixtures/processor-manifest/processor-manifest-v2/valid/reference.json @@ -1,7 +1,7 @@ { "schema_version": "processor-manifest/v2", "identity": { - "name": "aces-reference-processor", + "name": "raes-reference-processor", "version": "0.2.0" }, "supported_contract_versions": [ diff --git a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/bad-profile-id-pattern.json b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/bad-profile-id-pattern.json index 54fd036bd..a614aa842 100644 --- a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/bad-profile-id-pattern.json +++ b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/bad-profile-id-pattern.json @@ -17,7 +17,7 @@ }, "derivation": { "key_derivation_function": "blake3.derive_key", - "context_template": "aces-random-stream-v1|profile={profile_id}" + "context_template": "raes-random-stream-v1|profile={profile_id}" }, "block_encoding": { "block_bytes": 32, diff --git a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/empty-transforms.json b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/empty-transforms.json index e44957a58..65e94ee95 100644 --- a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/empty-transforms.json +++ b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/empty-transforms.json @@ -17,7 +17,7 @@ }, "derivation": { "key_derivation_function": "blake3.derive_key", - "context_template": "aces-random-stream-v1|profile={profile_id}" + "context_template": "raes-random-stream-v1|profile={profile_id}" }, "block_encoding": { "block_bytes": 32, diff --git a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/missing-generator.json b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/missing-generator.json index 910972a39..8ead061b8 100644 --- a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/missing-generator.json +++ b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/missing-generator.json @@ -12,7 +12,7 @@ }, "derivation": { "key_derivation_function": "blake3.derive_key", - "context_template": "aces-random-stream-v1|profile={profile_id}" + "context_template": "raes-random-stream-v1|profile={profile_id}" }, "block_encoding": { "block_bytes": 32, diff --git a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/transform-key-mismatch.json b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/transform-key-mismatch.json index e4aa24013..fead8e4ca 100644 --- a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/transform-key-mismatch.json +++ b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/transform-key-mismatch.json @@ -17,7 +17,7 @@ }, "derivation": { "key_derivation_function": "blake3.derive_key", - "context_template": "aces-random-stream-v1|profile={profile_id}" + "context_template": "raes-random-stream-v1|profile={profile_id}" }, "block_encoding": { "block_bytes": 32, diff --git a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/unknown-field.json b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/unknown-field.json index 1b68d710d..cbfad41bb 100644 --- a/contracts/fixtures/profiles/random-stream-profile-v1/invalid/unknown-field.json +++ b/contracts/fixtures/profiles/random-stream-profile-v1/invalid/unknown-field.json @@ -17,7 +17,7 @@ }, "derivation": { "key_derivation_function": "blake3.derive_key", - "context_template": "aces-random-stream-v1|profile={profile_id}" + "context_template": "raes-random-stream-v1|profile={profile_id}" }, "block_encoding": { "block_bytes": 32, diff --git a/contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-v1.json b/contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-v1.json index 0ce19136e..a99ba55ef 100644 --- a/contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-v1.json +++ b/contracts/fixtures/profiles/random-stream-profile-v1/valid/blake3-xof-v1.json @@ -17,7 +17,7 @@ }, "derivation": { "key_derivation_function": "blake3.derive_key", - "context_template": "aces-random-stream-v1|profile={profile_id}" + "context_template": "raes-random-stream-v1|profile={profile_id}" }, "block_encoding": { "block_bytes": 32, diff --git a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json index d1a2251b0..243da1c20 100644 --- a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json +++ b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json @@ -18,7 +18,7 @@ "explicit_non_claims": ["No deployability or scientific-adequacy claim."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "semantic-validity", "subject": "One SDL source", diff --git a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/missing-required-gate.json b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/missing-required-gate.json index 5da2a73b8..6fc257d37 100644 --- a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/missing-required-gate.json +++ b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/missing-required-gate.json @@ -1,7 +1,7 @@ { "schema_version": "validation-basis-disclosure/v1", "disclosure": { - "profile_id": "aces-structural-validation", + "profile_id": "raes-structural-validation", "profile_version": "v1", "subject_kind": "scenario", "subject_ref": { diff --git a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/run-missing-ref-version.json b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/run-missing-ref-version.json index 58455b369..b5fffc027 100644 --- a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/run-missing-ref-version.json +++ b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/run-missing-ref-version.json @@ -1,7 +1,7 @@ { "schema_version": "validation-basis-disclosure/v1", "disclosure": { - "profile_id": "aces-evidence-backed-validation", + "profile_id": "raes-evidence-backed-validation", "profile_version": "v1", "subject_kind": "experiment_run", "subject_ref": { @@ -23,7 +23,7 @@ {"ref_kind": "evidence-record", "ref_id": "evidence-1"} ], "producer_refs": [ - {"ref_kind": "processor", "ref_id": "aces-reference-processor"} + {"ref_kind": "processor", "ref_id": "raes-reference-processor"} ], "recorded_at": "2026-07-24T00:00:00Z" } diff --git a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/scenario-snapshot-missing-digest.json b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/scenario-snapshot-missing-digest.json index 989dfe267..ed00ee898 100644 --- a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/scenario-snapshot-missing-digest.json +++ b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/scenario-snapshot-missing-digest.json @@ -1,7 +1,7 @@ { "schema_version": "validation-basis-disclosure/v1", "disclosure": { - "profile_id": "aces-semantic-validation", + "profile_id": "raes-semantic-validation", "profile_version": "v1", "subject_kind": "scenario_snapshot", "subject_ref": { diff --git a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/unknown-profile.json b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/unknown-profile.json index 16bc0238f..4e9d752fc 100644 --- a/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/unknown-profile.json +++ b/contracts/fixtures/profiles/validation-basis-disclosure-v1/invalid/unknown-profile.json @@ -1,7 +1,7 @@ { "schema_version": "validation-basis-disclosure/v1", "disclosure": { - "profile_id": "aces-missing-validation", + "profile_id": "raes-missing-validation", "profile_version": "v1", "subject_kind": "scenario", "subject_ref": { diff --git a/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/evidence-backed-run.json b/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/evidence-backed-run.json index 983dfdb86..0debbc40f 100644 --- a/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/evidence-backed-run.json +++ b/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/evidence-backed-run.json @@ -1,7 +1,7 @@ { "schema_version": "validation-basis-disclosure/v1", "disclosure": { - "profile_id": "aces-evidence-backed-validation", + "profile_id": "raes-evidence-backed-validation", "profile_version": "v1", "subject_kind": "experiment_run", "subject_ref": { @@ -24,7 +24,7 @@ {"ref_kind": "evidence-record", "ref_id": "evidence-1"} ], "producer_refs": [ - {"ref_kind": "processor", "ref_id": "aces-reference-processor"} + {"ref_kind": "processor", "ref_id": "raes-reference-processor"} ], "recorded_at": "2026-07-24T00:00:00Z" } diff --git a/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/scenario-snapshot.json b/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/scenario-snapshot.json index 5fcc88977..d8fae96b3 100644 --- a/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/scenario-snapshot.json +++ b/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/scenario-snapshot.json @@ -1,7 +1,7 @@ { "schema_version": "validation-basis-disclosure/v1", "disclosure": { - "profile_id": "aces-semantic-validation", + "profile_id": "raes-semantic-validation", "profile_version": "v1", "subject_kind": "scenario_snapshot", "subject_ref": { diff --git a/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/structural-scenario.json b/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/structural-scenario.json index 483119fa7..a208c023a 100644 --- a/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/structural-scenario.json +++ b/contracts/fixtures/profiles/validation-basis-disclosure-v1/valid/structural-scenario.json @@ -1,7 +1,7 @@ { "schema_version": "validation-basis-disclosure/v1", "disclosure": { - "profile_id": "aces-structural-validation", + "profile_id": "raes-structural-validation", "profile_version": "v1", "subject_kind": "scenario", "subject_ref": { diff --git a/contracts/fixtures/profiles/validation-profile-catalog-v1/invalid/dangling-reference.json b/contracts/fixtures/profiles/validation-profile-catalog-v1/invalid/dangling-reference.json index 37b2f0591..70decf3a4 100644 --- a/contracts/fixtures/profiles/validation-profile-catalog-v1/invalid/dangling-reference.json +++ b/contracts/fixtures/profiles/validation-profile-catalog-v1/invalid/dangling-reference.json @@ -1,6 +1,6 @@ { "schema_version": "validation-profile-catalog/v1", - "profile_family": "aces-validation", + "profile_family": "raes-validation", "catalog_version": "v1", "strengths": [ { @@ -29,9 +29,9 @@ ], "profiles": [ { - "profile_id": "aces-structural-validation", + "profile_id": "raes-structural-validation", "profile_version": "v1", - "title": "ACES structural validation", + "title": "RAES structural validation", "intended_subject_kinds": [ "scenario" ], diff --git a/contracts/fixtures/profiles/validation-profile-catalog-v1/valid/minimal.json b/contracts/fixtures/profiles/validation-profile-catalog-v1/valid/minimal.json index b503be768..e80a17097 100644 --- a/contracts/fixtures/profiles/validation-profile-catalog-v1/valid/minimal.json +++ b/contracts/fixtures/profiles/validation-profile-catalog-v1/valid/minimal.json @@ -1,6 +1,6 @@ { "schema_version": "validation-profile-catalog/v1", - "profile_family": "aces-validation", + "profile_family": "raes-validation", "catalog_version": "v1", "strengths": [ { @@ -29,9 +29,9 @@ ], "profiles": [ { - "profile_id": "aces-structural-validation", + "profile_id": "raes-structural-validation", "profile_version": "v1", - "title": "ACES structural validation", + "title": "RAES structural validation", "intended_subject_kinds": [ "scenario" ], diff --git a/contracts/fixtures/provenance/sdl-lineage-ledger-v1/valid/minimal.json b/contracts/fixtures/provenance/sdl-lineage-ledger-v1/valid/minimal.json index a7867e637..21c4c5597 100644 --- a/contracts/fixtures/provenance/sdl-lineage-ledger-v1/valid/minimal.json +++ b/contracts/fixtures/provenance/sdl-lineage-ledger-v1/valid/minimal.json @@ -37,9 +37,9 @@ "claims": [ { "plane": "semantics", - "classification": "aces_native", + "classification": "raes_native", "source_refs": [], - "aces_boundaries": [ + "raes_boundaries": [ { "artifact": "contracts/example.json", "symbol_or_pointer": "#/properties/example" diff --git a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/basic-address-and-block.json b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/basic-address-and-block.json index 5883fd7dd..49c5d99f4 100644 --- a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/basic-address-and-block.json +++ b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/basic-address-and-block.json @@ -8,7 +8,7 @@ "encoding": "hex-fixed-width", "value": "3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f" }, - "stream_key_hex": "6cff4da8bebe6b5a1206ad110183db7b66fef594e11fc3a06047e0a1cb70dac7", + "stream_key_hex": "29379bbf1c7dbd133939723057b9a40b7d641b302c58c8d2258ad9ee69d047d3", "address": { "namespace": "study-namespace", "trial_coordinate": { @@ -22,5 +22,5 @@ "local_coordinate": 0 }, "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a22636f6e646974696f6e2d61737369676e6d656e74222c226c6f63616c5f636f6f7264696e617465223a302c226e616d657370616365223a2273747564792d6e616d657370616365222c2273656c656374696f6e5f706f6c6963795f6964223a22706f6c6963792d61222c22747269616c5f636f6f7264696e617465223a7b22626c6f636b5f6964223a22626c6f636b2d31222c22636f6e646974696f6e5f6964223a22636f6e646974696f6e2d61222c227265706c69636174655f6964223a227265706c69636174652d31227d2c22766172696174696f6e5f706f696e745f6964223a22706f696e742d61227d", - "raw_block_hex": "b6fb579d5b4dec5f754e261abffe587ab43bb4af47757af13179787b0e4f293a" + "raw_block_hex": "f854884d49362106f94d6c0fe8cc408abc1dcdafdf7db8db1587a95bff293197" } diff --git a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-basic-draw.json b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-basic-draw.json index 3f11dadd4..e7e495c4c 100644 --- a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-basic-draw.json +++ b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-basic-draw.json @@ -8,7 +8,7 @@ "encoding": "hex-fixed-width", "value": "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5" }, - "stream_key_hex": "faab80ef9a35dc21293d3f23b58d43687d7808373998e501768be0bedffdfcf9", + "stream_key_hex": "0bd8f475de652a6f23763d7cb9bcf92110cdc36f6039b9708fff8e641a902503", "address": { "namespace": "study-namespace", "trial_coordinate": { @@ -20,7 +20,7 @@ "local_coordinate": 5 }, "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a2273616d706c696e672d73656c656374696f6e222c226c6f63616c5f636f6f7264696e617465223a352c226e616d657370616365223a2273747564792d6e616d657370616365222c2273656c656374696f6e5f706f6c6963795f6964223a22706f6c6963792d62222c22747269616c5f636f6f7264696e617465223a7b22636f6e646974696f6e5f6964223a22636f6e646974696f6e2d62227d2c22766172696174696f6e5f706f696e745f6964223a22706f696e742d62227d", - "raw_block_hex": "adc4a2adcbb22d7bd63d5e5552018fb0d076dd7b422b12af08afb999c3602ad5", + "raw_block_hex": "e5a8a68e2efe0edfc587c1e4c43a795672e9c22961ec698f50cc1250a6b4b565", "transform": { "transform_id": "bounded-integer", "transform_version": "1", @@ -31,7 +31,7 @@ "expected_rejection_exhausted": false, "outcome": { "kind": "public-value", - "value": "3" + "value": "9" } } } diff --git a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-boundary-single-value.json b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-boundary-single-value.json index 565686689..6742460ed 100644 --- a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-boundary-single-value.json +++ b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-boundary-single-value.json @@ -8,7 +8,7 @@ "encoding": "hex-fixed-width", "value": "1111111111111111111111111111111111111111111111111111111111111111" }, - "stream_key_hex": "f1d0a8e114a9f281184c21d56e6a66bda0dccb7eb08f6b40ef773d025d1b79d3", + "stream_key_hex": "5bf3c27418c0416cc41520c0b3b7f126092cf1b429c909b86abd8a4d44f243fc", "address": { "namespace": "study-namespace", "trial_coordinate": { @@ -20,7 +20,7 @@ "local_coordinate": 1 }, "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a227363686564756c65722d746965627265616b222c226c6f63616c5f636f6f7264696e617465223a312c226e616d657370616365223a2273747564792d6e616d657370616365222c2273656c656374696f6e5f706f6c6963795f6964223a22706f6c6963792d63222c22747269616c5f636f6f7264696e617465223a7b22636f6e646974696f6e5f6964223a22636f6e646974696f6e2d63227d2c22766172696174696f6e5f706f696e745f6964223a22706f696e742d63227d", - "raw_block_hex": "89d9889a7897f4ecba3a2f152d296b97242db739ea31e2da1e01cf8fff43944a", + "raw_block_hex": "f559feefd03d50c4db1d136833644e712cc4165d639f22fcae18761c22b1c904", "transform": { "transform_id": "bounded-integer", "transform_version": "1", diff --git a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-exhaustion.json b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-exhaustion.json index 87c5f68f5..85404de3d 100644 --- a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-exhaustion.json +++ b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-exhaustion.json @@ -8,7 +8,7 @@ "encoding": "hex-fixed-width", "value": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" }, - "stream_key_hex": "889fb9d507d2d624ceda9d130d0c810fc872b8d01f949345c2b32b7e6c716684", + "stream_key_hex": "f3d133b83db4c97454bd75188af34ea0617285a0e77c8693b52884addf97838d", "address": { "namespace": "study-namespace", "trial_coordinate": { @@ -17,10 +17,10 @@ "selection_policy_id": "policy-e", "variation_point_id": "point-e", "draw_purpose": "observation-noise", - "local_coordinate": 15 + "local_coordinate": 0 }, - "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a226f62736572766174696f6e2d6e6f697365222c226c6f63616c5f636f6f7264696e617465223a31352c226e616d657370616365223a2273747564792d6e616d657370616365222c2273656c656374696f6e5f706f6c6963795f6964223a22706f6c6963792d65222c22747269616c5f636f6f7264696e617465223a7b22636f6e646974696f6e5f6964223a22636f6e646974696f6e2d65227d2c22766172696174696f6e5f706f696e745f6964223a22706f696e742d65227d", - "raw_block_hex": "ddfcbd3421437b702feefe677a958f9e7eb884c3e034e9fc2e33e73b65e442b0", + "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a226f62736572766174696f6e2d6e6f697365222c226c6f63616c5f636f6f7264696e617465223a302c226e616d657370616365223a2273747564792d6e616d657370616365222c2273656c656374696f6e5f706f6c6963795f6964223a22706f6c6963792d65222c22747269616c5f636f6f7264696e617465223a7b22636f6e646974696f6e5f6964223a22636f6e646974696f6e2d65227d2c22766172696174696f6e5f706f696e745f6964223a22706f696e742d65227d", + "raw_block_hex": "ede867fc0ed9f3f1df4c503451aa6d5ba1d76805e0d42772394b53e47deb5a45", "transform": { "transform_id": "bounded-integer", "transform_version": "1", diff --git a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-rejection-then-accept.json b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-rejection-then-accept.json index 1cae3ff64..e4a22ae5f 100644 --- a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-rejection-then-accept.json +++ b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/bounded-integer-rejection-then-accept.json @@ -8,7 +8,7 @@ "encoding": "hex-fixed-width", "value": "7777777777777777777777777777777777777777777777777777777777777777" }, - "stream_key_hex": "e7a855caa683549214031e4a3313eb304beb54c1540c9ea65a05f9d742e05fa7", + "stream_key_hex": "dbe8eecef305c44dffc6889a4b4195963a668aed131944440a0922d0e4c4b051", "address": { "namespace": "study-namespace", "trial_coordinate": { @@ -17,10 +17,10 @@ "selection_policy_id": "policy-d", "variation_point_id": "point-d", "draw_purpose": "agent-policy", - "local_coordinate": 0 + "local_coordinate": 4 }, - "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a226167656e742d706f6c696379222c226c6f63616c5f636f6f7264696e617465223a302c226e616d657370616365223a2273747564792d6e616d657370616365222c2273656c656374696f6e5f706f6c6963795f6964223a22706f6c6963792d64222c22747269616c5f636f6f7264696e617465223a7b22636f6e646974696f6e5f6964223a22636f6e646974696f6e2d64227d2c22766172696174696f6e5f706f696e745f6964223a22706f696e742d64227d", - "raw_block_hex": "d7a44b4e285a3591eabf482a77e6eb675e8a0249da4656532b76d97fda2aab36", + "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a226167656e742d706f6c696379222c226c6f63616c5f636f6f7264696e617465223a342c226e616d657370616365223a2273747564792d6e616d657370616365222c2273656c656374696f6e5f706f6c6963795f6964223a22706f6c6963792d64222c22747269616c5f636f6f7264696e617465223a7b22636f6e646974696f6e5f6964223a22636f6e646974696f6e2d64227d2c22766172696174696f6e5f706f696e745f6964223a22706f696e742d64227d", + "raw_block_hex": "ec14167e458c1a8fd85bb2a352dcc12007636b8388ccf185c2e8851aff3088c2", "transform": { "transform_id": "bounded-integer", "transform_version": "1", @@ -31,7 +31,7 @@ "expected_rejection_exhausted": false, "outcome": { "kind": "public-value", - "value": "164" + "value": "20" } } } diff --git a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/zero-heavy-seed-and-address.json b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/zero-heavy-seed-and-address.json index 58a98cdf0..7c2d472a3 100644 --- a/contracts/fixtures/random-stream-vectors/blake3-xof-v1/zero-heavy-seed-and-address.json +++ b/contracts/fixtures/random-stream-vectors/blake3-xof-v1/zero-heavy-seed-and-address.json @@ -8,7 +8,7 @@ "encoding": "hex-fixed-width", "value": "0000000000000000000000000000000000000000000000000000000000000000" }, - "stream_key_hex": "08350ad899ff2d0d223d15ef7add51ec186b859525a1ab58f0c8f5c101d2faea", + "stream_key_hex": "39a7b17cae76d0fc6cb1570061a75120f2b674895df48587c4d82853906866fd", "address": { "namespace": "n", "trial_coordinate": {}, @@ -18,5 +18,5 @@ "local_coordinate": 0 }, "address_canonical_bytes_hex": "7b22647261775f707572706f7365223a226f74686572222c226c6f63616c5f636f6f7264696e617465223a302c226e616d657370616365223a226e222c2273656c656374696f6e5f706f6c6963795f6964223a2270222c22747269616c5f636f6f7264696e617465223a7b7d2c22766172696174696f6e5f706f696e745f6964223a2276227d", - "raw_block_hex": "66409f97615301b72c11ff0848f8699be673557cf9a93ac4e6720c25d60ee035" + "raw_block_hex": "603c8a2cc1ba9d29bd28d13e4fe8e0cf315b326ea231d26537c6348e22e23d94" } diff --git a/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/invalid/cross-outcome-payload.json b/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/invalid/cross-outcome-payload.json index fbb5cb8b3..8892c806f 100644 --- a/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/invalid/cross-outcome-payload.json +++ b/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/invalid/cross-outcome-payload.json @@ -1,8 +1,8 @@ { - "analysis_profile": "aces-finite-domain-satisfiability-v1", + "analysis_profile": "raes-finite-domain-satisfiability-v1", "authored_digest": { "algorithm": "sha256", - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "value": "sha256:f87bd4b2d9bdf62293871d0f047912a3fa3abfefeaf162c66878938870268726" }, "diagnostics": [], @@ -10,17 +10,17 @@ "normalized_model": { "authored_digest": { "algorithm": "sha256", - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "value": "sha256:f87bd4b2d9bdf62293871d0f047912a3fa3abfefeaf162c66878938870268726" }, "clauses": [], - "profile": "aces-finite-domain-constraints/v1", + "profile": "raes-finite-domain-constraints/v1", "source_digest": "sha256:a5902bcb0e43a9aa8d88ffaa2fa7c608781adb3052ca96dd54573ea3c9be14d7", "symbols": [], - "theory_profile": "aces-finite-domain-theory/v1", - "translation_profile": "aces-sdl-authoring-translation/v1" + "theory_profile": "raes-finite-domain-theory/v1", + "translation_profile": "raes-sdl-authoring-translation/v1" }, - "normalized_model_digest": "sha256:5f72c3a95df8e76b1ae6fb6a17c2c1b2dda9771e84445e8fe8b6ea14971a7bc4", + "normalized_model_digest": "sha256:9bddbf5372f75576d9eda0417b1249b7446792b3331da47afacfbca4cace5bfd", "outcome": "satisfiable", "profile": "scenario-satisfiability-evidence/v1", "solver_configuration": { @@ -32,14 +32,14 @@ "model": true, "package": "z3-solver", "package_version": "4.16.0.0", - "profile": "aces-z3-finite-domain/v1", + "profile": "raes-z3-finite-domain/v1", "random_seed": 0, "threads": 1, "timeout_ms": 5000, "unsat_core": true, "witness_selection": "canonical-lexicographic/v1" }, - "solver_configuration_digest": "sha256:63e58f4637dbd8328d84a286e1e5af1f3a69557e5209f683909ce22f39838e7d", + "solver_configuration_digest": "sha256:1204635e17e759e9ad3bd6be2ecb28c6de05c07ead6dfdd15936ed5d3d5b81b2", "source": { "byte_digest": "sha256:a5902bcb0e43a9aa8d88ffaa2fa7c608781adb3052ca96dd54573ea3c9be14d7", "source_id": "unsupported-control.sdl.yaml" diff --git a/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/valid/unsupported-target.json b/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/valid/unsupported-target.json index 6e251b862..4ca42165b 100644 --- a/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/valid/unsupported-target.json +++ b/contracts/fixtures/satisfiability/scenario-satisfiability-evidence-v1/valid/unsupported-target.json @@ -1,8 +1,8 @@ { - "analysis_profile": "aces-finite-domain-satisfiability-v1", + "analysis_profile": "raes-finite-domain-satisfiability-v1", "authored_digest": { "algorithm": "sha256", - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "value": "sha256:f87bd4b2d9bdf62293871d0f047912a3fa3abfefeaf162c66878938870268726" }, "diagnostics": [ @@ -18,7 +18,7 @@ "normalized_model": { "authored_digest": { "algorithm": "sha256", - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "value": "sha256:f87bd4b2d9bdf62293871d0f047912a3fa3abfefeaf162c66878938870268726" }, "clauses": [ @@ -30,7 +30,7 @@ "symbol_id": "symbol:a0d149e5d24ff238fff329fd011bc2c070f22b29da67b0d25e522482fca1c904" } ], - "profile": "aces-finite-domain-constraints/v1", + "profile": "raes-finite-domain-constraints/v1", "source_digest": "sha256:a5902bcb0e43a9aa8d88ffaa2fa7c608781adb3052ca96dd54573ea3c9be14d7", "symbols": [ { @@ -40,10 +40,10 @@ "variable": "cpu_count" } ], - "theory_profile": "aces-finite-domain-theory/v1", - "translation_profile": "aces-sdl-authoring-translation/v1" + "theory_profile": "raes-finite-domain-theory/v1", + "translation_profile": "raes-sdl-authoring-translation/v1" }, - "normalized_model_digest": "sha256:2f0f762771dc329419ab739766f684c18261aba28c2fbc50a26ee8ad80224ba5", + "normalized_model_digest": "sha256:beab3ddd6ed8fc115e2aadcd72254beff5da2d8b083a4f767b29773072cec523", "outcome": "unsupported", "profile": "scenario-satisfiability-evidence/v1", "solver_configuration": { @@ -55,21 +55,21 @@ "model": true, "package": "z3-solver", "package_version": "4.16.0.0", - "profile": "aces-z3-finite-domain/v1", + "profile": "raes-z3-finite-domain/v1", "random_seed": 0, "threads": 1, "timeout_ms": 5000, "unsat_core": true, "witness_selection": "canonical-lexicographic/v1" }, - "solver_configuration_digest": "sha256:63e58f4637dbd8328d84a286e1e5af1f3a69557e5209f683909ce22f39838e7d", + "solver_configuration_digest": "sha256:1204635e17e759e9ad3bd6be2ecb28c6de05c07ead6dfdd15936ed5d3d5b81b2", "source": { "byte_digest": "sha256:a5902bcb0e43a9aa8d88ffaa2fa7c608781adb3052ca96dd54573ea3c9be14d7", "source_id": "unsupported-control.sdl.yaml" }, "unsat_core": null, "unsupported": { - "profile": "aces-satisfiability-unsupported/v1", + "profile": "raes-satisfiability-unsupported/v1", "reason_codes": ["scenario-satisfiability.unsupported-target"] }, "witness": null diff --git a/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/invalid/wrong-profile.json b/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/invalid/wrong-profile.json index 4161dcd27..3eedb2c4a 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/invalid/wrong-profile.json +++ b/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/invalid/wrong-profile.json @@ -1,10 +1,10 @@ { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "scenario": { "name": "concrete-scenario", "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } diff --git a/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/valid/minimal.json b/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/valid/minimal.json index 81e797097..9945adf2e 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/valid/minimal.json +++ b/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/valid/minimal.json @@ -1,10 +1,10 @@ { - "profile": "aces-sdl-instantiated-snapshot/v1", + "profile": "raes-sdl-instantiated-snapshot/v1", "scenario": { "name": "concrete-scenario", "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } diff --git a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/imports-empty.json b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/imports-empty.json index ec47d5b4b..fba0aab4b 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/imports-empty.json +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/imports-empty.json @@ -3,7 +3,7 @@ "imports": [], "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } diff --git a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/module-null.json b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/module-null.json index 9fd9fccdb..0908d8317 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/module-null.json +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/module-null.json @@ -3,7 +3,7 @@ "module": null, "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } diff --git a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/unresolved-variable.json b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/unresolved-variable.json index 6e0e019e6..48af92c21 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/unresolved-variable.json +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/unresolved-variable.json @@ -3,7 +3,7 @@ "description": "deploy ${region} cluster", "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } diff --git a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/variables-empty.json b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/variables-empty.json index 8154bb14d..826d64062 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/variables-empty.json +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/variables-empty.json @@ -3,7 +3,7 @@ "variables": {}, "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } diff --git a/contracts/fixtures/sdl/instantiated-scenario-v1/valid/minimal.json b/contracts/fixtures/sdl/instantiated-scenario-v1/valid/minimal.json index ff6b3ad2c..a1546f957 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-v1/valid/minimal.json +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/valid/minimal.json @@ -3,7 +3,7 @@ "description": "a fully concrete instantiated scenario with no unresolved references", "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } diff --git a/contracts/fixtures/sdl/variation-points-v1/composition/module.yaml b/contracts/fixtures/sdl/variation-points-v1/composition/module.yaml index d9deb7c0e..aa6fe27e9 100644 --- a/contracts/fixtures/sdl/variation-points-v1/composition/module.yaml +++ b/contracts/fixtures/sdl/variation-points-v1/composition/module.yaml @@ -1,7 +1,7 @@ name: fixture-module version: 1.0.0 module: - id: aces/fixture-family + id: raes/fixture-family version: 1.0.0 exports: nodes: [primary, secondary] diff --git a/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/authoring-binding-scope-not-governed.json b/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/authoring-binding-scope-not-governed.json index 8bb581620..32c4fd905 100644 --- a/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/authoring-binding-scope-not-governed.json +++ b/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/authoring-binding-scope-not-governed.json @@ -2,7 +2,7 @@ "schema_version": "semantic-profile/v1", "profile_id": "reference-stack-v1", "title": "Reference Stack Interoperability Profile", - "description": "Shared semantic assumptions for the current ACES reference authoring, exchange, processing, and execution stack.", + "description": "Shared semantic assumptions for the current RAES reference authoring, exchange, processing, and execution stack.", "concept_catalog_version": "concept-families/v1", "authoring": { "required_contracts": [ @@ -38,7 +38,7 @@ }, { "id": "gov-919-native-extension-discipline", - "statement": "ACES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." + "statement": "RAES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." } ] }, diff --git a/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/missing-concept-catalog-version.json b/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/missing-concept-catalog-version.json index 0c6e97b98..1551211c4 100644 --- a/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/missing-concept-catalog-version.json +++ b/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/missing-concept-catalog-version.json @@ -2,7 +2,7 @@ "schema_version": "semantic-profile/v1", "profile_id": "reference-stack-v1", "title": "Reference Stack Interoperability Profile", - "description": "Shared semantic assumptions for the current ACES reference authoring, exchange, processing, and execution stack.", + "description": "Shared semantic assumptions for the current RAES reference authoring, exchange, processing, and execution stack.", "authoring": { "required_contracts": [ "sdl-authoring-input-v1", @@ -32,7 +32,7 @@ }, { "id": "gov-919-native-extension-discipline", - "statement": "ACES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." + "statement": "RAES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." } ] }, diff --git a/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/processing-binding-scope-not-governed.json b/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/processing-binding-scope-not-governed.json index 2f4d86bd1..4eaf6d96f 100644 --- a/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/processing-binding-scope-not-governed.json +++ b/contracts/fixtures/semantic-profile/semantic-profile-v1/invalid/processing-binding-scope-not-governed.json @@ -2,7 +2,7 @@ "schema_version": "semantic-profile/v1", "profile_id": "reference-stack-v1", "title": "Reference Stack Interoperability Profile", - "description": "Shared semantic assumptions for the current ACES reference authoring, exchange, processing, and execution stack.", + "description": "Shared semantic assumptions for the current RAES reference authoring, exchange, processing, and execution stack.", "concept_catalog_version": "concept-families/v1", "authoring": { "required_contracts": [ @@ -33,7 +33,7 @@ }, { "id": "gov-919-native-extension-discipline", - "statement": "ACES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." + "statement": "RAES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." } ] }, diff --git a/contracts/fixtures/semantic-profile/semantic-profile-v1/valid/reference-stack-v1.json b/contracts/fixtures/semantic-profile/semantic-profile-v1/valid/reference-stack-v1.json index 3f05af42b..124269cf0 100644 --- a/contracts/fixtures/semantic-profile/semantic-profile-v1/valid/reference-stack-v1.json +++ b/contracts/fixtures/semantic-profile/semantic-profile-v1/valid/reference-stack-v1.json @@ -2,7 +2,7 @@ "schema_version": "semantic-profile/v1", "profile_id": "reference-stack-v1", "title": "Reference Stack Interoperability Profile", - "description": "Shared semantic assumptions for the current ACES reference authoring, exchange, processing, and execution stack.", + "description": "Shared semantic assumptions for the current RAES reference authoring, exchange, processing, and execution stack.", "concept_catalog_version": "concept-families/v1", "authoring": { "required_contracts": [ @@ -34,7 +34,7 @@ }, { "id": "gov-919-native-extension-discipline", - "statement": "ACES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." + "statement": "RAES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." } ] }, diff --git a/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json b/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json index f07ed7e44..80def562f 100644 --- a/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json +++ b/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json @@ -210,7 +210,7 @@ "predicate": { "kind": "presence", "property": "runtime", - "semantic_ref": "urn:aces:declared-property:runtime", + "semantic_ref": "urn:raes:declared-property:runtime", "operator": "exists" }, "quantifier": "all", diff --git a/contracts/profiles/random-stream/blake3-xof-v1.json b/contracts/profiles/random-stream/blake3-xof-v1.json index 0ce19136e..a99ba55ef 100644 --- a/contracts/profiles/random-stream/blake3-xof-v1.json +++ b/contracts/profiles/random-stream/blake3-xof-v1.json @@ -17,7 +17,7 @@ }, "derivation": { "key_derivation_function": "blake3.derive_key", - "context_template": "aces-random-stream-v1|profile={profile_id}" + "context_template": "raes-random-stream-v1|profile={profile_id}" }, "block_encoding": { "block_bytes": 32, diff --git a/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json index fe609d562..94ce222b2 100644 --- a/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json +++ b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json @@ -56,7 +56,7 @@ "explicit_non_claims": ["Does not claim deployability, realization, experimental control, reproducibility, backend conformance, or behavioral equivalence."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "semantic-validity", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "semantic-validity", "subject": "An artifact admitted by the valid-sdl-fragment profile", "left_carrier_ref": "scientific-scenario-completeness@rev1:valid-sdl-fragment", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "Parser and semantic validator acceptance for the submitted SDL artifact under its declared profile.", "assurance_status": "tested", "evidence_refs": ["implementations/python/packages/raes/validator/", "implementations/python/tests/"], @@ -87,7 +87,7 @@ "explicit_non_claims": ["Does not claim that every backend can realize the scenario, that deployment succeeded, or that realized behavior is equivalent."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against deployable-scenario-intent", "left_carrier_ref": "candidate-scenario", "right_carrier_ref": "scientific-scenario-completeness@rev1:deployable-scenario-intent", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The required concern dispositions for one candidate artifact and its named external contract witnesses.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], @@ -118,7 +118,7 @@ "explicit_non_claims": ["Does not claim participant implementations are strategically equivalent, that hidden global state is visible, or that finite probes prove bisimulation."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against participant-evaluation-scenario", "left_carrier_ref": "candidate-scenario-and-experiment-bindings", "right_carrier_ref": "scientific-scenario-completeness@rev1:participant-evaluation-scenario", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The required concern dispositions and contract witnesses for one bound participant-evaluation scenario.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], @@ -148,7 +148,7 @@ "explicit_non_claims": ["Does not claim causal validity, successful execution, adequate power, reproducibility, or backend behavioral equivalence."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against controlled-experiment-scenario", "left_carrier_ref": "candidate-scenario-and-study-design", "right_carrier_ref": "scientific-scenario-completeness@rev1:controlled-experiment-scenario", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The pre-run design declarations and required concern witnesses for one controlled-experiment input.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], @@ -178,7 +178,7 @@ "explicit_non_claims": ["Does not guarantee bitwise replay, equal outcomes, scientific validity, generalizability, or behavioral equivalence across apparatuses."], "behavioral_claims": [ { - "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "profile-satisfaction", "subject": "An artifact assessed against reproducible-benchmark-study-input", "left_carrier_ref": "candidate-scenario-and-governed-experiment-bundle", "right_carrier_ref": "scientific-scenario-completeness@rev1:reproducible-benchmark-study-input", "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The preserved design, apparatus, participant, evidence, lineage, and analysis declarations for one benchmark or study input.", "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], diff --git a/contracts/profiles/semantic/reference-stack-v1.json b/contracts/profiles/semantic/reference-stack-v1.json index 3f05af42b..124269cf0 100644 --- a/contracts/profiles/semantic/reference-stack-v1.json +++ b/contracts/profiles/semantic/reference-stack-v1.json @@ -2,7 +2,7 @@ "schema_version": "semantic-profile/v1", "profile_id": "reference-stack-v1", "title": "Reference Stack Interoperability Profile", - "description": "Shared semantic assumptions for the current ACES reference authoring, exchange, processing, and execution stack.", + "description": "Shared semantic assumptions for the current RAES reference authoring, exchange, processing, and execution stack.", "concept_catalog_version": "concept-families/v1", "authoring": { "required_contracts": [ @@ -34,7 +34,7 @@ }, { "id": "gov-919-native-extension-discipline", - "statement": "ACES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." + "statement": "RAES-native concept families follow the explicit extension-scope, relation-rule, and non-ambiguity discipline declared in the concept catalog." } ] }, diff --git a/contracts/profiles/validation/validation-profile-catalog-v1.json b/contracts/profiles/validation/validation-profile-catalog-v1.json index 30db47756..83ffee640 100644 --- a/contracts/profiles/validation/validation-profile-catalog-v1.json +++ b/contracts/profiles/validation/validation-profile-catalog-v1.json @@ -1,6 +1,6 @@ { "schema_version": "validation-profile-catalog/v1", - "profile_family": "aces-validation", + "profile_family": "raes-validation", "catalog_version": "v1", "strengths": [ { @@ -11,7 +11,7 @@ { "strength_id": "semantic", "rank": 2, - "definition": "Structural validation plus ACES domain invariants, reference resolution, lifecycle separation, and cross-artifact consistency." + "definition": "Structural validation plus RAES domain invariants, reference resolution, lifecycle separation, and cross-artifact consistency." }, { "strength_id": "behavioral", @@ -78,7 +78,7 @@ }, { "gate_kind": "semantic_invariant_validation", - "definition": "ACES domain invariants have been evaluated." + "definition": "RAES domain invariants have been evaluated." }, { "gate_kind": "reference_resolution", @@ -165,9 +165,9 @@ ], "profiles": [ { - "profile_id": "aces-structural-validation", + "profile_id": "raes-structural-validation", "profile_version": "v1", - "title": "ACES structural validation", + "title": "RAES structural validation", "intended_subject_kinds": [ "scenario", "scenario_snapshot", @@ -198,9 +198,9 @@ "extension_terms_allowed": false }, { - "profile_id": "aces-semantic-validation", + "profile_id": "raes-semantic-validation", "profile_version": "v1", - "title": "ACES semantic validation", + "title": "RAES semantic validation", "intended_subject_kinds": [ "scenario", "scenario_snapshot", @@ -235,9 +235,9 @@ "extension_terms_allowed": false }, { - "profile_id": "aces-behavioral-validation", + "profile_id": "raes-behavioral-validation", "profile_version": "v1", - "title": "ACES behavioral validation", + "title": "RAES behavioral validation", "intended_subject_kinds": [ "scenario", "scenario_snapshot", @@ -277,9 +277,9 @@ "extension_terms_allowed": false }, { - "profile_id": "aces-evidence-backed-validation", + "profile_id": "raes-evidence-backed-validation", "profile_version": "v1", - "title": "ACES evidence-backed validation", + "title": "RAES evidence-backed validation", "intended_subject_kinds": [ "scenario", "scenario_snapshot", @@ -326,9 +326,9 @@ "extension_terms_allowed": false }, { - "profile_id": "aces-falsification-backed-validation", + "profile_id": "raes-falsification-backed-validation", "profile_version": "v1", - "title": "ACES falsification-backed validation", + "title": "RAES falsification-backed validation", "intended_subject_kinds": [ "scenario", "scenario_snapshot", diff --git a/contracts/schema-publication/entries/aces-semantic-invariants-v1.json b/contracts/schema-publication/entries/aces-semantic-invariants-v1.json deleted file mode 100644 index 2c306d4d2..000000000 --- a/contracts/schema-publication/entries/aces-semantic-invariants-v1.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "contract_id": "aces-semantic-invariants-v1", - "schema_path": "contracts/schemas/profiles/aces-semantic-invariants-v1.json", - "stability": "draft", - "content_hash": "2d3576e151d4d1e2ff10f33cbd2760a35aab3a5032cd6c239c8b19bdc8b96d2f", - "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "2d3576e151d4d1e2ff10f33cbd2760a35aab3a5032cd6c239c8b19bdc8b96d2f" - } -} diff --git a/contracts/schema-publication/entries/associated-artifact-manifest-v1.json b/contracts/schema-publication/entries/associated-artifact-manifest-v1.json index 20db37380..a9df6525e 100644 --- a/contracts/schema-publication/entries/associated-artifact-manifest-v1.json +++ b/contracts/schema-publication/entries/associated-artifact-manifest-v1.json @@ -2,9 +2,9 @@ "contract_id": "associated-artifact-manifest-v1", "schema_path": "contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json", "stability": "draft", - "content_hash": "238a1351cf24d33e885a373c4472bad7637e689d984ce04b3f906e0f20c7611b", + "content_hash": "6b9db0d57e9163a0554bc456b425924121438640e1bc052c722e6cd1f4f300f3", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "238a1351cf24d33e885a373c4472bad7637e689d984ce04b3f906e0f20c7611b" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "6b9db0d57e9163a0554bc456b425924121438640e1bc052c722e6cd1f4f300f3" } } diff --git a/contracts/schema-publication/entries/atlas-tactics-source-v1.json b/contracts/schema-publication/entries/atlas-tactics-source-v1.json index 21bfcccf5..82de74dfc 100644 --- a/contracts/schema-publication/entries/atlas-tactics-source-v1.json +++ b/contracts/schema-publication/entries/atlas-tactics-source-v1.json @@ -2,9 +2,9 @@ "contract_id": "atlas-tactics-source-v1", "schema_path": "contracts/schemas/concept-authority/atlas-tactics-source-v1.json", "stability": "draft", - "content_hash": "17e144abf9c8de10fa7342ab8662306ad0d4170a2df1c67829ac641a686bd562", + "content_hash": "25303bd3579d95cb16dc97d45ccd6de9915fac21345cf7dea7f2b317b70612b9", "last_change": { - "summary": "Initial publication of the pinned MITRE ATLAS tactics v2026.06 source schema for the ACT-609 adopted AI offensive behavior vocabulary.", - "content_hash": "17e144abf9c8de10fa7342ab8662306ad0d4170a2df1c67829ac641a686bd562" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "25303bd3579d95cb16dc97d45ccd6de9915fac21345cf7dea7f2b317b70612b9" } } diff --git a/contracts/schema-publication/entries/attack-enterprise-tactics-source-v1.json b/contracts/schema-publication/entries/attack-enterprise-tactics-source-v1.json index a45a9a47c..b7bb79f4e 100644 --- a/contracts/schema-publication/entries/attack-enterprise-tactics-source-v1.json +++ b/contracts/schema-publication/entries/attack-enterprise-tactics-source-v1.json @@ -2,9 +2,9 @@ "contract_id": "attack-enterprise-tactics-source-v1", "schema_path": "contracts/schemas/concept-authority/attack-enterprise-tactics-source-v1.json", "stability": "draft", - "content_hash": "32dc4af64f786b3c7a520181f9266cbc3fcfd54d69683faa861c38a495783c7f", + "content_hash": "20ee7e7ce46c59d46702bfb359dc924f19f0cbdd09ff6dc8b57aa9aaaf8be304", "last_change": { - "summary": "Initial publication of the pinned MITRE ATT&CK Enterprise tactics v19.1 source schema for the ACT-609 adopted offensive behavior vocabulary.", - "content_hash": "32dc4af64f786b3c7a520181f9266cbc3fcfd54d69683faa861c38a495783c7f" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "20ee7e7ce46c59d46702bfb359dc924f19f0cbdd09ff6dc8b57aa9aaaf8be304" } } diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index f5d62636a..9052109da 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": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48", + "content_hash": "d4e5419c2ba534d55a358b20808a8759f0a819e4ecf6a12f39d04bc713e9b138", "last_change": { - "summary": "Added a typed, owner-governed apparatus configuration target registry and experiment binding contract support.", - "content_hash": "8645c2b5e00cd595d2661959fe2dfa0ac91e5cca26d4455fe87dad8e6a97db48" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "d4e5419c2ba534d55a358b20808a8759f0a819e4ecf6a12f39d04bc713e9b138" } } diff --git a/contracts/schema-publication/entries/backend-profile-v1.json b/contracts/schema-publication/entries/backend-profile-v1.json index 2998372de..b6c2ae38a 100644 --- a/contracts/schema-publication/entries/backend-profile-v1.json +++ b/contracts/schema-publication/entries/backend-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "fd118e131b273684ac540675a8ba8d4b6ef1f10990ddd0d0dc5776c1b6c4a233", + "content_hash": "22625802ed2c93e46c4e5688d12300eb74c0dd3b635867a0ee72613241fcfd69", "last_change": { - "summary": "Added the authoritative experiment binding descriptor contract to the governed backend contract vocabulary.", - "content_hash": "fd118e131b273684ac540675a8ba8d4b6ef1f10990ddd0d0dc5776c1b6c4a233" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "22625802ed2c93e46c4e5688d12300eb74c0dd3b635867a0ee72613241fcfd69" } } diff --git a/contracts/schema-publication/entries/behavioral-relations-v1.json b/contracts/schema-publication/entries/behavioral-relations-v1.json index 479825dd1..8b53c3a58 100644 --- a/contracts/schema-publication/entries/behavioral-relations-v1.json +++ b/contracts/schema-publication/entries/behavioral-relations-v1.json @@ -2,9 +2,9 @@ "contract_id": "behavioral-relations-v1", "schema_path": "contracts/schemas/concept-authority/behavioral-relations-v1.json", "stability": "draft", - "content_hash": "e480fac32221c864dc953eef2700459a3db4b8a435abd02e69fc767ed7e46d76", + "content_hash": "1d28d7e81f489f2bcbc8772e7eb85d6a45800f99b96b2d3a80e66fa20be29a22", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "e480fac32221c864dc953eef2700459a3db4b8a435abd02e69fc767ed7e46d76" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "1d28d7e81f489f2bcbc8772e7eb85d6a45800f99b96b2d3a80e66fa20be29a22" } } diff --git a/contracts/schema-publication/entries/concept-families-v1.json b/contracts/schema-publication/entries/concept-families-v1.json index 1d86ec67d..776f75902 100644 --- a/contracts/schema-publication/entries/concept-families-v1.json +++ b/contracts/schema-publication/entries/concept-families-v1.json @@ -2,5 +2,9 @@ "contract_id": "concept-families-v1", "schema_path": "contracts/schemas/concept-authority/concept-families-v1.json", "stability": "draft", - "content_hash": "5e75b2d14d9f2537f5bb2e172edccb8ab158671b587dc01abc7477aabf2a5a3a" + "content_hash": "196b00f05a7100ecda790a05114c6bb47423a5ad4bbc8cf693fd30c9acd6f945", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "196b00f05a7100ecda790a05114c6bb47423a5ad4bbc8cf693fd30c9acd6f945" + } } diff --git a/contracts/schema-publication/entries/controlled-vocabularies-v1.json b/contracts/schema-publication/entries/controlled-vocabularies-v1.json index 60a67c725..17cbf12d0 100644 --- a/contracts/schema-publication/entries/controlled-vocabularies-v1.json +++ b/contracts/schema-publication/entries/controlled-vocabularies-v1.json @@ -2,9 +2,9 @@ "contract_id": "controlled-vocabularies-v1", "schema_path": "contracts/schemas/concept-authority/controlled-vocabularies-v1.json", "stability": "draft", - "content_hash": "c7565b28b4f3a5511b4b744b17210ddf9d79961d374bfbe1ace5a732cf637758", + "content_hash": "d0f03584fef13caf5258ad0eb559782235feaeab27021fd2ecd5f2575d7b6a7e", "last_change": { - "summary": "Added external source metadata to controlled vocabularies and pinned ACT-609 offensive behavior base terms to MITRE ATT&CK Enterprise tactics v19.1.", - "content_hash": "c7565b28b4f3a5511b4b744b17210ddf9d79961d374bfbe1ace5a732cf637758" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "d0f03584fef13caf5258ad0eb559782235feaeab27021fd2ecd5f2575d7b6a7e" } } diff --git a/contracts/schema-publication/entries/evaluation-history-event-stream-v1.json b/contracts/schema-publication/entries/evaluation-history-event-stream-v1.json index c9c72dcd1..d8baea0f9 100644 --- a/contracts/schema-publication/entries/evaluation-history-event-stream-v1.json +++ b/contracts/schema-publication/entries/evaluation-history-event-stream-v1.json @@ -2,5 +2,9 @@ "contract_id": "evaluation-history-event-stream-v1", "schema_path": "contracts/schemas/control-plane/evaluation-history-event-stream-v1.json", "stability": "draft", - "content_hash": "f34f5e940e267a2f5cc449fafcc79b8d8da9f537fd0ec4fd296c7843278ed7f4" + "content_hash": "b55ce200b8ef69dd0fd5a892611e40c50b497ea424dcbbbd11aa35c7edf10970", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "b55ce200b8ef69dd0fd5a892611e40c50b497ea424dcbbbd11aa35c7edf10970" + } } diff --git a/contracts/schema-publication/entries/evaluation-plan-v1.json b/contracts/schema-publication/entries/evaluation-plan-v1.json index a667b38a2..ce74d113a 100644 --- a/contracts/schema-publication/entries/evaluation-plan-v1.json +++ b/contracts/schema-publication/entries/evaluation-plan-v1.json @@ -2,9 +2,9 @@ "contract_id": "evaluation-plan-v1", "schema_path": "contracts/schemas/plans/evaluation-plan-v1.json", "stability": "draft", - "content_hash": "804f1970cbf9d80e80c3f984deb1d0f034470df7b25ee10ce7c7959b6ad69a4b", + "content_hash": "76211999362eed211814484ada9d12ab2c479871ed976ba9a51f1ec299d65410", "last_change": { - "summary": "Added proposition and assertion semantic-admission resources to the closed evaluation-plan vocabulary.", - "content_hash": "804f1970cbf9d80e80c3f984deb1d0f034470df7b25ee10ce7c7959b6ad69a4b" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "76211999362eed211814484ada9d12ab2c479871ed976ba9a51f1ec299d65410" } } diff --git a/contracts/schema-publication/entries/evaluation-result-envelope-v1.json b/contracts/schema-publication/entries/evaluation-result-envelope-v1.json index 88c2b85ce..664b1e185 100644 --- a/contracts/schema-publication/entries/evaluation-result-envelope-v1.json +++ b/contracts/schema-publication/entries/evaluation-result-envelope-v1.json @@ -2,5 +2,9 @@ "contract_id": "evaluation-result-envelope-v1", "schema_path": "contracts/schemas/control-plane/evaluation-result-envelope-v1.json", "stability": "draft", - "content_hash": "a3a99913c8e2092fb39483e278d52be2f2a41c553a500b776d08a2500638c95d" + "content_hash": "8b312c6b6d1e72af53ffde3d0c33634ac86f84ad3fbd6c4fbcf5902bca544877", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "8b312c6b6d1e72af53ffde3d0c33634ac86f84ad3fbd6c4fbcf5902bca544877" + } } diff --git a/contracts/schema-publication/entries/experiment-apparatus-context-v1.json b/contracts/schema-publication/entries/experiment-apparatus-context-v1.json index e1353a5eb..73088fcdf 100644 --- a/contracts/schema-publication/entries/experiment-apparatus-context-v1.json +++ b/contracts/schema-publication/entries/experiment-apparatus-context-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-apparatus-context-v1", "schema_path": "contracts/schemas/experiment-core/experiment-apparatus-context-v1.json", "stability": "draft", - "content_hash": "758ea626dfb224bd59c0a8cf39fec3f5318d7d27e915a506489d56a4be649490", + "content_hash": "8e174c238d93867603c4df93ba9bc2a736e4e9fa36c32088f2aa911557831204", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "758ea626dfb224bd59c0a8cf39fec3f5318d7d27e915a506489d56a4be649490" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "8e174c238d93867603c4df93ba9bc2a736e4e9fa36c32088f2aa911557831204" } } diff --git a/contracts/schema-publication/entries/experiment-authoring-input-v1.json b/contracts/schema-publication/entries/experiment-authoring-input-v1.json index e31bcdbde..9a663b06a 100644 --- a/contracts/schema-publication/entries/experiment-authoring-input-v1.json +++ b/contracts/schema-publication/entries/experiment-authoring-input-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-authoring-input-v1", "schema_path": "contracts/schemas/experiment-core/experiment-authoring-input-v1.json", "stability": "draft", - "content_hash": "47a2a28bd72c252aff22a651e58b216cd4700e9d7c4cabc87728bce0657183a6", + "content_hash": "2ffe9d8c055458997b3f78159e7b54eb1ea05a3bf52ce3adaa64234220457cce", "last_change": { - "summary": "Added explicit binding semantics and authoritative cross-plane binding descriptors joined to factor levels and conditions.", - "content_hash": "47a2a28bd72c252aff22a651e58b216cd4700e9d7c4cabc87728bce0657183a6" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "2ffe9d8c055458997b3f78159e7b54eb1ea05a3bf52ce3adaa64234220457cce" } } diff --git a/contracts/schema-publication/entries/experiment-binding-descriptors-v1.json b/contracts/schema-publication/entries/experiment-binding-descriptors-v1.json index 7b77375c6..5368ea6a8 100644 --- a/contracts/schema-publication/entries/experiment-binding-descriptors-v1.json +++ b/contracts/schema-publication/entries/experiment-binding-descriptors-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-binding-descriptors-v1", "schema_path": "contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json", "stability": "draft", - "content_hash": "88d57279e1d4467de97fa32893dfad58f3dccb5bf6a9d6e9610ce84e78265f2c", + "content_hash": "fe1e2308c9c101f1b115a5b673b369eab68431a5c3befc41cba9cf5169475f97", "last_change": { - "summary": "Published explicit, typed, plane-specific experiment binding descriptors with factor and condition provenance.", - "content_hash": "88d57279e1d4467de97fa32893dfad58f3dccb5bf6a9d6e9610ce84e78265f2c" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "fe1e2308c9c101f1b115a5b673b369eab68431a5c3befc41cba9cf5169475f97" } } diff --git a/contracts/schema-publication/entries/experiment-capture-spec-v1.json b/contracts/schema-publication/entries/experiment-capture-spec-v1.json index 3d7d53c2d..64d6a63f2 100644 --- a/contracts/schema-publication/entries/experiment-capture-spec-v1.json +++ b/contracts/schema-publication/entries/experiment-capture-spec-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-capture-spec-v1", "schema_path": "contracts/schemas/experiment-core/experiment-capture-spec-v1.json", "stability": "draft", - "content_hash": "abd5fc6cfc1c77159328531c531112c34685a348463405d30f7f63082c945705", + "content_hash": "59a5a2fc7c28eb5d27725916def11b11af919e13be7942ded83df30c05be833d", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "abd5fc6cfc1c77159328531c531112c34685a348463405d30f7f63082c945705" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "59a5a2fc7c28eb5d27725916def11b11af919e13be7942ded83df30c05be833d" } } diff --git a/contracts/schema-publication/entries/experiment-derived-measure-v1.json b/contracts/schema-publication/entries/experiment-derived-measure-v1.json index bb02caff7..6282a604a 100644 --- a/contracts/schema-publication/entries/experiment-derived-measure-v1.json +++ b/contracts/schema-publication/entries/experiment-derived-measure-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-derived-measure-v1", "schema_path": "contracts/schemas/experiment-core/experiment-derived-measure-v1.json", "stability": "draft", - "content_hash": "33dc362225a5850c45f7ef2fbca7b5ef320e687865c4cdedb5b40c2da70b9c65", + "content_hash": "99668f7dd86d118944dc3c4c90998a042bbbf00fad0d34d0fb9e78c9a95e3e30", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "33dc362225a5850c45f7ef2fbca7b5ef320e687865c4cdedb5b40c2da70b9c65" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "99668f7dd86d118944dc3c4c90998a042bbbf00fad0d34d0fb9e78c9a95e3e30" } } diff --git a/contracts/schema-publication/entries/experiment-evidence-record-v1.json b/contracts/schema-publication/entries/experiment-evidence-record-v1.json index 13c080329..cf1ef0b93 100644 --- a/contracts/schema-publication/entries/experiment-evidence-record-v1.json +++ b/contracts/schema-publication/entries/experiment-evidence-record-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-evidence-record-v1", "schema_path": "contracts/schemas/experiment-core/experiment-evidence-record-v1.json", "stability": "draft", - "content_hash": "97e633634abdefb8cd4343b14f557065e79616e40edb87da14294ff892823317", + "content_hash": "3a41753ecb4c852af0dbf324794493597641a16e9ec564e642905e459e8da92f", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "97e633634abdefb8cd4343b14f557065e79616e40edb87da14294ff892823317" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "3a41753ecb4c852af0dbf324794493597641a16e9ec564e642905e459e8da92f" } } diff --git a/contracts/schema-publication/entries/experiment-run-v1.json b/contracts/schema-publication/entries/experiment-run-v1.json index a84977cc5..1a7ddabdf 100644 --- a/contracts/schema-publication/entries/experiment-run-v1.json +++ b/contracts/schema-publication/entries/experiment-run-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-run-v1", "schema_path": "contracts/schemas/experiment-core/experiment-run-v1.json", "stability": "draft", - "content_hash": "718d8b108bfc7cc7cee4d55970c12e7fd87c3873f13aaee51b2b4de468ba5db7", + "content_hash": "f6e1c277a1a48e764cbbcf8433d666fa7bbd9bb1d38953f8b55b9c4a1a1d1180", "last_change": { - "summary": "Added realized cross-plane binding provenance with explicit source, target, value origin, and configuration digest.", - "content_hash": "718d8b108bfc7cc7cee4d55970c12e7fd87c3873f13aaee51b2b4de468ba5db7" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "f6e1c277a1a48e764cbbcf8433d666fa7bbd9bb1d38953f8b55b9c4a1a1d1180" } } diff --git a/contracts/schema-publication/entries/experiment-study-v1.json b/contracts/schema-publication/entries/experiment-study-v1.json index 03350e2b6..c0c26cdf8 100644 --- a/contracts/schema-publication/entries/experiment-study-v1.json +++ b/contracts/schema-publication/entries/experiment-study-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-study-v1", "schema_path": "contracts/schemas/experiment-core/experiment-study-v1.json", "stability": "draft", - "content_hash": "a0fda94413a08751303f4d57a2f4bc6507cc5da50bc99f065b661a28693345e4", + "content_hash": "e3968289f399d09a974dfb7ac04266d5e37f9ad68abaa73e49c3862c87f69a03", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "a0fda94413a08751303f4d57a2f4bc6507cc5da50bc99f065b661a28693345e4" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "e3968289f399d09a974dfb7ac04266d5e37f9ad68abaa73e49c3862c87f69a03" } } diff --git a/contracts/schema-publication/entries/experiment-task-v1.json b/contracts/schema-publication/entries/experiment-task-v1.json index f79df79d7..57d65f4a2 100644 --- a/contracts/schema-publication/entries/experiment-task-v1.json +++ b/contracts/schema-publication/entries/experiment-task-v1.json @@ -2,9 +2,9 @@ "contract_id": "experiment-task-v1", "schema_path": "contracts/schemas/experiment-core/experiment-task-v1.json", "stability": "draft", - "content_hash": "2fadfb327b1a534b591481429322c799b522955d298c7cb01df8d75f4386c8f3", + "content_hash": "5f3745dde0403ecfd24c5e37cc8522ac5568c8b01fc235cfc8b1b337b2a5071e", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "2fadfb327b1a534b591481429322c799b522955d298c7cb01df8d75f4386c8f3" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "5f3745dde0403ecfd24c5e37cc8522ac5568c8b01fc235cfc8b1b337b2a5071e" } } diff --git a/contracts/schema-publication/entries/exploit-path-analysis-evidence-v1.json b/contracts/schema-publication/entries/exploit-path-analysis-evidence-v1.json index e92b57a01..e9b0500d6 100644 --- a/contracts/schema-publication/entries/exploit-path-analysis-evidence-v1.json +++ b/contracts/schema-publication/entries/exploit-path-analysis-evidence-v1.json @@ -2,9 +2,9 @@ "contract_id": "exploit-path-analysis-evidence-v1", "schema_path": "contracts/schemas/exploit-path-analysis/exploit-path-analysis-evidence-v1.json", "stability": "draft", - "content_hash": "61bba2ad5561da86a05192cb16809e5098e9717c31c2b3100d55013b5d7832f7", + "content_hash": "d51c86c5f939fdc485bb5d1b2057d0069a5c976316e4f71c84eb85fa3ea6f77a", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "61bba2ad5561da86a05192cb16809e5098e9717c31c2b3100d55013b5d7832f7" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "d51c86c5f939fdc485bb5d1b2057d0069a5c976316e4f71c84eb85fa3ea6f77a" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json index 31b28e636..223a568b9 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": "6833f448f35ead054ba95e4a621309c320b5bc227e32d2e12472f276482e5f59", + "content_hash": "fc0302c9c6f75df99c1f46e02faa74475b01d99e8498f9ff8f951555a7410c1b", "last_change": { - "summary": "Published participant-directed inject deliveries and governed participant action-argument domains in immutable scenario snapshots on the RAES-only package surface.", - "content_hash": "6833f448f35ead054ba95e4a621309c320b5bc227e32d2e12472f276482e5f59" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "fc0302c9c6f75df99c1f46e02faa74475b01d99e8498f9ff8f951555a7410c1b" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-v1.json b/contracts/schema-publication/entries/instantiated-scenario-v1.json index cfccdfdec..86fca2022 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": "1017a5db698151a50081675174b6c0f3cfb3eb2f22ec0532cd7334f7ee3ec76a", + "content_hash": "d39c40be6508c0ae1dbfaada67a0d009bd2fba08d23ffe12f4aab9744a6d6e3c", "last_change": { - "summary": "Published participant-directed inject deliveries and governed participant action-argument domains in instantiated scenarios on the RAES-only package surface.", - "content_hash": "1017a5db698151a50081675174b6c0f3cfb3eb2f22ec0532cd7334f7ee3ec76a" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "d39c40be6508c0ae1dbfaada67a0d009bd2fba08d23ffe12f4aab9744a6d6e3c" } } diff --git a/contracts/schema-publication/entries/nist-csf-defensive-categories-source-v1.json b/contracts/schema-publication/entries/nist-csf-defensive-categories-source-v1.json index 602d8b17a..0dd3f4687 100644 --- a/contracts/schema-publication/entries/nist-csf-defensive-categories-source-v1.json +++ b/contracts/schema-publication/entries/nist-csf-defensive-categories-source-v1.json @@ -2,9 +2,9 @@ "contract_id": "nist-csf-defensive-categories-source-v1", "schema_path": "contracts/schemas/concept-authority/nist-csf-defensive-categories-source-v1.json", "stability": "draft", - "content_hash": "60d34916929a8607dda79fb2578e162e04d1f91d1554b6991058ff79968bc82a", + "content_hash": "f5aa81e3c9c0cac489bc1ff89eb65a94b751d50f73c58baedb82371faf72c14f", "last_change": { - "summary": "Initial publication of the pinned NIST CSF 2.0 defensive-category source schema for the ACT-610 adapted defensive behavior vocabulary.", - "content_hash": "60d34916929a8607dda79fb2578e162e04d1f91d1554b6991058ff79968bc82a" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "f5aa81e3c9c0cac489bc1ff89eb65a94b751d50f73c58baedb82371faf72c14f" } } diff --git a/contracts/schema-publication/entries/operation-receipt-v1.json b/contracts/schema-publication/entries/operation-receipt-v1.json index cad39da39..e9581c6c6 100644 --- a/contracts/schema-publication/entries/operation-receipt-v1.json +++ b/contracts/schema-publication/entries/operation-receipt-v1.json @@ -2,5 +2,9 @@ "contract_id": "operation-receipt-v1", "schema_path": "contracts/schemas/control-plane/operation-receipt-v1.json", "stability": "draft", - "content_hash": "07bdbdaf5def120d43500da30a7ea38b736fdce8125e2cdb62b5b6851bf90504" + "content_hash": "195b33906287ca5b04fdf1820c48b79f25e6279311f7c79bfbe4c43f1aa7aeba", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "195b33906287ca5b04fdf1820c48b79f25e6279311f7c79bfbe4c43f1aa7aeba" + } } diff --git a/contracts/schema-publication/entries/operation-status-v1.json b/contracts/schema-publication/entries/operation-status-v1.json index ce44b07d1..5b4838f50 100644 --- a/contracts/schema-publication/entries/operation-status-v1.json +++ b/contracts/schema-publication/entries/operation-status-v1.json @@ -2,9 +2,9 @@ "contract_id": "operation-status-v1", "schema_path": "contracts/schemas/control-plane/operation-status-v1.json", "stability": "draft", - "content_hash": "fd9af93db03252516e91ffe2cc4eae6c006221b5a10ef6b9998762ec51a5ed53", + "content_hash": "407c2b148ea3e9490d5e9d0288f8ae1199d04be3f0ad5a2483d84e7ee0b6510a", "last_change": { - "summary": "Constrained backend-reported changed addresses to unique canonical compiled addresses for DSL-101/DSL-102.", - "content_hash": "fd9af93db03252516e91ffe2cc4eae6c006221b5a10ef6b9998762ec51a5ed53" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "407c2b148ea3e9490d5e9d0288f8ae1199d04be3f0ad5a2483d84e7ee0b6510a" } } diff --git a/contracts/schema-publication/entries/orchestration-plan-v1.json b/contracts/schema-publication/entries/orchestration-plan-v1.json index 3a446cc7b..81281e9e7 100644 --- a/contracts/schema-publication/entries/orchestration-plan-v1.json +++ b/contracts/schema-publication/entries/orchestration-plan-v1.json @@ -2,9 +2,9 @@ "contract_id": "orchestration-plan-v1", "schema_path": "contracts/schemas/plans/orchestration-plan-v1.json", "stability": "draft", - "content_hash": "2ada1a85e8049b7ab52e8c867ab6360fb7fe2833c62ab9469d0b7a49aa0dd2e7", + "content_hash": "b2fe219bb20fe8c30aa8e7cacced28e44671512a7f809d1ab5a1c980b0ab9fef", "last_change": { - "summary": "Constrained orchestration operations to the orchestration address domain and closed resource-type vocabulary for DSL-101/DSL-102/SEM-205.", - "content_hash": "2ada1a85e8049b7ab52e8c867ab6360fb7fe2833c62ab9469d0b7a49aa0dd2e7" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "b2fe219bb20fe8c30aa8e7cacced28e44671512a7f809d1ab5a1c980b0ab9fef" } } diff --git a/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json b/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json index 90cc7d6ca..022929272 100644 --- a/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json +++ b/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-behavior-history-event-stream-v1", "schema_path": "contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json", "stability": "draft", - "content_hash": "4aeeaba0d4834a38bed72aca834733806fe8625d763b15e7d2dba764637f4234", + "content_hash": "4baa867a23fc0b33d8b9091e6dec7e027e9c8cafe4fe1fa0a0ab89e5b7f81cc0", "last_change": { - "summary": "Revised the participant behavior-history event-stream contract as part of the backend-facing contract family publication (ADR-060, issue #76).", - "content_hash": "4aeeaba0d4834a38bed72aca834733806fe8625d763b15e7d2dba764637f4234" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "4baa867a23fc0b33d8b9091e6dec7e027e9c8cafe4fe1fa0a0ab89e5b7f81cc0" } } diff --git a/contracts/schema-publication/entries/participant-configuration-result-v1.json b/contracts/schema-publication/entries/participant-configuration-result-v1.json index 7a421024d..1caf5f733 100644 --- a/contracts/schema-publication/entries/participant-configuration-result-v1.json +++ b/contracts/schema-publication/entries/participant-configuration-result-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-configuration-result-v1", "schema_path": "contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json", "stability": "draft", - "content_hash": "915c2557c352fc5866fa0e5171abb02afb0b9eea485749aefa94ba0bc0dc3c51", + "content_hash": "4a591f8bf253fc7caa2a30edc05a665f1ffa64c4764d3fb14f440fa559c03743", "last_change": { - "summary": "Published atomic normalized participant configuration results with value provenance and a canonical digest.", - "content_hash": "915c2557c352fc5866fa0e5171abb02afb0b9eea485749aefa94ba0bc0dc3c51" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "4a591f8bf253fc7caa2a30edc05a665f1ffa64c4764d3fb14f440fa559c03743" } } diff --git a/contracts/schema-publication/entries/participant-context-view-v1.json b/contracts/schema-publication/entries/participant-context-view-v1.json index f6036df55..e156fc656 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": "24c40473cb12273bdfebbb1a941b2d266cdad2976f331cda30973153fe8753e5", + "content_hash": "9d7bfc3de15e89b20bd27e704feb7683806014cbb3fb0babf4f0193db2602ff4", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "24c40473cb12273bdfebbb1a941b2d266cdad2976f331cda30973153fe8753e5" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "9d7bfc3de15e89b20bd27e704feb7683806014cbb3fb0babf4f0193db2602ff4" } } diff --git a/contracts/schema-publication/entries/participant-control-occurrence-v1.json b/contracts/schema-publication/entries/participant-control-occurrence-v1.json index f41d2e607..ce75ff2cd 100644 --- a/contracts/schema-publication/entries/participant-control-occurrence-v1.json +++ b/contracts/schema-publication/entries/participant-control-occurrence-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-control-occurrence-v1", "schema_path": "contracts/schemas/participant-runtime/participant-control-occurrence-v1.json", "stability": "draft", - "content_hash": "edceb2ce36178a4d3c69c2d983ea90737c3a85c213a4a11bed93ed0591890754", + "content_hash": "b9ead3b36df6c998df60f2b328ac3433f547d135fc9526d36b54e8c335a3489f", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "edceb2ce36178a4d3c69c2d983ea90737c3a85c213a4a11bed93ed0591890754" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "b9ead3b36df6c998df60f2b328ac3433f547d135fc9526d36b54e8c335a3489f" } } diff --git a/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json b/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json index e766e97c3..3fee72863 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": "392ab485349bef35463a2fd0e843133a8adff2614ffb0ca456c880d7c1d4f0d4", + "content_hash": "34f79eaed648530dbf7b2c948a1cb8e33320caea5f1b0586776f8f28d2b38c10", "last_change": { - "summary": "Published the API-423 closed participant-crossing policy, transformation, disclosure, realization, evidence, and provenance occurrence family.", - "content_hash": "392ab485349bef35463a2fd0e843133a8adff2614ffb0ca456c880d7c1d4f0d4" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "34f79eaed648530dbf7b2c948a1cb8e33320caea5f1b0586776f8f28d2b38c10" } } diff --git a/contracts/schema-publication/entries/participant-decision-surface-v1.json b/contracts/schema-publication/entries/participant-decision-surface-v1.json index 0e1554909..9fddb8963 100644 --- a/contracts/schema-publication/entries/participant-decision-surface-v1.json +++ b/contracts/schema-publication/entries/participant-decision-surface-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-decision-surface-v1", "schema_path": "contracts/schemas/control-plane/participant-decision-surface-v1.json", "stability": "draft", - "content_hash": "77920c1edfa6fe1785d50d6d33a194acd59fdc41845c016eedbf88af342bd2e2", + "content_hash": "de312b4eaa535e16bbfa5d5d59081a6dd86ec41936d8c2c402cbe4ca62ff0748", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "77920c1edfa6fe1785d50d6d33a194acd59fdc41845c016eedbf88af342bd2e2" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "de312b4eaa535e16bbfa5d5d59081a6dd86ec41936d8c2c402cbe4ca62ff0748" } } diff --git a/contracts/schema-publication/entries/participant-episode-history-event-stream-v1.json b/contracts/schema-publication/entries/participant-episode-history-event-stream-v1.json index 2d7cd3122..6e5baf654 100644 --- a/contracts/schema-publication/entries/participant-episode-history-event-stream-v1.json +++ b/contracts/schema-publication/entries/participant-episode-history-event-stream-v1.json @@ -2,5 +2,9 @@ "contract_id": "participant-episode-history-event-stream-v1", "schema_path": "contracts/schemas/control-plane/participant-episode-history-event-stream-v1.json", "stability": "draft", - "content_hash": "9fb1a9b0edaf79987e66dd61a34d9c3d4f95f020424783778a2ba4f64eaa2272" + "content_hash": "2ef1c837394083ef263bec41a46113e047f9d8a8ba1a75b09abb5e4e4e95c605", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "2ef1c837394083ef263bec41a46113e047f9d8a8ba1a75b09abb5e4e4e95c605" + } } diff --git a/contracts/schema-publication/entries/participant-episode-state-envelope-v1.json b/contracts/schema-publication/entries/participant-episode-state-envelope-v1.json index 3d4ee5364..5ea70e5b3 100644 --- a/contracts/schema-publication/entries/participant-episode-state-envelope-v1.json +++ b/contracts/schema-publication/entries/participant-episode-state-envelope-v1.json @@ -2,5 +2,9 @@ "contract_id": "participant-episode-state-envelope-v1", "schema_path": "contracts/schemas/control-plane/participant-episode-state-envelope-v1.json", "stability": "draft", - "content_hash": "e442e8c17d47fa275855664db993b53c501f0fb04e82b661f5bbe9fcb68ae39c" + "content_hash": "506a10360067ddd75e086c3e1c6cc031201dfb7ba163220d1ef2d7c53c78e3ed", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "506a10360067ddd75e086c3e1c6cc031201dfb7ba163220d1ef2d7c53c78e3ed" + } } diff --git a/contracts/schema-publication/entries/participant-history-view-v1.json b/contracts/schema-publication/entries/participant-history-view-v1.json index 04f76e8d1..2b5ef81d2 100644 --- a/contracts/schema-publication/entries/participant-history-view-v1.json +++ b/contracts/schema-publication/entries/participant-history-view-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-history-view-v1", "schema_path": "contracts/schemas/control-plane/participant-history-view-v1.json", "stability": "draft", - "content_hash": "380257f288c098c0b94e343dbeb6d51c5216c6b14d91543cc2c147dd06633435", + "content_hash": "c98b133455fe41b5ce832243b80cdcaa62e2b60ea351e8e298b11b5c336fe3a6", "last_change": { - "summary": "Initial publication of the participant history-view control-plane contract: the participant-local interaction-history projection in the backend-facing contract family (ADR-060, issue #76).", - "content_hash": "380257f288c098c0b94e343dbeb6d51c5216c6b14d91543cc2c147dd06633435" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "c98b133455fe41b5ce832243b80cdcaa62e2b60ea351e8e298b11b5c336fe3a6" } } diff --git a/contracts/schema-publication/entries/participant-implementation-manifest-v1.json b/contracts/schema-publication/entries/participant-implementation-manifest-v1.json index f6578bf51..b46677ccd 100644 --- a/contracts/schema-publication/entries/participant-implementation-manifest-v1.json +++ b/contracts/schema-publication/entries/participant-implementation-manifest-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-implementation-manifest-v1", "schema_path": "contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json", "stability": "draft", - "content_hash": "5b7c636a98c540e1afc74ca74a1e9a421609f498c3b85c473a52c1516e398984", + "content_hash": "cfeb4e4d3122ffac80370b97b6fd0e2551a001f97fe574ea5b7e63656a7b04bc", "last_change": { - "summary": "Added typed participant configuration targets, aliases, defaults, sensitivity, and governed validator ownership.", - "content_hash": "5b7c636a98c540e1afc74ca74a1e9a421609f498c3b85c473a52c1516e398984" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "cfeb4e4d3122ffac80370b97b6fd0e2551a001f97fe574ea5b7e63656a7b04bc" } } diff --git a/contracts/schema-publication/entries/participant-implementation-provenance-v1.json b/contracts/schema-publication/entries/participant-implementation-provenance-v1.json index 7084e0330..a4d1007bc 100644 --- a/contracts/schema-publication/entries/participant-implementation-provenance-v1.json +++ b/contracts/schema-publication/entries/participant-implementation-provenance-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-implementation-provenance-v1", "schema_path": "contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json", "stability": "draft", - "content_hash": "88076dc67b837b282a0efe482d4b74d5fb053ad7b44cb6bc0d862b4fe17eb30e", + "content_hash": "76d081fc933ededd72648dbab37451b3ce638ed346cd6e0bee69a8fb94d445be", "last_change": { - "summary": "Required participant configuration references and authoritative normalized configuration digests to be supplied together.", - "content_hash": "88076dc67b837b282a0efe482d4b74d5fb053ad7b44cb6bc0d862b4fe17eb30e" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "76d081fc933ededd72648dbab37451b3ce638ed346cd6e0bee69a8fb94d445be" } } diff --git a/contracts/schema-publication/entries/participant-joint-action-record-v1.json b/contracts/schema-publication/entries/participant-joint-action-record-v1.json index 2e0ee4820..fc4fd0970 100644 --- a/contracts/schema-publication/entries/participant-joint-action-record-v1.json +++ b/contracts/schema-publication/entries/participant-joint-action-record-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-joint-action-record-v1", "schema_path": "contracts/schemas/participant-runtime/participant-joint-action-record-v1.json", "stability": "draft", - "content_hash": "412ed8b1156463900b6d06c28b3ed39ccd3fa4a693d78bff534a7f82226a2a1c", + "content_hash": "0af0593bc1fde776db0835d0baff6cd0ae232051e74eac1b8a26f7d1a532c88e", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "412ed8b1156463900b6d06c28b3ed39ccd3fa4a693d78bff534a7f82226a2a1c" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "0af0593bc1fde776db0835d0baff6cd0ae232051e74eac1b8a26f7d1a532c88e" } } diff --git a/contracts/schema-publication/entries/participant-lifecycle-event-v1.json b/contracts/schema-publication/entries/participant-lifecycle-event-v1.json index 40ad7faf9..b998548de 100644 --- a/contracts/schema-publication/entries/participant-lifecycle-event-v1.json +++ b/contracts/schema-publication/entries/participant-lifecycle-event-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-lifecycle-event-v1", "schema_path": "contracts/schemas/participant-runtime/participant-lifecycle-event-v1.json", "stability": "draft", - "content_hash": "b7c86b3ad1530153f1185483f2310d6ad331a3bf60a562a12d16d993d02cb90a", + "content_hash": "3bdfd98d46e24c93a9659550f870c8e7c146255e627e74117ccf7b246dcb2afe", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "b7c86b3ad1530153f1185483f2310d6ad331a3bf60a562a12d16d993d02cb90a" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "3bdfd98d46e24c93a9659550f870c8e7c146255e627e74117ccf7b246dcb2afe" } } diff --git a/contracts/schema-publication/entries/participant-observation-envelope-v1.json b/contracts/schema-publication/entries/participant-observation-envelope-v1.json index a3dd1533b..4256b2210 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": "802cbc844f702694cf985148b6f71168c89d59c2b2edeebac282d7a8a114bbe9", + "content_hash": "c5ad70819e897df97b60db1960e5eac3ac3e1435641e7013b5fd9ae259710823", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "802cbc844f702694cf985148b6f71168c89d59c2b2edeebac282d7a8a114bbe9" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "c5ad70819e897df97b60db1960e5eac3ac3e1435641e7013b5fd9ae259710823" } } diff --git a/contracts/schema-publication/entries/participant-outcome-report-v1.json b/contracts/schema-publication/entries/participant-outcome-report-v1.json index a264697ca..f14249077 100644 --- a/contracts/schema-publication/entries/participant-outcome-report-v1.json +++ b/contracts/schema-publication/entries/participant-outcome-report-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-outcome-report-v1", "schema_path": "contracts/schemas/participant-runtime/participant-outcome-report-v1.json", "stability": "draft", - "content_hash": "e8e3eb397095e91753b6235f52c849c6c4cd823433b2bd2a1dcc15fafbcbd268", + "content_hash": "782a8d5117940d55d50802e4cd26b34d1740a1b2d1a0503b6240c16b6bc66da9", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "e8e3eb397095e91753b6235f52c849c6c4cd823433b2bd2a1dcc15fafbcbd268" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "782a8d5117940d55d50802e4cd26b34d1740a1b2d1a0503b6240c16b6bc66da9" } } diff --git a/contracts/schema-publication/entries/participant-shared-state-record-v1.json b/contracts/schema-publication/entries/participant-shared-state-record-v1.json index 607352c1a..858df1888 100644 --- a/contracts/schema-publication/entries/participant-shared-state-record-v1.json +++ b/contracts/schema-publication/entries/participant-shared-state-record-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-shared-state-record-v1", "schema_path": "contracts/schemas/participant-runtime/participant-shared-state-record-v1.json", "stability": "draft", - "content_hash": "af85858ae18c680d570a24c2991d06a98d13dd2901f2dd287ff2e50080c1a200", + "content_hash": "525cb514917251807cb5c1cdf52dca0118663416e59f36da35045e8cfed8d31b", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "af85858ae18c680d570a24c2991d06a98d13dd2901f2dd287ff2e50080c1a200" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "525cb514917251807cb5c1cdf52dca0118663416e59f36da35045e8cfed8d31b" } } diff --git a/contracts/schema-publication/entries/participant-status-view-v1.json b/contracts/schema-publication/entries/participant-status-view-v1.json index d70ae8aee..49a0644df 100644 --- a/contracts/schema-publication/entries/participant-status-view-v1.json +++ b/contracts/schema-publication/entries/participant-status-view-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-status-view-v1", "schema_path": "contracts/schemas/control-plane/participant-status-view-v1.json", "stability": "draft", - "content_hash": "aef73b7d52c99de2a51cb2a58de0e30c37fc7fb2af5627cd6241a6aad288d07c", + "content_hash": "704e0997fa71fd8bc199f70034f769d301ccfd4d23a577dc676ae77fbae7758a", "last_change": { - "summary": "Initial publication of the participant status-view control-plane contract: the participant status/lifecycle projection in the backend-facing contract family (ADR-060, issue #76).", - "content_hash": "aef73b7d52c99de2a51cb2a58de0e30c37fc7fb2af5627cd6241a6aad288d07c" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "704e0997fa71fd8bc199f70034f769d301ccfd4d23a577dc676ae77fbae7758a" } } diff --git a/contracts/schema-publication/entries/participant-time-management-context-v1.json b/contracts/schema-publication/entries/participant-time-management-context-v1.json index 823933eec..1ad013e64 100644 --- a/contracts/schema-publication/entries/participant-time-management-context-v1.json +++ b/contracts/schema-publication/entries/participant-time-management-context-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-time-management-context-v1", "schema_path": "contracts/schemas/participant-runtime/participant-time-management-context-v1.json", "stability": "draft", - "content_hash": "a6d8e169d9adf1db043e81eca47d38c94c7560386d85f9005a3e4e8ed7ec0a69", + "content_hash": "f73a94028c86c301ea487b08ddf985b0badb03a91309e3028dfed05226614c3c", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "a6d8e169d9adf1db043e81eca47d38c94c7560386d85f9005a3e4e8ed7ec0a69" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "f73a94028c86c301ea487b08ddf985b0badb03a91309e3028dfed05226614c3c" } } diff --git a/contracts/schema-publication/entries/processor-manifest-v2.json b/contracts/schema-publication/entries/processor-manifest-v2.json index e33ec7646..b7b189d4c 100644 --- a/contracts/schema-publication/entries/processor-manifest-v2.json +++ b/contracts/schema-publication/entries/processor-manifest-v2.json @@ -2,9 +2,9 @@ "contract_id": "processor-manifest-v2", "schema_path": "contracts/schemas/processor-manifest/processor-manifest-v2.json", "stability": "draft", - "content_hash": "bd2e67b226d6eb18eb1cfec69e8840e7d8349161f3357fa71832771f9b8e68dc", + "content_hash": "a1afaba381dfec1a3e05467baee34335a15c3e8c6f9e2b6cd20fe99f0484a174", "last_change": { - "summary": "Added a typed, owner-governed apparatus configuration target registry and experiment binding contract support.", - "content_hash": "bd2e67b226d6eb18eb1cfec69e8840e7d8349161f3357fa71832771f9b8e68dc" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "a1afaba381dfec1a3e05467baee34335a15c3e8c6f9e2b6cd20fe99f0484a174" } } diff --git a/contracts/schema-publication/entries/proposition-truth-result-v1.json b/contracts/schema-publication/entries/proposition-truth-result-v1.json index dccca5ad6..718f974a9 100644 --- a/contracts/schema-publication/entries/proposition-truth-result-v1.json +++ b/contracts/schema-publication/entries/proposition-truth-result-v1.json @@ -2,9 +2,9 @@ "contract_id": "proposition-truth-result-v1", "schema_path": "contracts/schemas/control-plane/proposition-truth-result-v1.json", "stability": "draft", - "content_hash": "4a37fa964b8a7e3e641f381a27ea6960e9bc1efb8edfe426b9c19e173f7a045f", + "content_hash": "36ead895c2f3be5c975077ccaf4e75e8026fd0a9243c5a67a894999e4f424ac2", "last_change": { - "summary": "Initial publication of portable four-valued proposition truth results with typed evidence, provenance, temporal context, loss disclosure, and capability-bounded unsupported outcomes.", - "content_hash": "4a37fa964b8a7e3e641f381a27ea6960e9bc1efb8edfe426b9c19e173f7a045f" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "36ead895c2f3be5c975077ccaf4e75e8026fd0a9243c5a67a894999e4f424ac2" } } diff --git a/contracts/schema-publication/entries/provisioning-plan-v1.json b/contracts/schema-publication/entries/provisioning-plan-v1.json index a05dc9834..ac69cd29c 100644 --- a/contracts/schema-publication/entries/provisioning-plan-v1.json +++ b/contracts/schema-publication/entries/provisioning-plan-v1.json @@ -2,9 +2,9 @@ "contract_id": "provisioning-plan-v1", "schema_path": "contracts/schemas/plans/provisioning-plan-v1.json", "stability": "draft", - "content_hash": "7775808973fc934776a5d7ed073bb6f9dfb007c40197fee771e1767dc1f1745b", + "content_hash": "70af50f87d61eb5337ca6658b5e9e9d227a99f38fc66dead05bca5122200f003", "last_change": { - "summary": "Added the domain-controller-placement provisioning resource identity for typed identity-domain bootstrap intent (issue #845).", - "content_hash": "7775808973fc934776a5d7ed073bb6f9dfb007c40197fee771e1767dc1f1745b" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "70af50f87d61eb5337ca6658b5e9e9d227a99f38fc66dead05bca5122200f003" } } diff --git a/contracts/schema-publication/entries/raes-semantic-invariants-v1.json b/contracts/schema-publication/entries/raes-semantic-invariants-v1.json new file mode 100644 index 000000000..af9523d8e --- /dev/null +++ b/contracts/schema-publication/entries/raes-semantic-invariants-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "raes-semantic-invariants-v1", + "schema_path": "contracts/schemas/profiles/raes-semantic-invariants-v1.json", + "stability": "draft", + "content_hash": "cfe89c457ff1ac21334768ab82b30ce2b1e28eb165bb64a91d1b222e05ece943", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "cfe89c457ff1ac21334768ab82b30ce2b1e28eb165bb64a91d1b222e05ece943" + } +} diff --git a/contracts/schema-publication/entries/random-stream-profile-v1.json b/contracts/schema-publication/entries/random-stream-profile-v1.json index a27d64e68..68416488c 100644 --- a/contracts/schema-publication/entries/random-stream-profile-v1.json +++ b/contracts/schema-publication/entries/random-stream-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "random-stream-profile-v1", "schema_path": "contracts/schemas/profiles/random-stream-profile-v1.json", "stability": "draft", - "content_hash": "4c9f405bfa0dce89d463cb0780bd68847922acdbd0836822c033edf967ada0d3", + "content_hash": "28f4a59b393157c1995a5043029c9b109cc512a7e84d64784835619fdff40a59", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "4c9f405bfa0dce89d463cb0780bd68847922acdbd0836822c033edf967ada0d3" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "28f4a59b393157c1995a5043029c9b109cc512a7e84d64784835619fdff40a59" } } diff --git a/contracts/schema-publication/entries/random-stream-vector-v1.json b/contracts/schema-publication/entries/random-stream-vector-v1.json index 6e1df2df4..6a1158b66 100644 --- a/contracts/schema-publication/entries/random-stream-vector-v1.json +++ b/contracts/schema-publication/entries/random-stream-vector-v1.json @@ -2,9 +2,9 @@ "contract_id": "random-stream-vector-v1", "schema_path": "contracts/schemas/profiles/random-stream-vector-v1.json", "stability": "draft", - "content_hash": "211900771cefa8c7e21b6194850d776a367a32afa9b270bc4f9a71798c58476e", + "content_hash": "4936d1422c64f27ac0f454068db28e81e24f6ead50771bb305d67d2f94fa992e", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "211900771cefa8c7e21b6194850d776a367a32afa9b270bc4f9a71798c58476e" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "4936d1422c64f27ac0f454068db28e81e24f6ead50771bb305d67d2f94fa992e" } } diff --git a/contracts/schema-publication/entries/realization-envelope-v1.json b/contracts/schema-publication/entries/realization-envelope-v1.json index b124bc282..31c501348 100644 --- a/contracts/schema-publication/entries/realization-envelope-v1.json +++ b/contracts/schema-publication/entries/realization-envelope-v1.json @@ -2,9 +2,9 @@ "contract_id": "realization-envelope-v1", "schema_path": "contracts/schemas/realization-envelope/realization-envelope-v1.json", "stability": "draft", - "content_hash": "bdb320445010600d2f5e3d5c750504ca7b2244f22589468194d70a341d55981b", + "content_hash": "124da1052563cc1457232da763919f02c23f935a86e21e49c1e0d7b781606253", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "bdb320445010600d2f5e3d5c750504ca7b2244f22589468194d70a341d55981b" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "124da1052563cc1457232da763919f02c23f935a86e21e49c1e0d7b781606253" } } diff --git a/contracts/schema-publication/entries/realized-time-model-v1.json b/contracts/schema-publication/entries/realized-time-model-v1.json index 942fd5a40..aa9e56952 100644 --- a/contracts/schema-publication/entries/realized-time-model-v1.json +++ b/contracts/schema-publication/entries/realized-time-model-v1.json @@ -2,9 +2,9 @@ "contract_id": "realized-time-model-v1", "schema_path": "contracts/schemas/time/realized-time-model-v1.json", "stability": "draft", - "content_hash": "7b87524b4a23b981270b2501264c3911aaf1075280e3da136ab0b38393e895ed", + "content_hash": "0034fe68c2981896e4c8e08743bd9e18ae20f9aa9bde91d92f1af1b2dc0537ba", "last_change": { - "summary": "Published the EXP-734 run-scoped time realization and apparatus provenance contract.", - "content_hash": "7b87524b4a23b981270b2501264c3911aaf1075280e3da136ab0b38393e895ed" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "0034fe68c2981896e4c8e08743bd9e18ae20f9aa9bde91d92f1af1b2dc0537ba" } } diff --git a/contracts/schema-publication/entries/reference-models-v1.json b/contracts/schema-publication/entries/reference-models-v1.json index 23d034168..d72f89056 100644 --- a/contracts/schema-publication/entries/reference-models-v1.json +++ b/contracts/schema-publication/entries/reference-models-v1.json @@ -2,5 +2,9 @@ "contract_id": "reference-models-v1", "schema_path": "contracts/schemas/concept-authority/reference-models-v1.json", "stability": "draft", - "content_hash": "28ae8b46e4bcb4436a01adf3a70472b2f8ffc1fc8c13ff7ea8766492652f8267" + "content_hash": "e028dfbb74c458399eb3012b4abcb09bef09e047fc1dd74dd0f4dc1f20fa64f0", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "e028dfbb74c458399eb3012b4abcb09bef09e047fc1dd74dd0f4dc1f20fa64f0" + } } diff --git a/contracts/schema-publication/entries/reusable-asset-trust-policy-v1.json b/contracts/schema-publication/entries/reusable-asset-trust-policy-v1.json index b12e9de9b..aacececf1 100644 --- a/contracts/schema-publication/entries/reusable-asset-trust-policy-v1.json +++ b/contracts/schema-publication/entries/reusable-asset-trust-policy-v1.json @@ -2,9 +2,9 @@ "contract_id": "reusable-asset-trust-policy-v1", "schema_path": "contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json", "stability": "draft", - "content_hash": "fcd663287814f1c5072e3f5f4421c67edda0670965d6b04b5bc417501bba127c", + "content_hash": "855845de67295da833b426797dfbd60e13bafeaebc2b92e2a1c195748ccebeab", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "fcd663287814f1c5072e3f5f4421c67edda0670965d6b04b5bc417501bba127c" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "855845de67295da833b426797dfbd60e13bafeaebc2b92e2a1c195748ccebeab" } } diff --git a/contracts/schema-publication/entries/runtime-fact-binding-plane-v1.json b/contracts/schema-publication/entries/runtime-fact-binding-plane-v1.json index 7ee59eadf..7b17c4b15 100644 --- a/contracts/schema-publication/entries/runtime-fact-binding-plane-v1.json +++ b/contracts/schema-publication/entries/runtime-fact-binding-plane-v1.json @@ -2,9 +2,9 @@ "contract_id": "runtime-fact-binding-plane-v1", "schema_path": "contracts/schemas/participant-runtime/runtime-fact-binding-plane-v1.json", "stability": "draft", - "content_hash": "753c6eea97c86e7b083d05ed8ea32adf57fc2bd9145f55454cd35c5057630dbc", + "content_hash": "4ebbf42cc283ad2419d0bbb8724f30e437bec1b1cf1058b4c580475eb83f1699", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "753c6eea97c86e7b083d05ed8ea32adf57fc2bd9145f55454cd35c5057630dbc" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "4ebbf42cc283ad2419d0bbb8724f30e437bec1b1cf1058b4c580475eb83f1699" } } diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index 3ca616461..1e51470d7 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": "ce74f9bb39d44c18badc1451d7f4d363d58e3938de8e360dc020ea2d1e4bcdf7", + "content_hash": "f31a38dd78bf650e5751f8ae15019227faca3e39d8b7a20d1c32d713aaf90240", "last_change": { - "summary": "Added first-class append-only API-409 participant control history for RUN-310 supervisory lifecycle persistence and replay.", - "content_hash": "ce74f9bb39d44c18badc1451d7f4d363d58e3938de8e360dc020ea2d1e4bcdf7" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "f31a38dd78bf650e5751f8ae15019227faca3e39d8b7a20d1c32d713aaf90240" } } diff --git a/contracts/schema-publication/entries/scenario-instantiation-request-v1.json b/contracts/schema-publication/entries/scenario-instantiation-request-v1.json index 989de1604..a94c67f40 100644 --- a/contracts/schema-publication/entries/scenario-instantiation-request-v1.json +++ b/contracts/schema-publication/entries/scenario-instantiation-request-v1.json @@ -2,9 +2,9 @@ "contract_id": "scenario-instantiation-request-v1", "schema_path": "contracts/schemas/sdl/scenario-instantiation-request-v1.json", "stability": "draft", - "content_hash": "130a103664dd69819c1dbe13437e94436dfb2b5db871f135e9f1750c741b412f", + "content_hash": "54a8cdf1108003f61b79b9d9c0f0a7321a4078a55cf81f604c6882dd420b6616", "last_change": { - "summary": "Constrained instantiation parameter names to portable local identifiers while leaving parameter values outside declaration identity for DSL-101.", - "content_hash": "130a103664dd69819c1dbe13437e94436dfb2b5db871f135e9f1750c741b412f" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "54a8cdf1108003f61b79b9d9c0f0a7321a4078a55cf81f604c6882dd420b6616" } } diff --git a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json index 74fe6fb5e..0743bc1cb 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": "01e502976847abfed1d37f7b6e972971d9109fac34e4e94ef31b5971a0a93032", + "content_hash": "83cfeca54fae16bbdfaad20f15652aaf384e4b2477f382aa0169c084b165ca69", "last_change": { - "summary": "Carried participant-directed inject deliveries and governed participant action-argument domains into satisfiability evidence on the RAES-only package surface.", - "content_hash": "01e502976847abfed1d37f7b6e972971d9109fac34e4e94ef31b5971a0a93032" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "83cfeca54fae16bbdfaad20f15652aaf384e4b2477f382aa0169c084b165ca69" } } diff --git a/contracts/schema-publication/entries/scheduler-isolation-proof-v1.json b/contracts/schema-publication/entries/scheduler-isolation-proof-v1.json index 030c6b63e..90d554bef 100644 --- a/contracts/schema-publication/entries/scheduler-isolation-proof-v1.json +++ b/contracts/schema-publication/entries/scheduler-isolation-proof-v1.json @@ -2,9 +2,9 @@ "contract_id": "scheduler-isolation-proof-v1", "schema_path": "contracts/schemas/control-plane/scheduler-isolation-proof-v1.json", "stability": "draft", - "content_hash": "8c2fae279f0446a01c11e8188c5f45dd3ee459526bcc3fb6b6cdf38cafce49a8", + "content_hash": "ebf37a997518da860d7e023aef431a2d7eb5c1ea9888d88d015ed29ab1a3fd72", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "8c2fae279f0446a01c11e8188c5f45dd3ee459526bcc3fb6b6cdf38cafce49a8" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "ebf37a997518da860d7e023aef431a2d7eb5c1ea9888d88d015ed29ab1a3fd72" } } diff --git a/contracts/schema-publication/entries/scientific-completeness-assessment-v1.json b/contracts/schema-publication/entries/scientific-completeness-assessment-v1.json index 7148013ee..87878c896 100644 --- a/contracts/schema-publication/entries/scientific-completeness-assessment-v1.json +++ b/contracts/schema-publication/entries/scientific-completeness-assessment-v1.json @@ -2,9 +2,9 @@ "contract_id": "scientific-completeness-assessment-v1", "schema_path": "contracts/schemas/profiles/scientific-completeness-assessment-v1.json", "stability": "draft", - "content_hash": "b475160b099d20a14916a39a09ec5d4049547978edc69e50031081c688ca7da6", + "content_hash": "77d73cf5f338802afd8d3d62552148d45e22af0eac562b8d2aaa86a7043ef1de", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "b475160b099d20a14916a39a09ec5d4049547978edc69e50031081c688ca7da6" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "77d73cf5f338802afd8d3d62552148d45e22af0eac562b8d2aaa86a7043ef1de" } } diff --git a/contracts/schema-publication/entries/scientific-completeness-taxonomy-v1.json b/contracts/schema-publication/entries/scientific-completeness-taxonomy-v1.json index 1f63ef5b1..0ed34d232 100644 --- a/contracts/schema-publication/entries/scientific-completeness-taxonomy-v1.json +++ b/contracts/schema-publication/entries/scientific-completeness-taxonomy-v1.json @@ -2,9 +2,9 @@ "contract_id": "scientific-completeness-taxonomy-v1", "schema_path": "contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json", "stability": "draft", - "content_hash": "7c0904b5ef9b791e3eedbe758755ea3ed0f4eabe0db3c40d9a75eb507823fd90", + "content_hash": "8236164dfb48091b5e3dd8ca187ea6ee4df07ad8da48bc6b84628c74c166b129", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "7c0904b5ef9b791e3eedbe758755ea3ed0f4eabe0db3c40d9a75eb507823fd90" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "8236164dfb48091b5e3dd8ca187ea6ee4df07ad8da48bc6b84628c74c166b129" } } diff --git a/contracts/schema-publication/entries/sdl-authoring-input-v1.json b/contracts/schema-publication/entries/sdl-authoring-input-v1.json index 57098c56a..9d16b3d9e 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": "65ae337e1fbe2a4b66e8d0cbbb98c66c172360d328fd82c5352a223a1e6429cc", + "content_hash": "c12f8eb2819b43c2b2ed15de3e7fd71fcb4e039a25b59512de30099c28c85515", "last_change": { - "summary": "Published closed participant-directed inject delivery and governed participant action-argument authoring semantics on the RAES-only package surface.", - "content_hash": "65ae337e1fbe2a4b66e8d0cbbb98c66c172360d328fd82c5352a223a1e6429cc" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "c12f8eb2819b43c2b2ed15de3e7fd71fcb4e039a25b59512de30099c28c85515" } } diff --git a/contracts/schema-publication/entries/sdl-lineage-ledger-v1.json b/contracts/schema-publication/entries/sdl-lineage-ledger-v1.json index c456f82a1..7de4a7752 100644 --- a/contracts/schema-publication/entries/sdl-lineage-ledger-v1.json +++ b/contracts/schema-publication/entries/sdl-lineage-ledger-v1.json @@ -2,9 +2,9 @@ "contract_id": "sdl-lineage-ledger-v1", "schema_path": "contracts/schemas/provenance/sdl-lineage-ledger-v1.json", "stability": "draft", - "content_hash": "ac8a7737debd83fe70cfe791a85f72455d6e474c806e3faf9692cdce38123e6d", + "content_hash": "9cbb6dc73655256fb149d2fdabea9af41fae71d56852f0afa528f28ddfdeaf20", "last_change": { - "summary": "Initial publication of the revision-pinned SDL lineage, derivation, citation identity, compatibility, and third-party notice-disposition contract for issue #726.", - "content_hash": "ac8a7737debd83fe70cfe791a85f72455d6e474c806e3faf9692cdce38123e6d" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "9cbb6dc73655256fb149d2fdabea9af41fae71d56852f0afa528f28ddfdeaf20" } } diff --git a/contracts/schema-publication/entries/semantic-profile-v1.json b/contracts/schema-publication/entries/semantic-profile-v1.json index efdde7c7c..279ea9fbf 100644 --- a/contracts/schema-publication/entries/semantic-profile-v1.json +++ b/contracts/schema-publication/entries/semantic-profile-v1.json @@ -2,5 +2,9 @@ "contract_id": "semantic-profile-v1", "schema_path": "contracts/schemas/profiles/semantic-profile-v1.json", "stability": "draft", - "content_hash": "ca78369c63879f21cf57b3ee29e2015163dbdc6a17d496b22ea98b1bd0b38bce" + "content_hash": "a3d9fdfea509aad4ea13977457a6467771c33126d2a43cf553161065114d6b90", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "a3d9fdfea509aad4ea13977457a6467771c33126d2a43cf553161065114d6b90" + } } diff --git a/contracts/schema-publication/entries/time-model-v1.json b/contracts/schema-publication/entries/time-model-v1.json index dd1e3bc14..f710d0c46 100644 --- a/contracts/schema-publication/entries/time-model-v1.json +++ b/contracts/schema-publication/entries/time-model-v1.json @@ -2,9 +2,9 @@ "contract_id": "time-model-v1", "schema_path": "contracts/schemas/time/time-model-v1.json", "stability": "draft", - "content_hash": "5978b5a9afc8f27af066803d9b7628681b07db7a4b9a2cad66a87dd148e47e9f", + "content_hash": "14b9679439dd53c3a0e70f1518c3271ecef729032d2085f0b38707271204870a", "last_change": { - "summary": "Published the API-421 backend-neutral shared-time declaration contract.", - "content_hash": "5978b5a9afc8f27af066803d9b7628681b07db7a4b9a2cad66a87dd148e47e9f" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "14b9679439dd53c3a0e70f1518c3271ecef729032d2085f0b38707271204870a" } } diff --git a/contracts/schema-publication/entries/time-runtime-state-v1.json b/contracts/schema-publication/entries/time-runtime-state-v1.json index 528d50808..76156d582 100644 --- a/contracts/schema-publication/entries/time-runtime-state-v1.json +++ b/contracts/schema-publication/entries/time-runtime-state-v1.json @@ -2,9 +2,9 @@ "contract_id": "time-runtime-state-v1", "schema_path": "contracts/schemas/time/time-runtime-state-v1.json", "stability": "draft", - "content_hash": "69206696ef491df49037dcbaaf25a7b7a7766f87f142b743ca69d2b073cc9f57", + "content_hash": "f5092734db63333c6ffd1120730bc5b3343c90fa2ae6056d2e9a51194e4bca1f", "last_change": { - "summary": "Published the ASR-528 typed clock readback and append-only transition history contract.", - "content_hash": "69206696ef491df49037dcbaaf25a7b7a7766f87f142b743ca69d2b073cc9f57" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "f5092734db63333c6ffd1120730bc5b3343c90fa2ae6056d2e9a51194e4bca1f" } } diff --git a/contracts/schema-publication/entries/trial-cleanup-plan-v1.json b/contracts/schema-publication/entries/trial-cleanup-plan-v1.json index a57289759..ecd1cbd3d 100644 --- a/contracts/schema-publication/entries/trial-cleanup-plan-v1.json +++ b/contracts/schema-publication/entries/trial-cleanup-plan-v1.json @@ -2,9 +2,9 @@ "contract_id": "trial-cleanup-plan-v1", "schema_path": "contracts/schemas/plans/trial-cleanup-plan-v1.json", "stability": "draft", - "content_hash": "7e0a814f64946f5de1de4b5f540d42e401904202ee608f2015f85df15aee510b", + "content_hash": "4d0ced18dc531cd5e5619bfe5558b49ad5856b5b94afc3d6ca40a4475aedc032", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "7e0a814f64946f5de1de4b5f540d42e401904202ee608f2015f85df15aee510b" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "4d0ced18dc531cd5e5619bfe5558b49ad5856b5b94afc3d6ca40a4475aedc032" } } diff --git a/contracts/schema-publication/entries/trial-cleanup-receipt-v1.json b/contracts/schema-publication/entries/trial-cleanup-receipt-v1.json index 7ce16ca43..a6a8455fb 100644 --- a/contracts/schema-publication/entries/trial-cleanup-receipt-v1.json +++ b/contracts/schema-publication/entries/trial-cleanup-receipt-v1.json @@ -2,9 +2,9 @@ "contract_id": "trial-cleanup-receipt-v1", "schema_path": "contracts/schemas/control-plane/trial-cleanup-receipt-v1.json", "stability": "draft", - "content_hash": "5714bdc219d2f19fb4c69ce64076b671ba85332462d63ac4c7e31f863fb3f8cd", + "content_hash": "90462009caf9c41e35f764ac9be4e5777d7745994d7d04195ac6a08851a2d690", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "5714bdc219d2f19fb4c69ce64076b671ba85332462d63ac4c7e31f863fb3f8cd" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "90462009caf9c41e35f764ac9be4e5777d7745994d7d04195ac6a08851a2d690" } } diff --git a/contracts/schema-publication/entries/uco-alignment-v1.json b/contracts/schema-publication/entries/uco-alignment-v1.json index f4407e714..b8c7ec027 100644 --- a/contracts/schema-publication/entries/uco-alignment-v1.json +++ b/contracts/schema-publication/entries/uco-alignment-v1.json @@ -2,9 +2,9 @@ "contract_id": "uco-alignment-v1", "schema_path": "contracts/schemas/concept-authority/uco-alignment-v1.json", "stability": "draft", - "content_hash": "445ef9ba99cddaae2b3308a054d26dd968d194af1c2f98710151f2d5a747f5bf", + "content_hash": "dc84dac9987d237a7309a15289ee3572dc1096fa5c0ae6b06201e770a1de0086", "last_change": { - "summary": "Initial publication of the UCO alignment evidence contract (concept-authority review CA-5, issue #495): maps adopted/adapted cyber-domain concept families to pinned UCO 1.4.0 object types, with adapted-family divergences enumerated.", - "content_hash": "445ef9ba99cddaae2b3308a054d26dd968d194af1c2f98710151f2d5a747f5bf" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "dc84dac9987d237a7309a15289ee3572dc1096fa5c0ae6b06201e770a1de0086" } } diff --git a/contracts/schema-publication/entries/validation-basis-disclosure-v1.json b/contracts/schema-publication/entries/validation-basis-disclosure-v1.json index d3aa8c84e..c975e47ad 100644 --- a/contracts/schema-publication/entries/validation-basis-disclosure-v1.json +++ b/contracts/schema-publication/entries/validation-basis-disclosure-v1.json @@ -2,9 +2,9 @@ "contract_id": "validation-basis-disclosure-v1", "schema_path": "contracts/schemas/profiles/validation-basis-disclosure-v1.json", "stability": "draft", - "content_hash": "f24834c634f9cf1fc30dd16e5649b7898dc2e867ab7c80b30652e8a75ebfc49b", + "content_hash": "df23116b3c073e6db1c00ef2cbe419eb63af44a938095616ebfa799b39f59f69", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "f24834c634f9cf1fc30dd16e5649b7898dc2e867ab7c80b30652e8a75ebfc49b" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "df23116b3c073e6db1c00ef2cbe419eb63af44a938095616ebfa799b39f59f69" } } diff --git a/contracts/schema-publication/entries/validation-profile-catalog-v1.json b/contracts/schema-publication/entries/validation-profile-catalog-v1.json index 5a63ce3d2..499f9c6a2 100644 --- a/contracts/schema-publication/entries/validation-profile-catalog-v1.json +++ b/contracts/schema-publication/entries/validation-profile-catalog-v1.json @@ -2,9 +2,9 @@ "contract_id": "validation-profile-catalog-v1", "schema_path": "contracts/schemas/profiles/validation-profile-catalog-v1.json", "stability": "draft", - "content_hash": "6c5ba8ad4fa6d5841e68b09742b1ced48455fe3617d2cc1bf4a9c8c1ef0c9bc9", + "content_hash": "426679f49e406a66b4b5bd9dde39ba4f01afcb0360161cf6d489cf197c85cd17", "last_change": { - "summary": "Retargeted qualified Python validator references and current project descriptions from the retired ACES package identity to RAES as part of the #884 hard cut; schema identifiers and validation semantics are unchanged.", - "content_hash": "6c5ba8ad4fa6d5841e68b09742b1ced48455fe3617d2cc1bf4a9c8c1ef0c9bc9" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "426679f49e406a66b4b5bd9dde39ba4f01afcb0360161cf6d489cf197c85cd17" } } diff --git a/contracts/schema-publication/entries/workflow-cancellation-request-v1.json b/contracts/schema-publication/entries/workflow-cancellation-request-v1.json index a5498b5de..c4a5d0ea8 100644 --- a/contracts/schema-publication/entries/workflow-cancellation-request-v1.json +++ b/contracts/schema-publication/entries/workflow-cancellation-request-v1.json @@ -2,5 +2,9 @@ "contract_id": "workflow-cancellation-request-v1", "schema_path": "contracts/schemas/control-plane/workflow-cancellation-request-v1.json", "stability": "draft", - "content_hash": "89ab39ece6fb1473a0f967a50186b29e0c6581257db3ad113324b31d05401435" + "content_hash": "32f9286d95c824af920799fcac005851a46d93b437ec3ece04da2a58b7641178", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "32f9286d95c824af920799fcac005851a46d93b437ec3ece04da2a58b7641178" + } } diff --git a/contracts/schema-publication/entries/workflow-history-event-stream-v1.json b/contracts/schema-publication/entries/workflow-history-event-stream-v1.json index 8de28f1a9..ed1a61c3c 100644 --- a/contracts/schema-publication/entries/workflow-history-event-stream-v1.json +++ b/contracts/schema-publication/entries/workflow-history-event-stream-v1.json @@ -2,5 +2,9 @@ "contract_id": "workflow-history-event-stream-v1", "schema_path": "contracts/schemas/control-plane/workflow-history-event-stream-v1.json", "stability": "draft", - "content_hash": "6fa5f0a9b87da0cc1582204dc1324c5d7ffa30c2171aba74aedbbda7e109c243" + "content_hash": "32f8eb4cd8800a93374f1f76e497b1b96f08e583f4aa2d34775e915c1112a68b", + "last_change": { + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "32f8eb4cd8800a93374f1f76e497b1b96f08e583f4aa2d34775e915c1112a68b" + } } diff --git a/contracts/schema-publication/entries/workflow-result-envelope-v1.json b/contracts/schema-publication/entries/workflow-result-envelope-v1.json index 27384b779..83ba17d87 100644 --- a/contracts/schema-publication/entries/workflow-result-envelope-v1.json +++ b/contracts/schema-publication/entries/workflow-result-envelope-v1.json @@ -2,9 +2,9 @@ "contract_id": "workflow-result-envelope-v1", "schema_path": "contracts/schemas/control-plane/workflow-result-envelope-v1.json", "stability": "draft", - "content_hash": "587d87619359a921ff369662e2af090cf49c764694cb855f02188358f298c66c", + "content_hash": "0a0db3f0da692b83f72a312eb75ffecc25fb74b56600c7ecb64a759f8a3b4c28", "last_change": { - "summary": "Published SCE-004 action-attempt, tool-selection, fact-version, outcome, evidence, and assertion-truth provenance.", - "content_hash": "587d87619359a921ff369662e2af090cf49c764694cb855f02188358f298c66c" + "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", + "content_hash": "0a0db3f0da692b83f72a312eb75ffecc25fb74b56600c7ecb64a759f8a3b4c28" } } diff --git a/contracts/schema-publication/tombstones/aces-semantic-invariants-v1.json b/contracts/schema-publication/tombstones/aces-semantic-invariants-v1.json new file mode 100644 index 000000000..b554c5959 --- /dev/null +++ b/contracts/schema-publication/tombstones/aces-semantic-invariants-v1.json @@ -0,0 +1,4 @@ +{ + "schema_path": "contracts/schemas/profiles/aces-semantic-invariants-v1.json", + "summary": "Removed the retired semantic-invariant profile path during the ecosystem-wide RAES hard cut in issue #908. The replacement is contracts/schemas/profiles/raes-semantic-invariants-v1.json; no compatibility alias is published." +} diff --git a/contracts/schemas/README.md b/contracts/schemas/README.md index 9ad9e8067..dad720a6e 100644 --- a/contracts/schemas/README.md +++ b/contracts/schemas/README.md @@ -30,7 +30,7 @@ Current published schemas cover: `sdl/sdl-authoring-input-v1.json` begins after `sdl-yaml/v1` source decoding, canonical-field recognition, shorthand expansion, enum normalization, and typed -construction. Its title and `x-aces-document-phase` annotation state that +construction. Its title and `x-raes-document-phase` annotation state that boundary. Raw YAML properties such as duplicate keys, tags, directives, anchors, aliases, Core scalar resolution, and resource limits are specified in `specs/sdl/document-model.md` and tested by @@ -40,8 +40,8 @@ anchors, aliases, Core scalar resolution, and resource limits are specified in contract. It requires `instantiation_provenance`, forbids `variables`, `imports`, and `module` even when empty/null, and forbids a `${name}` token in every string value. `sdl/instantiated-scenario-snapshot-v1.json` adds the -required `aces-sdl-instantiated-snapshot/v1` canonical profile envelope. The -`x-aces-realization-dimension: false` annotation on +required `raes-sdl-instantiated-snapshot/v1` canonical profile envelope. The +`x-raes-realization-dimension: false` annotation on `instantiation_provenance` declares that this required exchange metadata is excluded only from realization-envelope child-dimension enumeration; it remains part of validation and canonical identity. The @@ -244,10 +244,10 @@ Run traceability and realized-form disclosure invariants keep claims grounded in evidence/derived-measure refs and keep realized choices distinct from authored scenario meaning and result values. Cross-artifact or graph invariants that standard JSON Schema cannot express are -published under the RAES semantic-invariant profile with `x-aces-invariants` +published under the RAES semantic-invariant profile with `x-raes-invariants` entries that name the validator and input contract paths. The generated schemas declare draft 2020-12 identity, and the annotation profile shape is published as -`aces-semantic-invariants-v1` and checked during generation. Generic JSON Schema +`raes-semantic-invariants-v1` and checked during generation. Generic JSON Schema validation remains structural; consumers of experiment-core records must apply the named semantic validators before accepting records as RAES-conformant. diff --git a/contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json b/contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json index a51177489..ecce95638 100644 --- a/contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json +++ b/contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json @@ -24,7 +24,7 @@ }, "ReusableAssetEvidenceRequirementModel": { "additionalProperties": false, - "description": "One evidence-class expectation an asset family must satisfy.\n\n``mechanism_ref`` names the *existing* RAES mechanism that carries the\nevidence (e.g. ``aces.lock.json`` digest pins, ``ExperimentChecksumModel``,\n``controlled-vocabularies-v1.source``). GOV-913 declares policy over the\nincumbent mechanisms; it does not introduce a parallel evidence store, so\nthis contract never carries the evidence payload itself \u2014 only the\nrequirement and a reference to where the evidence lives.", + "description": "One evidence-class expectation an asset family must satisfy.\n\n``mechanism_ref`` names the *existing* RAES mechanism that carries the\nevidence (e.g. ``raes.lock.json`` digest pins, ``ExperimentChecksumModel``,\n``controlled-vocabularies-v1.source``). GOV-913 declares policy over the\nincumbent mechanisms; it does not introduce a parallel evidence store, so\nthis contract never carries the evidence payload itself \u2014 only the\nrequirement and a reference to where the evidence lives.", "properties": { "description": { "minLength": 1, @@ -312,7 +312,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/reusable-asset-trust-policy-v1.json", + "$id": "https://raes.dev/schemas/reusable-asset-trust-policy-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Ecosystem trust/authenticity/integrity policy over reusable assets (GOV-913).\n\nA declarative, expectation-based policy: it declares, per asset family, the\nintegrity/authenticity/provenance/governance evidence the ecosystem requires,\nreferencing the existing RAES mechanisms that carry that evidence. It is not a\nper-asset trust record and it invents no cryptography. See\n``specs/authority/reusable-asset-trust-integrity.md`` (normative) and ADR-071.", diff --git a/contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json b/contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json index 888383ef8..484b750da 100644 --- a/contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json +++ b/contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json @@ -327,7 +327,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/associated-artifact-manifest-v1.json", + "$id": "https://raes.dev/schemas/associated-artifact-manifest-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -449,7 +449,7 @@ ], "title": "AssociatedArtifactManifestModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Full conformance requires matching the concrete parent, recomputing the canonical set digest, and binding every checksum and size to an explicitly supplied bounded byte stream.", "id": "associated-artifact-parent-set-and-byte-binding", @@ -463,13 +463,13 @@ "validator": "raes_contracts.associated_artifacts.validate_associated_artifact_manifest" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "associated-artifact-manifest-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/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 11b6c1de7..149884a4b 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -1432,7 +1432,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/backend-manifest-v2.json", + "$id": "https://raes.dev/schemas/backend-manifest-v2.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ diff --git a/contracts/schemas/concept-authority/atlas-tactics-source-v1.json b/contracts/schemas/concept-authority/atlas-tactics-source-v1.json index b6ad2cc5d..863404d28 100644 --- a/contracts/schemas/concept-authority/atlas-tactics-source-v1.json +++ b/contracts/schemas/concept-authority/atlas-tactics-source-v1.json @@ -90,7 +90,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/atlas-tactics-source-v1.json", + "$id": "https://raes.dev/schemas/atlas-tactics-source-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/concept-authority/attack-enterprise-tactics-source-v1.json b/contracts/schemas/concept-authority/attack-enterprise-tactics-source-v1.json index 1d5ed6ae3..7ddce7c3f 100644 --- a/contracts/schemas/concept-authority/attack-enterprise-tactics-source-v1.json +++ b/contracts/schemas/concept-authority/attack-enterprise-tactics-source-v1.json @@ -46,7 +46,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/attack-enterprise-tactics-source-v1.json", + "$id": "https://raes.dev/schemas/attack-enterprise-tactics-source-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/concept-authority/behavioral-relations-v1.json b/contracts/schemas/concept-authority/behavioral-relations-v1.json index 262535f64..701ebe950 100644 --- a/contracts/schemas/concept-authority/behavioral-relations-v1.json +++ b/contracts/schemas/concept-authority/behavioral-relations-v1.json @@ -698,7 +698,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/behavioral-relations-v1.json", + "$id": "https://raes.dev/schemas/behavioral-relations-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { @@ -735,8 +735,8 @@ "type": "string" }, "taxonomy_id": { - "const": "aces-behavioral-relations", - "default": "aces-behavioral-relations", + "const": "raes-behavioral-relations", + "default": "raes-behavioral-relations", "title": "Taxonomy Id", "type": "string" }, @@ -765,7 +765,7 @@ ], "title": "BehavioralRelationCatalogModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Relation map keys, bibliography references, claim-surface relation references, and worked-example keys must resolve exactly inside one taxonomy revision.", "id": "behavioral-relations-reference-resolution", @@ -779,13 +779,13 @@ "validator": "raes_contracts.behavioral_relations.BehavioralRelationCatalogModel" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "behavioral-relations-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/concept-authority/concept-families-v1.json b/contracts/schemas/concept-authority/concept-families-v1.json index 51983ba76..7ed41c54f 100644 --- a/contracts/schemas/concept-authority/concept-families-v1.json +++ b/contracts/schemas/concept-authority/concept-families-v1.json @@ -198,7 +198,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/concept-families-v1.json", + "$id": "https://raes.dev/schemas/concept-families-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/concept-authority/controlled-vocabularies-v1.json b/contracts/schemas/concept-authority/controlled-vocabularies-v1.json index 25b0d23a7..15bf16b79 100644 --- a/contracts/schemas/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/schemas/concept-authority/controlled-vocabularies-v1.json @@ -201,7 +201,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/controlled-vocabularies-v1.json", + "$id": "https://raes.dev/schemas/controlled-vocabularies-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/concept-authority/nist-csf-defensive-categories-source-v1.json b/contracts/schemas/concept-authority/nist-csf-defensive-categories-source-v1.json index 1c95887a1..6ea515e38 100644 --- a/contracts/schemas/concept-authority/nist-csf-defensive-categories-source-v1.json +++ b/contracts/schemas/concept-authority/nist-csf-defensive-categories-source-v1.json @@ -44,7 +44,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/nist-csf-defensive-categories-source-v1.json", + "$id": "https://raes.dev/schemas/nist-csf-defensive-categories-source-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/concept-authority/reference-models-v1.json b/contracts/schemas/concept-authority/reference-models-v1.json index 5c1a63c2c..307e1938e 100644 --- a/contracts/schemas/concept-authority/reference-models-v1.json +++ b/contracts/schemas/concept-authority/reference-models-v1.json @@ -77,7 +77,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/reference-models-v1.json", + "$id": "https://raes.dev/schemas/reference-models-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/concept-authority/uco-alignment-v1.json b/contracts/schemas/concept-authority/uco-alignment-v1.json index dd28e576a..168c0bb74 100644 --- a/contracts/schemas/concept-authority/uco-alignment-v1.json +++ b/contracts/schemas/concept-authority/uco-alignment-v1.json @@ -76,7 +76,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/uco-alignment-v1.json", + "$id": "https://raes.dev/schemas/uco-alignment-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/control-plane/evaluation-history-event-stream-v1.json b/contracts/schemas/control-plane/evaluation-history-event-stream-v1.json index 9416f6d7c..fe1927475 100644 --- a/contracts/schemas/control-plane/evaluation-history-event-stream-v1.json +++ b/contracts/schemas/control-plane/evaluation-history-event-stream-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/evaluation-history-event-stream-v1.json", + "$id": "https://raes.dev/schemas/evaluation-history-event-stream-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "items": { "additionalProperties": false, diff --git a/contracts/schemas/control-plane/evaluation-result-envelope-v1.json b/contracts/schemas/control-plane/evaluation-result-envelope-v1.json index 07803c47a..98b0b870d 100644 --- a/contracts/schemas/control-plane/evaluation-result-envelope-v1.json +++ b/contracts/schemas/control-plane/evaluation-result-envelope-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/evaluation-result-envelope-v1.json", + "$id": "https://raes.dev/schemas/evaluation-result-envelope-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/control-plane/operation-receipt-v1.json b/contracts/schemas/control-plane/operation-receipt-v1.json index 0e39bc190..ac0a08dd2 100644 --- a/contracts/schemas/control-plane/operation-receipt-v1.json +++ b/contracts/schemas/control-plane/operation-receipt-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/operation-receipt-v1.json", + "$id": "https://raes.dev/schemas/operation-receipt-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/control-plane/operation-status-v1.json b/contracts/schemas/control-plane/operation-status-v1.json index ab21c4175..201a20fd8 100644 --- a/contracts/schemas/control-plane/operation-status-v1.json +++ b/contracts/schemas/control-plane/operation-status-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/operation-status-v1.json", + "$id": "https://raes.dev/schemas/operation-status-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json b/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json index 5e3808aa0..6c26091aa 100644 --- a/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json +++ b/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json @@ -992,7 +992,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/participant-behavior-history-event-stream-v1.json", + "$id": "https://raes.dev/schemas/participant-behavior-history-event-stream-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "items": { "additionalProperties": false, diff --git a/contracts/schemas/control-plane/participant-context-view-v1.json b/contracts/schemas/control-plane/participant-context-view-v1.json index c9901a2f2..144057f20 100644 --- a/contracts/schemas/control-plane/participant-context-view-v1.json +++ b/contracts/schemas/control-plane/participant-context-view-v1.json @@ -236,7 +236,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-context-view-v1.json", + "$id": "https://raes.dev/schemas/participant-context-view-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -476,7 +476,7 @@ ], "title": "ParticipantContextViewModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Participant-visible context views drawing on an archival evidence_record or derived_measure source layer must mediate that source through transformation.input_source_ids.", "id": "context-view-sem216-archival-source-mediated", @@ -502,13 +502,13 @@ "validator": "raes_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "participant-context-view-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/control-plane/participant-decision-surface-v1.json b/contracts/schemas/control-plane/participant-decision-surface-v1.json index 2b9bfb315..3ec952df3 100644 --- a/contracts/schemas/control-plane/participant-decision-surface-v1.json +++ b/contracts/schemas/control-plane/participant-decision-surface-v1.json @@ -665,7 +665,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-decision-surface-v1.json", + "$id": "https://raes.dev/schemas/participant-decision-surface-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "One participant-local decision projection at one episode order point.", @@ -875,7 +875,7 @@ ], "title": "ParticipantDecisionSurfaceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Candidate, constrained-form, and open-ended action references must resolve to action entries and their governed selection shapes.", "id": "decision-surface-entry-reference-agreement", @@ -913,13 +913,13 @@ "validator": "raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_surface_relations" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "participant-decision-surface-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/control-plane/participant-episode-history-event-stream-v1.json b/contracts/schemas/control-plane/participant-episode-history-event-stream-v1.json index dc2b22435..dff63a2e9 100644 --- a/contracts/schemas/control-plane/participant-episode-history-event-stream-v1.json +++ b/contracts/schemas/control-plane/participant-episode-history-event-stream-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/participant-episode-history-event-stream-v1.json", + "$id": "https://raes.dev/schemas/participant-episode-history-event-stream-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "items": { "additionalProperties": false, diff --git a/contracts/schemas/control-plane/participant-episode-state-envelope-v1.json b/contracts/schemas/control-plane/participant-episode-state-envelope-v1.json index fc20739b8..a2625ca4f 100644 --- a/contracts/schemas/control-plane/participant-episode-state-envelope-v1.json +++ b/contracts/schemas/control-plane/participant-episode-state-envelope-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/participant-episode-state-envelope-v1.json", + "$id": "https://raes.dev/schemas/participant-episode-state-envelope-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/control-plane/participant-history-view-v1.json b/contracts/schemas/control-plane/participant-history-view-v1.json index 7fb10cb17..82786c6d1 100644 --- a/contracts/schemas/control-plane/participant-history-view-v1.json +++ b/contracts/schemas/control-plane/participant-history-view-v1.json @@ -1298,7 +1298,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/participant-history-view-v1.json", + "$id": "https://raes.dev/schemas/participant-history-view-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ diff --git a/contracts/schemas/control-plane/participant-status-view-v1.json b/contracts/schemas/control-plane/participant-status-view-v1.json index e5f47935a..9b4fd2852 100644 --- a/contracts/schemas/control-plane/participant-status-view-v1.json +++ b/contracts/schemas/control-plane/participant-status-view-v1.json @@ -78,7 +78,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-status-view-v1.json", + "$id": "https://raes.dev/schemas/participant-status-view-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "API-408 retrieval projection of one participant's episode status.", diff --git a/contracts/schemas/control-plane/proposition-truth-result-v1.json b/contracts/schemas/control-plane/proposition-truth-result-v1.json index 4c0a76a30..49df88839 100644 --- a/contracts/schemas/control-plane/proposition-truth-result-v1.json +++ b/contracts/schemas/control-plane/proposition-truth-result-v1.json @@ -156,7 +156,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/proposition-truth-result-v1.json", + "$id": "https://raes.dev/schemas/proposition-truth-result-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ diff --git a/contracts/schemas/control-plane/scheduler-isolation-proof-v1.json b/contracts/schemas/control-plane/scheduler-isolation-proof-v1.json index e95897d2c..f6a5f3446 100644 --- a/contracts/schemas/control-plane/scheduler-isolation-proof-v1.json +++ b/contracts/schemas/control-plane/scheduler-isolation-proof-v1.json @@ -40,7 +40,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/scheduler-isolation-proof-v1.json", + "$id": "https://raes.dev/schemas/scheduler-isolation-proof-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -235,7 +235,7 @@ ], "title": "SchedulerIsolationProofModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Scheduling defaults to serial; bounded parallelism requires independent evidence for range, capacity, ports, storage, control-plane locks, and cleanup.", "id": "scheduler-isolation-serial-default-and-complete-parallel-proof", @@ -249,13 +249,13 @@ "validator": "raes_contracts.contracts.SchedulerIsolationProofModel._validate_isolation" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "scheduler-isolation-proof-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/control-plane/trial-cleanup-receipt-v1.json b/contracts/schemas/control-plane/trial-cleanup-receipt-v1.json index da62462d9..732017860 100644 --- a/contracts/schemas/control-plane/trial-cleanup-receipt-v1.json +++ b/contracts/schemas/control-plane/trial-cleanup-receipt-v1.json @@ -90,7 +90,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/trial-cleanup-receipt-v1.json", + "$id": "https://raes.dev/schemas/trial-cleanup-receipt-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -205,7 +205,7 @@ ], "title": "TrialCleanupReceiptModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "A cleanup receipt keeps attempt identity distinct, reports cleanup independently from trial outcome, covers every triggered required obligation, and permits clean-state claims only after verified success.", "id": "trial-cleanup-receipt-binds-plan-and-required-outcomes", @@ -223,13 +223,13 @@ "validator": "raes_contracts.contracts.validate_trial_cleanup_receipt" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "trial-cleanup-receipt-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/control-plane/workflow-cancellation-request-v1.json b/contracts/schemas/control-plane/workflow-cancellation-request-v1.json index c397b66bb..fac3f2d34 100644 --- a/contracts/schemas/control-plane/workflow-cancellation-request-v1.json +++ b/contracts/schemas/control-plane/workflow-cancellation-request-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/workflow-cancellation-request-v1.json", + "$id": "https://raes.dev/schemas/workflow-cancellation-request-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/control-plane/workflow-history-event-stream-v1.json b/contracts/schemas/control-plane/workflow-history-event-stream-v1.json index 3678aa2ed..267b00ff2 100644 --- a/contracts/schemas/control-plane/workflow-history-event-stream-v1.json +++ b/contracts/schemas/control-plane/workflow-history-event-stream-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/workflow-history-event-stream-v1.json", + "$id": "https://raes.dev/schemas/workflow-history-event-stream-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "items": { "additionalProperties": false, diff --git a/contracts/schemas/control-plane/workflow-result-envelope-v1.json b/contracts/schemas/control-plane/workflow-result-envelope-v1.json index 20cc67dc0..20060ec53 100644 --- a/contracts/schemas/control-plane/workflow-result-envelope-v1.json +++ b/contracts/schemas/control-plane/workflow-result-envelope-v1.json @@ -151,7 +151,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/workflow-result-envelope-v1.json", + "$id": "https://raes.dev/schemas/workflow-result-envelope-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json b/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json index bc86944b1..ae3191123 100644 --- a/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json +++ b/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json @@ -623,7 +623,7 @@ ], "title": "ExperimentManifestReferenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Manifest digest qualifiers are limited to processor/backend manifest refs that can be checked against concrete manifest payload digests; manifest path qualifiers are not accepted in v1.", "id": "manifest-reference-digest-scope-valid", @@ -1127,7 +1127,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-apparatus-context-v1.json", + "$id": "https://raes.dev/schemas/experiment-apparatus-context-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Run-scoped apparatus context for interpreting experiment evidence.", @@ -1291,7 +1291,7 @@ ], "title": "ExperimentApparatusContextModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "The canonical processor and backend component manifest_ref values must be present in selected_manifests; digest-qualified selected manifests must be canonical component manifests.", "id": "canonical-apparatus-manifest-selected", @@ -1341,13 +1341,13 @@ "validator": "raes_contracts.contracts.validate_experiment_apparatus_context_archival_datetimes" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "experiment-apparatus-context-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-authoring-input-v1.json b/contracts/schemas/experiment-core/experiment-authoring-input-v1.json index 5fb4235e8..983178618 100644 --- a/contracts/schemas/experiment-core/experiment-authoring-input-v1.json +++ b/contracts/schemas/experiment-core/experiment-authoring-input-v1.json @@ -193,7 +193,7 @@ }, "title": "ExperimentApparatusConstraintModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every allowed processor/backend identity reference must have a matching required manifest ref_id with matching manifest id, subject identity, and manifest schema version.", "id": "apparatus-constraint-identity-manifest-resolves", @@ -466,7 +466,7 @@ ], "title": "ExperimentBindingDescriptorSetModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Binding ids must be unique and target resolution must be injective within each source condition.", "id": "binding-descriptors-canonical-targets-injective", @@ -1207,7 +1207,7 @@ ], "title": "ExperimentManifestReferenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Manifest digest qualifiers are limited to processor/backend manifest refs that can be checked against concrete manifest payload digests; manifest path qualifiers are not accepted in v1.", "id": "manifest-reference-digest-scope-valid", @@ -1567,7 +1567,7 @@ ], "title": "ExperimentRunAllocationPlanModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Run-allocation compared_conditions, condition_assignments keys, embedded condition ids, blocking factor ids, factor-level combinations, and run-level criteria signatures must be internally coherent.", "id": "run-allocation-condition-assignments-valid", @@ -1681,7 +1681,7 @@ ], "title": "ExperimentRunPlanModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "A run plan must declare exactly one of allocation or target_run_count, and every red-variant selection map key must equal its embedded variant_id.", "id": "run-plan-exactly-one-run-count-source", @@ -2278,7 +2278,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-authoring-input-v1.json", + "$id": "https://raes.dev/schemas/experiment-authoring-input-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -2446,7 +2446,7 @@ ], "title": "ExperimentSpecModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "When a run plan declares an allocation with blocking factors, every blocking factor must be a declared experiment-spec factor.", "id": "experiment-spec-blocking-factors-declared", @@ -2472,13 +2472,13 @@ "validator": "raes_contracts.contracts.ExperimentSpecModel._validate_binding_descriptors" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "experiment-authoring-input-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json b/contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json index dfa902c43..c44379a4a 100644 --- a/contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json +++ b/contracts/schemas/experiment-core/experiment-binding-descriptors-v1.json @@ -323,7 +323,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-binding-descriptors-v1.json", + "$id": "https://raes.dev/schemas/experiment-binding-descriptors-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Versioned, collision-free set of authoritative experiment bindings.", @@ -348,7 +348,7 @@ ], "title": "ExperimentBindingDescriptorSetModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Binding ids must be unique and target resolution must be injective within each source condition.", "id": "binding-descriptors-canonical-targets-injective", @@ -362,13 +362,13 @@ "validator": "raes_contracts.contracts.ExperimentBindingDescriptorSetModel._validate_unique_bindings_and_targets" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "experiment-binding-descriptors-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-capture-spec-v1.json b/contracts/schemas/experiment-core/experiment-capture-spec-v1.json index 7698cc7f6..7782100a3 100644 --- a/contracts/schemas/experiment-core/experiment-capture-spec-v1.json +++ b/contracts/schemas/experiment-core/experiment-capture-spec-v1.json @@ -381,7 +381,7 @@ ], "title": "ExperimentCaptureWindowModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Capture window ends_at must not precede starts_at when both timestamps are present.", "id": "capture-window-interval-valid", @@ -704,7 +704,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-capture-spec-v1.json", + "$id": "https://raes.dev/schemas/experiment-capture-spec-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Declarative EXP-707 specification of what experiment evidence to capture.", @@ -788,7 +788,7 @@ ], "title": "ExperimentCaptureSpecModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every capture_requirements object key must match the embedded requirement_id value, and window_refs must resolve to declared capture_windows.", "id": "capture-requirement-key-matches-requirement-id", @@ -802,14 +802,14 @@ "validator": "raes_contracts.contracts.ExperimentCaptureSpecModel._validate_capture_spec" } ], - "x-aces-plane": "authored_evidence_requirement", - "x-aces-semantic-profile": { + "x-raes-plane": "authored_evidence_requirement", + "x-raes-semantic-profile": { "contract_id": "experiment-capture-spec-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-derived-measure-v1.json b/contracts/schemas/experiment-core/experiment-derived-measure-v1.json index d1bea016c..d7d40ae63 100644 --- a/contracts/schemas/experiment-core/experiment-derived-measure-v1.json +++ b/contracts/schemas/experiment-core/experiment-derived-measure-v1.json @@ -253,7 +253,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-derived-measure-v1.json", + "$id": "https://raes.dev/schemas/experiment-derived-measure-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -428,7 +428,7 @@ ], "title": "ExperimentDerivedMeasureModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Reported derived measures must include a value; missing/withheld/not-applicable measures must not.", "id": "derived-measure-reported-value-present", @@ -454,14 +454,14 @@ "validator": "raes_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure" } ], - "x-aces-plane": "derived_analysis", - "x-aces-semantic-profile": { + "x-raes-plane": "derived_analysis", + "x-raes-semantic-profile": { "contract_id": "experiment-derived-measure-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-evidence-record-v1.json b/contracts/schemas/experiment-core/experiment-evidence-record-v1.json index 5dfd4a6b9..96a5aa434 100644 --- a/contracts/schemas/experiment-core/experiment-evidence-record-v1.json +++ b/contracts/schemas/experiment-core/experiment-evidence-record-v1.json @@ -548,7 +548,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-evidence-record-v1.json", + "$id": "https://raes.dev/schemas/experiment-evidence-record-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -716,7 +716,7 @@ ], "title": "ExperimentEvidenceRecordModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Evidence records must carry raw content as an artifact reference, content URI with checksum, or bounded payload summary; redacted/withheld records must disclose loss.", "id": "evidence-record-raw-content-present", @@ -742,14 +742,14 @@ "validator": "raes_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record" } ], - "x-aces-plane": "captured_evidence", - "x-aces-semantic-profile": { + "x-raes-plane": "captured_evidence", + "x-raes-semantic-profile": { "contract_id": "experiment-evidence-record-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-run-v1.json b/contracts/schemas/experiment-core/experiment-run-v1.json index d9f0a5889..35f85621b 100644 --- a/contracts/schemas/experiment-core/experiment-run-v1.json +++ b/contracts/schemas/experiment-core/experiment-run-v1.json @@ -482,7 +482,7 @@ ], "title": "ExperimentApparatusContextModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "The canonical processor and backend component manifest_ref values must be present in selected_manifests; digest-qualified selected manifests must be canonical component manifests.", "id": "canonical-apparatus-manifest-selected", @@ -930,7 +930,7 @@ ], "title": "ExperimentAugmentationDisclosureModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Augmentation disclosures must keep environment-visible, participant-visible, and comparability-relevant semantics explicit and must use processor/backend authority.", "id": "augmentation-disclosure-semantics-valid", @@ -1556,7 +1556,7 @@ ], "title": "ExperimentManifestReferenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Manifest digest qualifiers are limited to processor/backend manifest refs that can be checked against concrete manifest payload digests; manifest path qualifiers are not accepted in v1.", "id": "manifest-reference-digest-scope-valid", @@ -1873,7 +1873,7 @@ ], "title": "ExperimentRealizedFormDisclosureModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every realized-form disclosure must name a realized reference or value summary and use the right processor/backend realization authority for processor-realized and backend-realized concerns.", "id": "realized-form-disclosure-substantive", @@ -2177,7 +2177,7 @@ ], "title": "ExperimentRunTraceabilityModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Run provenance traceability references must be duplicate-free, and claim refs must be grounded by at least one derived measure ref.", "id": "run-traceability-refs-unique", @@ -2970,7 +2970,7 @@ ], "title": "RandomStreamDrawRecordModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "local_coordinate must match address.local_coordinate.", "id": "random-stream-draw-record-local-coordinate-matches-address", @@ -3307,7 +3307,7 @@ ], "title": "StreamAddressModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "draw_purpose must be a term from the random_streams.draw_purpose controlled vocabulary.", "id": "random-stream-address-draw-purpose-governed", @@ -4091,7 +4091,7 @@ ], "title": "ValidationBasisDisclosureModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "profile_id/profile_version/subject_kind must resolve one validation profile that declares the subject kind, and subject_ref.ref_kind must match the profile's ONE subject_kind mapping.", "id": "validation-basis-profile-join-resolves", @@ -4492,7 +4492,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-run-v1.json", + "$id": "https://raes.dev/schemas/experiment-run-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -4741,7 +4741,7 @@ ], "title": "ExperimentRunModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "ended_at must be greater than or equal to started_at.", "id": "ended-at-not-before-started-at", @@ -4879,13 +4879,13 @@ "validator": "raes_contracts.contracts.validate_experiment_run_archival_datetimes" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "experiment-run-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-study-v1.json b/contracts/schemas/experiment-core/experiment-study-v1.json index 88161e579..36aa869c5 100644 --- a/contracts/schemas/experiment-core/experiment-study-v1.json +++ b/contracts/schemas/experiment-core/experiment-study-v1.json @@ -212,7 +212,7 @@ ], "title": "ExperimentAnalysisPlanModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Analysis plans must name metrics plus structured statistical, uncertainty, multiplicity, and missing-data policies.", "id": "analysis-plan-substantive-methods-required", @@ -999,7 +999,7 @@ ], "title": "ExperimentRunAllocationPlanModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Run-allocation compared_conditions, condition_assignments keys, embedded condition ids, blocking factor ids, factor-level combinations, and run-level criteria signatures must be internally coherent.", "id": "run-allocation-condition-assignments-valid", @@ -1534,7 +1534,7 @@ ], "title": "ValidationBasisDisclosureModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "profile_id/profile_version/subject_kind must resolve one validation profile that declares the subject kind, and subject_ref.ref_kind must match the profile's ONE subject_kind mapping.", "id": "validation-basis-profile-join-resolves", @@ -1935,7 +1935,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-study-v1.json", + "$id": "https://raes.dev/schemas/experiment-study-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "allOf": [ @@ -2138,7 +2138,7 @@ ], "title": "ExperimentStudyModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Study and benchmark records must include research questions, revisioned behavioral claim bindings, run allocation, a substantive analysis plan, and validity notes.", "id": "claim-bearing-study-analysis-plan-required", @@ -2288,13 +2288,13 @@ "validator": "raes_contracts.contracts.validate_experiment_study_archival_datetimes" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "experiment-study-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/experiment-core/experiment-task-v1.json b/contracts/schemas/experiment-core/experiment-task-v1.json index 90b79af10..cf86adeaa 100644 --- a/contracts/schemas/experiment-core/experiment-task-v1.json +++ b/contracts/schemas/experiment-core/experiment-task-v1.json @@ -96,7 +96,7 @@ }, "title": "ExperimentApparatusConstraintModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every allowed processor/backend identity reference must have a matching required manifest ref_id with matching manifest id, subject identity, and manifest schema version.", "id": "apparatus-constraint-identity-manifest-resolves", @@ -447,7 +447,7 @@ ], "title": "ExperimentEvaluationProtocolModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every metric_definitions object key must match the embedded metric_id value.", "id": "metric-definition-key-matches-metric-id", @@ -793,7 +793,7 @@ ], "title": "ExperimentManifestReferenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Manifest digest qualifiers are limited to processor/backend manifest refs that can be checked against concrete manifest payload digests; manifest path qualifiers are not accepted in v1.", "id": "manifest-reference-digest-scope-valid", @@ -1442,7 +1442,7 @@ ], "title": "ValidationBasisDisclosureModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "profile_id/profile_version/subject_kind must resolve one validation profile that declares the subject kind, and subject_ref.ref_kind must match the profile's ONE subject_kind mapping.", "id": "validation-basis-profile-join-resolves", @@ -1843,7 +1843,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/experiment-task-v1.json", + "$id": "https://raes.dev/schemas/experiment-task-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Experiment task contract that separates scenario material from protocol intent.", @@ -1944,7 +1944,7 @@ ], "title": "ExperimentTaskModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every validation_basis_disclosures entry must declare subject_kind='experiment_task' and a subject_ref matching this task's task_id/task_version.", "id": "task-validation-basis-disclosure-identity-matches", @@ -1970,13 +1970,13 @@ "validator": "raes_contracts.contracts.validate_experiment_task_archival_datetimes" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "experiment-task-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/exploit-path-analysis/exploit-path-analysis-evidence-v1.json b/contracts/schemas/exploit-path-analysis/exploit-path-analysis-evidence-v1.json index 6453e3446..29c973c38 100644 --- a/contracts/schemas/exploit-path-analysis/exploit-path-analysis-evidence-v1.json +++ b/contracts/schemas/exploit-path-analysis/exploit-path-analysis-evidence-v1.json @@ -4,12 +4,6 @@ "additionalProperties": false, "description": "Governed binding from an admitted RAES snapshot concept to graph content.", "properties": { - "aces_address": { - "maxLength": 4096, - "pattern": "^(?:/(?:[^~/]|~[01])*)*$", - "title": "Aces Address", - "type": "string" - }, "binding_id": { "maxLength": 256, "pattern": "^[a-z][a-z0-9._:-]*$", @@ -23,6 +17,12 @@ "title": "Concept Kind", "type": "string" }, + "raes_address": { + "maxLength": 4096, + "pattern": "^(?:/(?:[^~/]|~[01])*)*$", + "title": "Raes Address", + "type": "string" + }, "target_id": { "maxLength": 256, "pattern": "^[a-z][a-z0-9._:-]*$", @@ -41,7 +41,7 @@ "required": [ "binding_id", "concept_kind", - "aces_address", + "raes_address", "target_kind", "target_id" ], @@ -330,7 +330,7 @@ "type": "string" }, "profile": { - "const": "aces-exploit-path-query/v1", + "const": "raes-exploit-path-query/v1", "title": "Profile", "type": "string" }, @@ -381,7 +381,7 @@ "type": "integer" }, "profile": { - "const": "aces-deterministic-attack-graph-search/v1", + "const": "raes-deterministic-attack-graph-search/v1", "title": "Profile", "type": "string" }, @@ -511,7 +511,7 @@ "type": "array" }, "profile": { - "const": "aces-exploit-path-invalid/v1", + "const": "raes-exploit-path-invalid/v1", "title": "Profile", "type": "string" }, @@ -542,7 +542,7 @@ "description": "Closed normalized attack graph derived from governed RAES bindings.", "properties": { "binding_profile": { - "const": "aces-sdl-snapshot-attack-binding/v1", + "const": "raes-sdl-snapshot-attack-binding/v1", "title": "Binding Profile", "type": "string" }, @@ -555,7 +555,7 @@ "type": "array" }, "profile": { - "const": "aces-attack-graph/v1", + "const": "raes-attack-graph/v1", "title": "Profile", "type": "string" }, @@ -575,7 +575,7 @@ "type": "array" }, "transition_semantics_profile": { - "const": "aces-monotonic-attack-transition/v1", + "const": "raes-monotonic-attack-transition/v1", "title": "Transition Semantics Profile", "type": "string" }, @@ -610,7 +610,7 @@ "type": "string" }, "profile": { - "const": "aces-sdl-semantic/v1", + "const": "raes-sdl-semantic/v1", "title": "Profile", "type": "string" }, @@ -668,7 +668,7 @@ "description": "Fail-closed unsupported reason set.", "properties": { "profile": { - "const": "aces-exploit-path-unsupported/v1", + "const": "raes-exploit-path-unsupported/v1", "title": "Profile", "type": "string" }, @@ -733,7 +733,7 @@ "type": "array" }, "profile": { - "const": "aces-exploit-path-witness/v1", + "const": "raes-exploit-path-witness/v1", "title": "Profile", "type": "string" }, @@ -758,7 +758,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/exploit-path-analysis-evidence-v1.json", + "$id": "https://raes.dev/schemas/exploit-path-analysis-evidence-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Closed evidence envelope binding source, snapshot, graph, query, and result.", @@ -832,7 +832,7 @@ ], "properties": { "analysis_profile": { - "const": "aces-exploit-path-analysis-v1", + "const": "raes-exploit-path-analysis-v1", "title": "Analysis Profile", "type": "string" }, @@ -942,7 +942,7 @@ ], "title": "ExploitPathAnalysisEvidenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "The snapshot, normalized graph, query, search configuration, witness final state, and unsupported reason code joins must validate against their canonical contract digests and diagnostics.", "id": "exploit-path-evidence-digest-joins", @@ -956,13 +956,13 @@ "validator": "raes_contracts.exploit_path.ExploitPathAnalysisEvidenceModel._validate_evidence_joins" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "exploit-path-analysis-evidence-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json b/contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json index bd7c2c2ba..11dbc8ee5 100644 --- a/contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json +++ b/contracts/schemas/participant-implementation-configuration/participant-configuration-result-v1.json @@ -210,7 +210,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-configuration-result-v1.json", + "$id": "https://raes.dev/schemas/participant-configuration-result-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Portable result of one complete, atomic participant configuration validation.", @@ -256,7 +256,7 @@ ], "title": "ParticipantConfigurationResultModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "The configuration digest must be the RFC 8785/JCS digest of the complete normalized configuration.", "id": "participant-configuration-digest-valid", @@ -270,13 +270,13 @@ "validator": "raes_contracts.contracts.ParticipantConfigurationResultModel._validate_configuration_digest" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "participant-configuration-result-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json b/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json index 71adec30d..db252b9a2 100644 --- a/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json +++ b/contracts/schemas/participant-implementation-manifest/participant-implementation-manifest-v1.json @@ -302,7 +302,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-implementation-manifest-v1.json", + "$id": "https://raes.dev/schemas/participant-implementation-manifest-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json b/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json index 84f26db6b..94bd627e8 100644 --- a/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json +++ b/contracts/schemas/participant-implementation-provenance/participant-implementation-provenance-v1.json @@ -221,7 +221,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-implementation-provenance-v1.json", + "$id": "https://raes.dev/schemas/participant-implementation-provenance-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/participant-runtime/participant-control-occurrence-v1.json b/contracts/schemas/participant-runtime/participant-control-occurrence-v1.json index 35a5464fc..dd42d1791 100644 --- a/contracts/schemas/participant-runtime/participant-control-occurrence-v1.json +++ b/contracts/schemas/participant-runtime/participant-control-occurrence-v1.json @@ -1802,7 +1802,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-control-occurrence-v1.json", + "$id": "https://raes.dev/schemas/participant-control-occurrence-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Closed participant-runtime carrier for one API-409 control fact.", @@ -2180,7 +2180,7 @@ ], "title": "ParticipantControlOccurrenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every occurrence must resolve one matching compiled ACT-617 declaration and preserve participant, episode, controller, authority, policy revision, order, proposal, target, and semantic identity joins.", "id": "participant-control-occurrence-context-agreement", @@ -2206,13 +2206,13 @@ "validator": "raes_contracts.contracts.ParticipantControlOccurrenceModel" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "participant-control-occurrence-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json b/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json index 35d6100e0..59fbe30ce 100644 --- a/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json +++ b/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json @@ -1786,7 +1786,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-crossing-occurrence-v1.json", + "$id": "https://raes.dev/schemas/participant-crossing-occurrence-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Closed participant-runtime carrier for one API-423 crossing fact.", @@ -2164,7 +2164,7 @@ ], "title": "ParticipantCrossingOccurrenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every crossing fact must resolve typed subjects, exact policy revisions, predecessor stages, evidence, markings, and order coordinates without retroactive authorization or identity reuse.", "id": "participant-crossing-context-agreement", @@ -2190,13 +2190,13 @@ "validator": "raes_contracts.contracts.ParticipantCrossingOccurrenceModel" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "participant-crossing-occurrence-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/participant-runtime/participant-joint-action-record-v1.json b/contracts/schemas/participant-runtime/participant-joint-action-record-v1.json index 96beae9e2..8d586e4fe 100644 --- a/contracts/schemas/participant-runtime/participant-joint-action-record-v1.json +++ b/contracts/schemas/participant-runtime/participant-joint-action-record-v1.json @@ -407,7 +407,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-joint-action-record-v1.json", + "$id": "https://raes.dev/schemas/participant-joint-action-record-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "RUN-308 joint action / concurrency record over behavior events.", diff --git a/contracts/schemas/participant-runtime/participant-lifecycle-event-v1.json b/contracts/schemas/participant-runtime/participant-lifecycle-event-v1.json index 4d03746ef..b52111554 100644 --- a/contracts/schemas/participant-runtime/participant-lifecycle-event-v1.json +++ b/contracts/schemas/participant-runtime/participant-lifecycle-event-v1.json @@ -389,7 +389,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-lifecycle-event-v1.json", + "$id": "https://raes.dev/schemas/participant-lifecycle-event-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "RUN-306 lifecycle boundary record for one participant action event.", diff --git a/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json b/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json index 29bb1c6e4..6ae742ac6 100644 --- a/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json +++ b/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json @@ -409,7 +409,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-observation-envelope-v1.json", + "$id": "https://raes.dev/schemas/participant-observation-envelope-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "SEM-210 participant-visible observation record with explicit guarantees.", diff --git a/contracts/schemas/participant-runtime/participant-outcome-report-v1.json b/contracts/schemas/participant-runtime/participant-outcome-report-v1.json index 0b9295ebb..42616cb74 100644 --- a/contracts/schemas/participant-runtime/participant-outcome-report-v1.json +++ b/contracts/schemas/participant-runtime/participant-outcome-report-v1.json @@ -413,7 +413,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-outcome-report-v1.json", + "$id": "https://raes.dev/schemas/participant-outcome-report-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "SEM-215 outcome interpretation report.\n\nThe carrier deliberately has no score, reward, or objective-success\nfield: reward and return remain ADR-054 step signals, and objective and\nevaluation results remain their own contract surfaces.", diff --git a/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json b/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json index 0494e8740..cf4e8a7d4 100644 --- a/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json +++ b/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json @@ -551,7 +551,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-shared-state-record-v1.json", + "$id": "https://raes.dev/schemas/participant-shared-state-record-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "anyOf": [ diff --git a/contracts/schemas/participant-runtime/participant-time-management-context-v1.json b/contracts/schemas/participant-runtime/participant-time-management-context-v1.json index dcd8d0bcf..f649a8232 100644 --- a/contracts/schemas/participant-runtime/participant-time-management-context-v1.json +++ b/contracts/schemas/participant-runtime/participant-time-management-context-v1.json @@ -351,7 +351,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/participant-time-management-context-v1.json", + "$id": "https://raes.dev/schemas/participant-time-management-context-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "RUN-308 time-management basis for concurrent or distributed runtime claims.", diff --git a/contracts/schemas/participant-runtime/runtime-fact-binding-plane-v1.json b/contracts/schemas/participant-runtime/runtime-fact-binding-plane-v1.json index 851b0d8c8..4aa007e56 100644 --- a/contracts/schemas/participant-runtime/runtime-fact-binding-plane-v1.json +++ b/contracts/schemas/participant-runtime/runtime-fact-binding-plane-v1.json @@ -685,7 +685,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/runtime-fact-binding-plane-v1.json", + "$id": "https://raes.dev/schemas/runtime-fact-binding-plane-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { @@ -733,7 +733,7 @@ }, "title": "RuntimeFactBindingPlaneModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every fact version resolves to a declaration, every binding event resolves to its compiled sink and optional immutable fact version with matching scope, sensitivity, provenance, redaction, and sink policy, and every projection exactly matches the immutable version it discloses.", "id": "runtime-fact-binding-references-resolve", @@ -747,13 +747,13 @@ "validator": "raes_contracts.contracts.runtime_facts.RuntimeFactBindingPlaneModel._validate_references" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "runtime-fact-binding-plane-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/plans/evaluation-plan-v1.json b/contracts/schemas/plans/evaluation-plan-v1.json index 24fc35040..feeaa8543 100644 --- a/contracts/schemas/plans/evaluation-plan-v1.json +++ b/contracts/schemas/plans/evaluation-plan-v1.json @@ -73,7 +73,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/evaluation-plan-v1.json", + "$id": "https://raes.dev/schemas/evaluation-plan-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/plans/orchestration-plan-v1.json b/contracts/schemas/plans/orchestration-plan-v1.json index 544aad407..3ffbbacc3 100644 --- a/contracts/schemas/plans/orchestration-plan-v1.json +++ b/contracts/schemas/plans/orchestration-plan-v1.json @@ -75,7 +75,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/orchestration-plan-v1.json", + "$id": "https://raes.dev/schemas/orchestration-plan-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/plans/provisioning-plan-v1.json b/contracts/schemas/plans/provisioning-plan-v1.json index e0bc20ab6..476dbfe75 100644 --- a/contracts/schemas/plans/provisioning-plan-v1.json +++ b/contracts/schemas/plans/provisioning-plan-v1.json @@ -117,7 +117,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/provisioning-plan-v1.json", + "$id": "https://raes.dev/schemas/provisioning-plan-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/plans/trial-cleanup-plan-v1.json b/contracts/schemas/plans/trial-cleanup-plan-v1.json index c08e22c24..a22ecba75 100644 --- a/contracts/schemas/plans/trial-cleanup-plan-v1.json +++ b/contracts/schemas/plans/trial-cleanup-plan-v1.json @@ -263,7 +263,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/trial-cleanup-plan-v1.json", + "$id": "https://raes.dev/schemas/trial-cleanup-plan-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Schedule-independent cleanup intent carried by one admitted trial entry.", @@ -329,7 +329,7 @@ ], "title": "TrialCleanupPlanModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Cleanup boundary and dependency references resolve, ordering is acyclic, required cleanup is verifiable, and retries after non-idempotent effects declare required retry-triggered reset obligations covering the affected boundaries or compensation.", "id": "trial-cleanup-plan-references-and-retry-safe", @@ -343,13 +343,13 @@ "validator": "raes_contracts.contracts.TrialCleanupPlanModel._validate_plan" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "trial-cleanup-plan-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/processor-manifest/processor-manifest-v2.json b/contracts/schemas/processor-manifest/processor-manifest-v2.json index 71bb16ee9..025dbf1ef 100644 --- a/contracts/schemas/processor-manifest/processor-manifest-v2.json +++ b/contracts/schemas/processor-manifest/processor-manifest-v2.json @@ -283,7 +283,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/processor-manifest-v2.json", + "$id": "https://raes.dev/schemas/processor-manifest-v2.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index d35cde8a1..16a4909da 100644 --- a/contracts/schemas/profiles/backend-profile-v1.json +++ b/contracts/schemas/profiles/backend-profile-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/backend-profile-v1.json", + "$id": "https://raes.dev/schemas/backend-profile-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Published backend capability profile (closed-world).", diff --git a/contracts/schemas/profiles/aces-semantic-invariants-v1.json b/contracts/schemas/profiles/raes-semantic-invariants-v1.json similarity index 76% rename from contracts/schemas/profiles/aces-semantic-invariants-v1.json rename to contracts/schemas/profiles/raes-semantic-invariants-v1.json index 0ec569e11..e0ef6bc52 100644 --- a/contracts/schemas/profiles/aces-semantic-invariants-v1.json +++ b/contracts/schemas/profiles/raes-semantic-invariants-v1.json @@ -1,6 +1,6 @@ { "$defs": { - "AcesSemanticInvariantEntryModel": { + "RaesSemanticInvariantEntryModel": { "additionalProperties": false, "description": "Machine-readable semantic invariant annotation entry.", "properties": { @@ -16,7 +16,7 @@ }, "inputs": { "items": { - "$ref": "#/$defs/AcesSemanticInvariantInputModel" + "$ref": "#/$defs/RaesSemanticInvariantInputModel" }, "minItems": 1, "title": "Inputs", @@ -40,10 +40,10 @@ "validator", "inputs" ], - "title": "AcesSemanticInvariantEntryModel", + "title": "RaesSemanticInvariantEntryModel", "type": "object" }, - "AcesSemanticInvariantInputModel": { + "RaesSemanticInvariantInputModel": { "additionalProperties": false, "description": "Input contract and instance path required by one RAES semantic invariant.", "properties": { @@ -62,10 +62,10 @@ "contract_id", "instance_path" ], - "title": "AcesSemanticInvariantInputModel", + "title": "RaesSemanticInvariantInputModel", "type": "object" }, - "AcesSemanticInvariantProfileReferenceModel": { + "RaesSemanticInvariantProfileReferenceModel": { "additionalProperties": false, "description": "Host-schema reference to the RAES semantic-invariant profile.", "properties": { @@ -75,22 +75,22 @@ "type": "string" }, "entry_schema_contract_id": { - "const": "aces-semantic-invariants-v1", + "const": "raes-semantic-invariants-v1", "title": "Entry Schema Contract Id", "type": "string" }, "entry_schema_pointer": { - "const": "#/$defs/AcesSemanticInvariantEntryModel", + "const": "#/$defs/RaesSemanticInvariantEntryModel", "title": "Entry Schema Pointer", "type": "string" }, "id": { - "const": "aces-semantic-invariants-v1", + "const": "raes-semantic-invariants-v1", "title": "Id", "type": "string" }, "keyword": { - "const": "x-aces-invariants", + "const": "x-raes-invariants", "title": "Keyword", "type": "string" }, @@ -100,7 +100,7 @@ "type": "boolean" }, "uri": { - "const": "https://aces.dev/schemas/semantic-invariants/v1", + "const": "https://raes.dev/schemas/semantic-invariants/v1", "title": "Uri", "type": "string" } @@ -114,49 +114,49 @@ "entry_schema_contract_id", "entry_schema_pointer" ], - "title": "AcesSemanticInvariantProfileReferenceModel", + "title": "RaesSemanticInvariantProfileReferenceModel", "type": "object" } }, - "$id": "https://aces.dev/schemas/semantic-invariants/v1", + "$id": "https://raes.dev/schemas/semantic-invariants/v1", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Published shape for RAES semantic-invariant annotations.", "properties": { "invariant_entry_schema": { - "const": "#/$defs/AcesSemanticInvariantEntryModel", + "const": "#/$defs/RaesSemanticInvariantEntryModel", "title": "Invariant Entry Schema", "type": "string" }, "invariants": { "items": { - "$ref": "#/$defs/AcesSemanticInvariantEntryModel" + "$ref": "#/$defs/RaesSemanticInvariantEntryModel" }, "title": "Invariants", "type": "array" }, "keyword": { - "const": "x-aces-invariants", + "const": "x-raes-invariants", "title": "Keyword", "type": "string" }, "profile_id": { - "const": "aces-semantic-invariants-v1", + "const": "raes-semantic-invariants-v1", "title": "Profile Id", "type": "string" }, "profile_reference_schema": { - "const": "#/$defs/AcesSemanticInvariantProfileReferenceModel", + "const": "#/$defs/RaesSemanticInvariantProfileReferenceModel", "title": "Profile Reference Schema", "type": "string" }, "schema_version": { - "const": "aces-semantic-invariants/v1", + "const": "raes-semantic-invariants/v1", "title": "Schema Version", "type": "string" }, "uri": { - "const": "https://aces.dev/schemas/semantic-invariants/v1", + "const": "https://raes.dev/schemas/semantic-invariants/v1", "title": "Uri", "type": "string" } @@ -170,6 +170,6 @@ "profile_reference_schema", "invariants" ], - "title": "AcesSemanticInvariantProfileModel", + "title": "RaesSemanticInvariantProfileModel", "type": "object" } diff --git a/contracts/schemas/profiles/random-stream-profile-v1.json b/contracts/schemas/profiles/random-stream-profile-v1.json index 16465c3de..451389d81 100644 --- a/contracts/schemas/profiles/random-stream-profile-v1.json +++ b/contracts/schemas/profiles/random-stream-profile-v1.json @@ -148,7 +148,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/random-stream-profile-v1.json", + "$id": "https://raes.dev/schemas/random-stream-profile-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Published random-stream profile: one closed, immutable compatibility unit.\n\nChanging any field mints a new ``profile_id`` (the EXP-718 preflight's \"One\nProfile And One Stateless API\" section). This model's JSON Schema is\ngenerated via ``schema_bundle()``, never hand-authored.", @@ -214,7 +214,7 @@ ], "title": "RandomStreamProfileModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "transforms dict keys must match each entry's embedded transform_id.", "id": "random-stream-profile-transform-keys-match", @@ -228,13 +228,13 @@ "validator": "raes_contracts.contracts.random_stream.RandomStreamProfileModel._validate_transform_keys" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "random-stream-profile-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/profiles/random-stream-vector-v1.json b/contracts/schemas/profiles/random-stream-vector-v1.json index 818fc4a82..fb27945d4 100644 --- a/contracts/schemas/profiles/random-stream-vector-v1.json +++ b/contracts/schemas/profiles/random-stream-vector-v1.json @@ -217,7 +217,7 @@ ], "title": "StreamAddressModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "draw_purpose must be a term from the random_streams.draw_purpose controlled vocabulary.", "id": "random-stream-address-draw-purpose-governed", @@ -295,7 +295,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/random-stream-vector-v1.json", + "$id": "https://raes.dev/schemas/random-stream-vector-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "One canonical cross-language conformance vector case.\n\nComputed independently of the reference engine (a throwaway script that\ncalls the ``blake3`` library directly), so the vector tests do not just\ntest the engine against itself.", @@ -367,13 +367,13 @@ ], "title": "RandomStreamVectorModel", "type": "object", - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "random-stream-vector-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/profiles/scientific-completeness-assessment-v1.json b/contracts/schemas/profiles/scientific-completeness-assessment-v1.json index f512ebc2b..5848f2176 100644 --- a/contracts/schemas/profiles/scientific-completeness-assessment-v1.json +++ b/contracts/schemas/profiles/scientific-completeness-assessment-v1.json @@ -148,7 +148,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/scientific-completeness-assessment-v1.json", + "$id": "https://raes.dev/schemas/scientific-completeness-assessment-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { @@ -196,7 +196,7 @@ ], "title": "ScientificCompletenessAssessmentModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Concern ids must be unique and each delivery status must carry its required executable evidence, external binding, issue refs, or exclusion rationale.", "id": "scientific-completeness-assessment-status-evidence", @@ -226,13 +226,13 @@ "validator": "raes_contracts.scientific_completeness.evaluate_profile_completeness" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "scientific-completeness-assessment-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json b/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json index 6293d4f86..50ea27350 100644 --- a/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json +++ b/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json @@ -276,7 +276,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/scientific-completeness-taxonomy-v1.json", + "$id": "https://raes.dev/schemas/scientific-completeness-taxonomy-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { @@ -321,7 +321,7 @@ ], "title": "ScientificCompletenessTaxonomyModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Concern and profile ids must be unique, and every profile disposition map must exactly cover the taxonomy concern set.", "id": "scientific-completeness-taxonomy-rectangular", @@ -351,13 +351,13 @@ "validator": "raes_contracts.scientific_completeness.CompletenessProfileModel.validate_behavioral_claims" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "scientific-completeness-taxonomy-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/profiles/semantic-profile-v1.json b/contracts/schemas/profiles/semantic-profile-v1.json index ddbfd60df..e75ab9e8d 100644 --- a/contracts/schemas/profiles/semantic-profile-v1.json +++ b/contracts/schemas/profiles/semantic-profile-v1.json @@ -92,7 +92,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/semantic-profile-v1.json", + "$id": "https://raes.dev/schemas/semantic-profile-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/profiles/validation-basis-disclosure-v1.json b/contracts/schemas/profiles/validation-basis-disclosure-v1.json index 355f2750a..e097c9507 100644 --- a/contracts/schemas/profiles/validation-basis-disclosure-v1.json +++ b/contracts/schemas/profiles/validation-basis-disclosure-v1.json @@ -151,7 +151,7 @@ ], "title": "ValidationBasisDisclosureModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "profile_id/profile_version/subject_kind must resolve one validation profile that declares the subject kind, and subject_ref.ref_kind must match the profile's ONE subject_kind mapping.", "id": "validation-basis-profile-join-resolves", @@ -552,7 +552,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/validation-basis-disclosure-v1.json", + "$id": "https://raes.dev/schemas/validation-basis-disclosure-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Published wrapper adding ``schema_version`` around the embeddable disclosure core.", @@ -572,13 +572,13 @@ ], "title": "ValidationBasisDisclosureDocumentModel", "type": "object", - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "validation-basis-disclosure-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/profiles/validation-profile-catalog-v1.json b/contracts/schemas/profiles/validation-profile-catalog-v1.json index 74c7dc774..5483184c9 100644 --- a/contracts/schemas/profiles/validation-profile-catalog-v1.json +++ b/contracts/schemas/profiles/validation-profile-catalog-v1.json @@ -187,7 +187,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/validation-profile-catalog-v1.json", + "$id": "https://raes.dev/schemas/validation-profile-catalog-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { @@ -213,8 +213,8 @@ "type": "array" }, "profile_family": { - "const": "aces-validation", - "default": "aces-validation", + "const": "raes-validation", + "default": "raes-validation", "title": "Profile Family", "type": "string" }, @@ -259,7 +259,7 @@ ], "title": "ValidationProfileCatalogModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Strength ranks and term ids must be unique, profile identities must be unique, required and optional gates must be disjoint, and every profile reference must resolve within the catalog.", "id": "validation-profile-catalog-reference-integrity", @@ -273,13 +273,13 @@ "validator": "raes_contracts.validation_profiles.ValidationProfileCatalogModel" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "validation-profile-catalog-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/provenance/sdl-lineage-ledger-v1.json b/contracts/schemas/provenance/sdl-lineage-ledger-v1.json index 3059de23c..ca1f1b73a 100644 --- a/contracts/schemas/provenance/sdl-lineage-ledger-v1.json +++ b/contracts/schemas/provenance/sdl-lineage-ledger-v1.json @@ -139,7 +139,7 @@ }, "CompatibilityDirection": { "enum": [ - "aces_relative_to_source", + "raes_relative_to_source", "not_applicable" ], "title": "CompatibilityDirection", @@ -254,14 +254,6 @@ "LineageClaimModel": { "additionalProperties": false, "properties": { - "aces_boundaries": { - "items": { - "$ref": "#/$defs/ArtifactBoundaryModel" - }, - "minItems": 1, - "title": "Aces Boundaries", - "type": "array" - }, "citation_refs": { "items": { "minLength": 1, @@ -295,6 +287,14 @@ "plane": { "$ref": "#/$defs/LineagePlane" }, + "raes_boundaries": { + "items": { + "$ref": "#/$defs/ArtifactBoundaryModel" + }, + "minItems": 1, + "title": "Raes Boundaries", + "type": "array" + }, "source_boundaries": { "items": { "$ref": "#/$defs/ArtifactBoundaryModel" @@ -314,7 +314,7 @@ "required": [ "plane", "classification", - "aces_boundaries", + "raes_boundaries", "divergence", "compatibility", "compatibility_direction" @@ -327,7 +327,7 @@ "adopted_syntax", "adopted_semantics", "adapted", - "aces_native" + "raes_native" ], "title": "LineageClassification", "type": "string" @@ -575,7 +575,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/sdl-lineage-ledger-v1.json", + "$id": "https://raes.dev/schemas/sdl-lineage-ledger-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/realization-envelope/realization-envelope-v1.json b/contracts/schemas/realization-envelope/realization-envelope-v1.json index aaa9467f6..bb9e27d2e 100644 --- a/contracts/schemas/realization-envelope/realization-envelope-v1.json +++ b/contracts/schemas/realization-envelope/realization-envelope-v1.json @@ -761,7 +761,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/realization-envelope-v1.json", + "$id": "https://raes.dev/schemas/realization-envelope-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Published backend carrier: shared set expression plus truthful realization claims.", @@ -965,7 +965,7 @@ ], "title": "BackendRealizationEnvelopeModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Configuration bounds, expression references, canonical configuration and envelope digests, and all cross-field realization disclosure semantics must validate together.", "id": "realization-envelope-canonical-semantics-valid", @@ -979,13 +979,13 @@ "validator": "raes_contracts.realization_envelope.validate_backend_realization_envelope" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "realization-envelope-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json index fe0723eae..85b6f8da8 100644 --- a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json +++ b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json @@ -1512,7 +1512,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Cross Tenant Isolation" @@ -3334,7 +3334,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Profile" @@ -3372,7 +3372,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Protocol" @@ -4076,7 +4076,7 @@ }, "instantiation_provenance": { "$ref": "#/$defs/InstantiationProvenance", - "x-aces-realization-dimension": false + "x-raes-realization-dimension": false }, "name": { "maxLength": 64, @@ -4205,15 +4205,15 @@ ], "title": "SDL Instantiated Scenario v1", "type": "object", - "x-aces-authored-identity-profile": "aces-sdl-semantic/v1", - "x-aces-document-phase": "instantiated-scenario" + "x-raes-authored-identity-profile": "raes-sdl-semantic/v1", + "x-raes-document-phase": "instantiated-scenario" }, "InstantiatedScenarioSnapshot": { "additionalProperties": false, "description": "Sealed canonical envelope for one portable instantiated artifact.", "properties": { "profile": { - "const": "aces-sdl-instantiated-snapshot/v1", + "const": "raes-sdl-instantiated-snapshot/v1", "title": "Profile", "type": "string" }, @@ -4227,7 +4227,7 @@ ], "title": "SDL Instantiated Scenario Snapshot v1", "type": "object", - "x-aces-document-phase": "canonical-instantiated-snapshot" + "x-raes-document-phase": "canonical-instantiated-snapshot" }, "InstantiationProvenance": { "additionalProperties": false, @@ -4819,7 +4819,7 @@ "type": "array" }, "profile": { - "const": "aces-finite-domain-constraints/v1", + "const": "raes-finite-domain-constraints/v1", "title": "Profile", "type": "string" }, @@ -4838,12 +4838,12 @@ "type": "array" }, "theory_profile": { - "const": "aces-finite-domain-theory/v1", + "const": "raes-finite-domain-theory/v1", "title": "Theory Profile", "type": "string" }, "translation_profile": { - "const": "aces-sdl-authoring-translation/v1", + "const": "raes-sdl-authoring-translation/v1", "title": "Translation Profile", "type": "string" } @@ -6659,7 +6659,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Channel" @@ -7624,7 +7624,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Kernel Boundary" @@ -7707,7 +7707,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Direction" @@ -7723,7 +7723,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Trust Type" @@ -7822,7 +7822,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Direction" @@ -7838,7 +7838,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Mapping Intent" @@ -7854,7 +7854,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Protocol" @@ -7875,7 +7875,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Tenant Claim Owner" @@ -8126,7 +8126,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Mutable State Owner" @@ -8149,7 +8149,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Reset Generation Owner" @@ -8165,7 +8165,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Tenant Isolation" @@ -8181,7 +8181,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Workload Authentication" @@ -18930,7 +18930,7 @@ "description": "Independently admitted canonical instantiation witness.", "properties": { "profile": { - "const": "aces-satisfiability-witness/v1", + "const": "raes-satisfiability-witness/v1", "title": "Profile", "type": "string" }, @@ -19034,7 +19034,7 @@ "type": "string" }, "profile": { - "const": "aces-sdl-semantic/v1", + "const": "raes-sdl-semantic/v1", "title": "Profile", "type": "string" }, @@ -19593,7 +19593,7 @@ "type": "string" }, "profile": { - "const": "aces-z3-finite-domain/v1", + "const": "raes-z3-finite-domain/v1", "title": "Profile", "type": "string" }, @@ -20452,7 +20452,7 @@ "type": "string" }, "profile": { - "const": "aces-unsatisfiable-core/v1", + "const": "raes-unsatisfiable-core/v1", "title": "Profile", "type": "string" } @@ -20470,7 +20470,7 @@ "description": "Stable fail-closed reason set for an unsupported analysis.", "properties": { "profile": { - "const": "aces-satisfiability-unsupported/v1", + "const": "raes-satisfiability-unsupported/v1", "title": "Profile", "type": "string" }, @@ -20980,13 +20980,13 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/scenario-satisfiability-evidence-v1.json", + "$id": "https://raes.dev/schemas/scenario-satisfiability-evidence-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Closed evidence envelope binding source, model, solver, and result.", "properties": { "analysis_profile": { - "const": "aces-finite-domain-satisfiability-v1", + "const": "raes-finite-domain-satisfiability-v1", "title": "Analysis Profile", "type": "string" }, @@ -21086,7 +21086,7 @@ ], "title": "ScenarioSatisfiabilityEvidenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Service-target content must resolve to one named service, retain exact tenant/reset ownership and content ordering, and bind observed-state postconditions to participant observation boundaries.", "id": "initial-service-state-semantics", @@ -21100,13 +21100,13 @@ "validator": "raes.validator.SemanticValidator._verify_service_materialization" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "scenario-satisfiability-evidence-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json index 02f26ecc7..0379461b1 100644 --- a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -4951,7 +4951,7 @@ }, "instantiation_provenance": { "$ref": "#/$defs/InstantiationProvenance", - "x-aces-realization-dimension": false + "x-raes-realization-dimension": false }, "name": { "allOf": [ @@ -5225,8 +5225,8 @@ ], "title": "SDL Instantiated Scenario v1", "type": "object", - "x-aces-authored-identity-profile": "aces-sdl-semantic/v1", - "x-aces-document-phase": "instantiated-scenario" + "x-raes-authored-identity-profile": "raes-sdl-semantic/v1", + "x-raes-document-phase": "instantiated-scenario" }, "InstantiationProvenance": { "additionalProperties": false, @@ -23788,7 +23788,7 @@ "type": "string" }, "profile": { - "const": "aces-sdl-semantic/v1", + "const": "raes-sdl-semantic/v1", "title": "Profile", "type": "string" }, @@ -25879,13 +25879,13 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/instantiated-scenario-snapshot-v1.json", + "$id": "https://raes.dev/schemas/instantiated-scenario-snapshot-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Sealed canonical envelope for one portable instantiated artifact.", "properties": { "profile": { - "const": "aces-sdl-instantiated-snapshot/v1", + "const": "raes-sdl-instantiated-snapshot/v1", "title": "Profile", "type": "string" }, @@ -25899,8 +25899,8 @@ ], "title": "SDL Instantiated Scenario Snapshot v1", "type": "object", - "x-aces-document-phase": "canonical-instantiated-snapshot", - "x-aces-invariants": [ + "x-raes-document-phase": "canonical-instantiated-snapshot", + "x-raes-invariants": [ { "description": "Generated artifact output names and paths, consumers, and dependency entries must be unique, and generated artifact consumers must be read-only.", "id": "stateful-generated-artifact-semantics", @@ -25950,13 +25950,13 @@ "validator": "raes.validator.SemanticValidator._verify_service_materialization" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "instantiated-scenario-snapshot-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index 2de7fb686..8e7878439 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -23145,7 +23145,7 @@ "type": "string" }, "profile": { - "const": "aces-sdl-semantic/v1", + "const": "raes-sdl-semantic/v1", "title": "Profile", "type": "string" }, @@ -25236,7 +25236,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/instantiated-scenario-v1.json", + "$id": "https://raes.dev/schemas/instantiated-scenario-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Scenario with all ``${var}`` references resolved to concrete values.\n\nUnlike the authoring-input contract, an instantiated scenario MUST NOT\ncontain any unresolved ``${name}`` substitution token in any string value,\nwhether a whole-string placeholder (``\"${os}\"``) or embedded\n(``\"host-${index}\"``). The invariant is enforced both by the model\nvalidator below and by the published ``instantiated-scenario-v1`` JSON\nSchema, which forbids the token in every string field. The schema is\nIf a resolved variable value itself introduces a literal ``${name}``\nsequence, the single-pass substitution step does not interpret it as a\nsecond substitution request; final model admission still treats the result\nas non-concrete and rejects the public instantiation.", @@ -25603,7 +25603,7 @@ }, "instantiation_provenance": { "$ref": "#/$defs/InstantiationProvenance", - "x-aces-realization-dimension": false + "x-raes-realization-dimension": false }, "name": { "allOf": [ @@ -25877,9 +25877,9 @@ ], "title": "SDL Instantiated Scenario v1", "type": "object", - "x-aces-authored-identity-profile": "aces-sdl-semantic/v1", - "x-aces-document-phase": "instantiated-scenario", - "x-aces-invariants": [ + "x-raes-authored-identity-profile": "raes-sdl-semantic/v1", + "x-raes-document-phase": "instantiated-scenario", + "x-raes-invariants": [ { "description": "Generated artifact output names and paths, consumers, and dependency entries must be unique, and generated artifact consumers must be read-only.", "id": "stateful-generated-artifact-semantics", @@ -25929,13 +25929,13 @@ "validator": "raes.validator.SemanticValidator._verify_service_materialization" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "instantiated-scenario-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/sdl/scenario-instantiation-request-v1.json b/contracts/schemas/sdl/scenario-instantiation-request-v1.json index 89e7bd017..8d0ac84ed 100644 --- a/contracts/schemas/sdl/scenario-instantiation-request-v1.json +++ b/contracts/schemas/sdl/scenario-instantiation-request-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/scenario-instantiation-request-v1.json", + "$id": "https://raes.dev/schemas/scenario-instantiation-request-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index d6a3b6b24..0b3949df2 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -1438,7 +1438,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Cross Tenant Isolation" @@ -3292,7 +3292,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Profile" @@ -3330,7 +3330,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Protocol" @@ -6515,7 +6515,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Channel" @@ -7541,7 +7541,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Kernel Boundary" @@ -7624,7 +7624,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Direction" @@ -7640,7 +7640,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Trust Type" @@ -7739,7 +7739,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Direction" @@ -7755,7 +7755,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Mapping Intent" @@ -7771,7 +7771,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Protocol" @@ -7792,7 +7792,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Tenant Claim Owner" @@ -8043,7 +8043,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Mutable State Owner" @@ -8066,7 +8066,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Reset Generation Owner" @@ -8082,7 +8082,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Tenant Isolation" @@ -8098,7 +8098,7 @@ }, "pattern": "^\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}$", "type": "string", - "x-aces-variable-reference": true + "x-raes-variable-reference": true } ], "title": "Workload Authentication" @@ -21157,7 +21157,7 @@ "type": "string" } }, - "$id": "https://aces.dev/schemas/sdl-authoring-input-v1.json", + "$id": "https://raes.dev/schemas/sdl-authoring-input-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Normalized SDL authoring object.\n\nThis model applies after ``sdl-yaml/v1`` source-profile checks, structural\nkey canonicalization, shorthand expansion, enum normalization, and typed\nconstruction, but before module expansion and instantiation. Its JSON\nSchema does not validate YAML presentation details.", @@ -21646,7 +21646,7 @@ } ], "default": null, - "x-aces-realization-dimension": false + "x-raes-realization-dimension": false }, "relationships": { "additionalProperties": { @@ -21868,8 +21868,8 @@ ], "title": "SDL Normalized Authoring Object v1", "type": "object", - "x-aces-document-phase": "normalized-authoring-object", - "x-aces-invariants": [ + "x-raes-document-phase": "normalized-authoring-object", + "x-raes-invariants": [ { "description": "Generated artifact output names and paths, consumers, and dependency entries must be unique, and generated artifact consumers must be read-only.", "id": "stateful-generated-artifact-semantics", @@ -21919,15 +21919,15 @@ "validator": "raes.validator.SemanticValidator._verify_service_materialization" } ], - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "sdl-authoring-input-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" }, - "x-aces-source-profile": "sdl-yaml/v1", - "x-aces-validates-raw-source": false + "x-raes-source-profile": "sdl-yaml/v1", + "x-raes-validates-raw-source": false } diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index d6cd31459..207f59acf 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -1964,7 +1964,7 @@ ], "title": "ParticipantControlOccurrenceModel", "type": "object", - "x-aces-invariants": [ + "x-raes-invariants": [ { "description": "Every occurrence must resolve one matching compiled ACT-617 declaration and preserve participant, episode, controller, authority, policy revision, order, proposal, target, and semantic identity joins.", "id": "participant-control-occurrence-context-agreement", @@ -6708,7 +6708,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/runtime-snapshot-v1.json", + "$id": "https://raes.dev/schemas/runtime-snapshot-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Published envelope for a live runtime snapshot.\n\nParticipant episode surfaces (``participant_episode_results`` and\n``participant_episode_history``) are both keyed by the stable\n``participant_address`` of the participant the state/history belongs\nto. SEM-208 participant behavior history is keyed the same way and\nrecords action, observation, and state-transition events with compiled\nbehavior-contract addresses. The episode results map carries the\ncurrently-live episode state per participant; prior episodes survive only\nthrough append-only history streams and the ``previous_episode_id`` chain\non each state.", @@ -6891,13 +6891,13 @@ }, "title": "RuntimeSnapshotEnvelopeModel", "type": "object", - "x-aces-semantic-profile": { + "x-raes-semantic-profile": { "contract_id": "runtime-snapshot-v1", - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", - "id": "aces-semantic-invariants-v1", - "keyword": "x-aces-invariants", + "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://aces.dev/schemas/semantic-invariants/v1" + "uri": "https://raes.dev/schemas/semantic-invariants/v1" } } diff --git a/contracts/schemas/time/realized-time-model-v1.json b/contracts/schemas/time/realized-time-model-v1.json index aebb69ea3..3bb95ec5d 100644 --- a/contracts/schemas/time/realized-time-model-v1.json +++ b/contracts/schemas/time/realized-time-model-v1.json @@ -693,7 +693,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/realized-time-model-v1.json", + "$id": "https://raes.dev/schemas/realized-time-model-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Run-scoped declaration/realization comparison and apparatus evidence.", diff --git a/contracts/schemas/time/time-model-v1.json b/contracts/schemas/time/time-model-v1.json index 139cde75d..efe2990c9 100644 --- a/contracts/schemas/time/time-model-v1.json +++ b/contracts/schemas/time/time-model-v1.json @@ -479,7 +479,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/time-model-v1.json", + "$id": "https://raes.dev/schemas/time-model-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Canonical backend-neutral declaration compiled from authored SDL.", diff --git a/contracts/schemas/time/time-runtime-state-v1.json b/contracts/schemas/time/time-runtime-state-v1.json index 496194654..4f27ba57b 100644 --- a/contracts/schemas/time/time-runtime-state-v1.json +++ b/contracts/schemas/time/time-runtime-state-v1.json @@ -175,7 +175,7 @@ "type": "object" } }, - "$id": "https://aces.dev/schemas/time-runtime-state-v1.json", + "$id": "https://raes.dev/schemas/time-runtime-state-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Typed observable state for all clocks governed by one declaration.", diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index e4b740373..336b780d6 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -1,6 +1,6 @@ # Architecture Decision Records -This directory contains Architecture Decision Records (ADRs) for the ACES SDL +This directory contains Architecture Decision Records (ADRs) for the RAES SDL ecosystem. ADRs capture significant architectural decisions along with their context, rationale, and consequences. @@ -138,6 +138,7 @@ adr-091-portable-time-capability-control-and-provenance-contracts adr-092-autonomous-benign-participants-under-shared-time adr-093-raes-rename-and-compatibility-boundaries adr-094-authoritative-cross-plane-experiment-bindings +adr-095-identity-cutover-and-historical-record-boundary ``` | ADR | Title | Status | Date | @@ -234,5 +235,6 @@ adr-094-authoritative-cross-plane-experiment-bindings | [090](adr-090-shared-time-domain-clock-and-progression-authority.md) | Shared Time-Domain, Clock, And Progression Authority | accepted | 2026-07-24 | | [091](adr-091-portable-time-capability-control-and-provenance-contracts.md) | Portable Time Capability, Control, And Provenance Contracts | accepted | 2026-07-24 | | [092](adr-092-autonomous-benign-participants-under-shared-time.md) | Autonomous Benign Participants Under Shared Time | accepted | 2026-07-24 | -| [093](adr-093-raes-rename-and-compatibility-boundaries.md) | RAES Rename and Compatibility Boundaries | accepted | 2026-07-23 | +| [093](adr-093-raes-rename-and-compatibility-boundaries.md) | RAES Rename and Compatibility Boundaries | superseded by ADR-095 | 2026-07-23 | | [094](adr-094-authoritative-cross-plane-experiment-bindings.md) | Authoritative Cross-Plane Experiment Bindings | accepted | 2026-07-26 | +| [095](adr-095-identity-cutover-and-historical-record-boundary.md) | Identity Cutover and Historical-Record Boundary | accepted | 2026-07-26 | diff --git a/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md b/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md index d542121ce..a292417b6 100644 --- a/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md +++ b/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md @@ -155,7 +155,7 @@ Positive: Negative: -- The contract-model surface in `aces_contracts` grows substantially, and the +- The contract-model surface in `raes_contracts` grows substantially, and the base envelope is modeled ahead of the `RUN-30x` carriers that will also embed it; that model must be reused, not duplicated, when those land. - Schemas exist before any runtime emits them. Consumers must read manifest diff --git a/docs/decisions/adrs/adr-067-participant-behavior-model.md b/docs/decisions/adrs/adr-067-participant-behavior-model.md index 9b3d44847..212a6e684 100644 --- a/docs/decisions/adrs/adr-067-participant-behavior-model.md +++ b/docs/decisions/adrs/adr-067-participant-behavior-model.md @@ -91,12 +91,12 @@ interaction is defined over: Action names, tool names, ATT&CK/CVE labels, backend commands, reward values, timestamps, scheduler order, and raw logs are not portable interaction -semantics unless they are bound through the governed ACES contracts above. +semantics unless they are bound through the governed RAES contracts above. -### 3. ACT-602 executable model means machine-checkable ACES contracts +### 3. ACT-602 executable model means machine-checkable RAES contracts The executable participant behavior model is executable because processors, -backends, conformance tools, and validators can check it, not because ACES +backends, conformance tools, and validators can check it, not because RAES standardizes one participant runtime loop or one external agent API. Executable behavior must flow through existing gates: @@ -113,7 +113,7 @@ Executable behavior must flow through existing gates: Backends may realize behavior with humans, scripts, policies, LLM agents, RL policies, emulators, simulators, services, or mixed controllers. The portable -claim is the ACES contract and evidence record, not the backend's private +claim is the RAES contract and evidence record, not the backend's private implementation. ### 4. ACT-606 behavior specifications are first-class aggregates @@ -228,8 +228,8 @@ meaning across two SDL surfaces. ### Treat backend or agent-framework APIs as the executable model Rejected. Gym-like, PettingZoo-like, CybORG-like, service, script, human, and -LLM-agent interfaces can all be useful realizations. None is the portable ACES -semantic authority. ACES claims must be expressed through its own contracts, +LLM-agent interfaces can all be useful realizations. None is the portable RAES +semantic authority. RAES claims must be expressed through its own contracts, evidence, capability, and conformance surfaces. ### Treat behavior modes as free-form strings @@ -268,7 +268,7 @@ backend acts. ### Risks - If a child issue treats action names or tool labels as action contracts, - ACES behavior portability will be overstated. + RAES behavior portability will be overstated. - If behavior modes are duplicated outside the controlled vocabulary, run comparability and conformance will drift. - If authority or scope is enforced only by credentials or backend sandboxing, diff --git a/docs/decisions/adrs/adr-072-validation-and-admission-profiles.md b/docs/decisions/adrs/adr-072-validation-and-admission-profiles.md index 72ddb5132..24ccd9bbb 100644 --- a/docs/decisions/adrs/adr-072-validation-and-admission-profiles.md +++ b/docs/decisions/adrs/adr-072-validation-and-admission-profiles.md @@ -20,7 +20,7 @@ spawned implementation issues #258 and #259. ## Context -ACES already has several validation and admission surfaces: +RAES already has several validation and admission surfaces: - SDL parsing and closed-world model validation; - SDL semantic validation, reference resolution, instantiation, compilation, @@ -40,7 +40,7 @@ consumers can over-read a weak signal as a strong validation result. ASR-511 requires the ecosystem to define layered validation and admission profiles that distinguish structural, semantic, behavioral, and stronger -validity claims. ASR-515 requires ACES to preserve and expose the profile, +validity claims. ASR-515 requires RAES to preserve and expose the profile, strength, and limitations of the basis used for scenarios, tasks, runs, studies, and related claims. These requirements have to be designed together: the disclosure shape depends on the profile taxonomy, and the taxonomy is not @@ -56,10 +56,10 @@ disclosure discipline. ### 1. Profiles name the kind and strength of a validation basis -ACES validation/admission profiles use an ordered strength vocabulary: +RAES validation/admission profiles use an ordered strength vocabulary: - `structural`: syntax, schema, closed-world shape, type, and vocabulary checks. -- `semantic`: structural validation plus ACES domain invariants, reference +- `semantic`: structural validation plus RAES domain invariants, reference resolution, lifecycle separation, and cross-artifact consistency checks. - `behavioral`: semantic validation plus a concrete processor, backend, conformance, runtime, or admission path that exercised the relevant behavior @@ -133,7 +133,7 @@ name the publication scope or audience when that matters. Portable profile ids, strength classes, gate kinds, limitation categories, and subject kinds are governed vocabulary terms. Backend-specific or processor-specific terms use the existing `x-:` extension -discipline and cannot replace the ACES portable terms. +discipline and cannot replace the RAES portable terms. This decision does not introduce a second schema registry, validator stack, claim graph, evidence store, admission service, profile loader, or persistence @@ -171,7 +171,7 @@ or persistence changes remain owned by #258 and #259. ### Positive -- ACES gains one vocabulary for explaining whether a validation claim is only +- RAES gains one vocabulary for explaining whether a validation claim is only structural, semantic, behavioral, evidence-backed, or falsification-backed. - Consumers can inspect which gate produced a claim and what limits it. - Existing scenario, experiment-core, participant-runtime, conformance, and diff --git a/docs/decisions/adrs/adr-075-ecosystem-versioning-deprecation-and-migration-governance.md b/docs/decisions/adrs/adr-075-ecosystem-versioning-deprecation-and-migration-governance.md index 35ab8d74f..842289403 100644 --- a/docs/decisions/adrs/adr-075-ecosystem-versioning-deprecation-and-migration-governance.md +++ b/docs/decisions/adrs/adr-075-ecosystem-versioning-deprecation-and-migration-governance.md @@ -16,7 +16,7 @@ Waivers: none ## Context -ACES now has several independently versioned surfaces: +RAES now has several independently versioned surfaces: - the Python distribution and Git tags; - published JSON Schema contract lineages; diff --git a/docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md b/docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md index 3392597b7..3508b939d 100644 --- a/docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md +++ b/docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md @@ -2,7 +2,7 @@ ## Status -accepted +superseded by ADR-095 ## Date diff --git a/docs/decisions/adrs/adr-095-identity-cutover-and-historical-record-boundary.md b/docs/decisions/adrs/adr-095-identity-cutover-and-historical-record-boundary.md new file mode 100644 index 000000000..c8db55be0 --- /dev/null +++ b/docs/decisions/adrs/adr-095-identity-cutover-and-historical-record-boundary.md @@ -0,0 +1,131 @@ +# ADR-095: Identity Cutover and Historical-Record Boundary + +## Status + +accepted + +## Date + +2026-07-26 + +## Classification + +Classification: FM2 + +Required artifacts: ADR, governed contract-migration evidence, whole-tree +verification, and regression tests. + +Waivers: no compatibility alias, runtime migration service, persistence +registry, second validator family, or new exception hierarchy is introduced. + +## Context + +GOV-944 completes the repository identity cutover. The remaining occurrences +span published schemas, closed contract data, wire names, host-visible +artifacts, environment and workflow bindings, source identifiers, examples, +and configuration. They are not interchangeable: a schema identity, a JSON +property, an authentication header, a guest transport marker, and a prose +reference have different owners and compatibility consequences. + +ADR-093 established the current project identity and hard-cut public import, +command, and distribution boundaries. It deliberately left governed contracts, +runtime artifacts, and workflow identifiers to their owning controls. ADR-009, +ADR-061, and ADR-075 already assign authority, schema evolution, and lifecycle +responsibilities; they must not be bypassed by a repository-wide text edit. + +The repository has no complete-tree guard against reintroducing the retired +identity. Existing positioning checks are intentionally narrow, and the +general policy-exception mechanism is a temporary waiver channel rather than a +record of immutable history. + +## Decision + +RAES is the only current repository-owned identity. Every current tracked +surface uses RAES, including contract identifiers and schema URIs, wire keys +and topics, runtime artifact and filesystem names, environment and workflow +keys, generated outputs, source identifiers, configuration, examples, and +prose. The canonical published-schema URI root is +`https://raes.dev/schemas/`. + +The cutover is a hard boundary. A renamed input, artifact, header, event, or +schema identity is not accepted as a fallback, alias, wrapper, redirect, +dual-read path, or compatibility mode. A surface that needs migration guidance +uses the existing lifecycle documentation and records; it does not keep a +retired runtime value alive. External owners of domains, quality-service keys, +or workflow-project identities must provision their replacement before the +repository points to it. This repository neither proves DNS ownership nor +implements an external redirect. + +Published-schema changes remain governed by ADR-061 and ADR-009. The +normative checked-in schema, its reference-model source, generated bundle, +publication record, fixtures, and consumer tests change together. Renaming a +schema path or contract id is a removal plus replacement for publication +purposes: retain the existing tombstone/change-ledger evidence, even though +runtime compatibility is not retained. A property name, discriminator, +extension keyword, event topic, or header is a payload-breaking change and is +tested as such. Draft status does not make a wire rename invisible. + +The repository adds one policy-time whole-tree retired-identity check. It +enumerates the tracked tree directly from Git, including hidden and generated +files, and fails closed when an inspected file, policy input, or historical +record cannot be read safely. Its matcher distinguishes standalone and +qualified identity tokens from incidental substrings. It runs in the canonical +`nox` policy/verification graph, not only on changed files or in an optional +developer command. It reports bounded path and location diagnostics through +the existing policy failure envelope. + +Historical retention is narrow and explicit. An exemption is valid only for +an exact tracked record whose purpose is to preserve a dated or immutable fact, +with its record class, rationale, and content identity recorded. Directory +prefixes, globs, generic policy waivers, generated-file exclusions, and +unbounded prose exemptions are invalid. A content change invalidates the +historical exemption unless it remains a separately verified historical record. +Accepted ADR pins, release history, provenance evidence, and dated research +records may supply that evidence at their owning boundary; they do not make +all documentation historical. + +## Alternatives Considered + +Use the project-positioning checker. Rejected: its fixed entrypoint set proves +framing, not the absence of a retired identity throughout repository-owned +contracts and artifacts. + +Use the shared policy-exception file. Rejected: expiration and path-scoped +waivers are appropriate for temporary policy debt, but not for permanent, +auditable historical facts. + +Keep aliases or dual-read paths while changing current output. Rejected: that +would leave a live retired identity, create ambiguous precedence and header +smuggling risks, and contradict the required hard cut. + +Create a central identity registry or migration service. Rejected: contract, +runtime, workflow, and publication owners already provide the relevant seams. +A new cross-package abstraction would conflate their validation and lifecycle +rules. + +## Consequences + +The implementation must use the existing contract models, schema bundle, +publication manifest, lifecycle records, policy-failure envelope, and `nox` +graph rather than parallel inventories or validators. It must preserve each +owner's existing parse, authorization, redaction, path-safety, fixed-argv, and +diagnostic behavior while changing only the identity value. + +The full-tree check has one extension seam: an exact, content-bound historical +record entry. Adding a future historical class requires its owning immutable +evidence and checker validation; adding a new current surface requires no +allowlist entry and therefore fails until it uses the RAES identity. This is a +verification seam, not a runtime configuration or persistence surface. + +Existing external consumers must treat renamed contract and wire values as +breaking. The repository supplies governed removal and migration evidence, but +does not claim backwards compatibility, exact data conversion, or automatic +host-state cleanup. The cutover does not alter SDL, runtime, authorization, +validation, or evidence semantics beyond their identity-bearing values. + +## References + +- [ADR-009](adr-009-normative-artifact-authority-and-repository-structure.md) +- [ADR-061](adr-061-published-schema-evolution-policy.md) +- [ADR-075](adr-075-ecosystem-versioning-deprecation-and-migration-governance.md) +- [ADR-093](adr-093-raes-rename-and-compatibility-boundaries.md) diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 64d3fc73e..cf19ded83 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -427,14 +427,9 @@ adrs: - date: 2026-07-24 ref: "#861" summary: "Required exact action provenance and capability-specific atomic participant batching." - - id: ADR-093 - path: docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md - pin: 72725b8f09063b0b1d8ec65c4fda58e5648113035588ee4effbece5e0b1c3436 - amendments: - - ref: "#866" - - ref: "#866-pypi-name-correction" - - ref: "#884" - - ref: "#894" - id: ADR-094 path: docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md pin: 90578754323795ca8775c47dd6095752ccb5a769a0f932ebe60abbe1d8954246 + - id: ADR-095 + path: docs/decisions/adrs/adr-095-identity-cutover-and-historical-record-boundary.md + pin: efb1d5fffe00e8a5ac95b67b686d59973cb250254e0e11343fd31a20b881a803 diff --git a/docs/decisions/issue-908-aces-naming-audit-preflight.md b/docs/decisions/issue-908-aces-naming-audit-preflight.md deleted file mode 100644 index 526dcdeb5..000000000 --- a/docs/decisions/issue-908-aces-naming-audit-preflight.md +++ /dev/null @@ -1,206 +0,0 @@ -# Issue 908 ACES Naming Audit Preflight - -Date: 2026-07-26 - -Issue: #908. Requirement: none. The GitHub issue is the authoritative audit -contract. - -This note classifies the remaining ACES-bearing surfaces before implementation. -It does not authorize a repository-wide replacement. The issue supplies an -inventory and asks for decisions on contract and runtime identities; it does -not provide acceptance criteria for an in-place breaking migration. - -## Architecture Decision - -ADR-093 remains the rename authority. Issue #908 completes its classification, -not the earlier Python import cut: - -| Surface | Disposition for #908 | Owning authority | -|---|---|---| -| Current non-contract prose, comments, and private symbol names | May use RAES when meaning and emitted values do not change | Owning docs/package/tool | -| `https://aces.dev/schemas/...` | Retain for every current schema lineage; the namespace is not retired by this issue | ADR-061, published schemas, schema-publication records | -| Contract ids, profile ids, `$defs` names, annotation keywords, wire discriminators, and wire keys | Retain in current contract versions; do not rename in place | Normative schemas/fixtures/specs and closed DTOs | -| `aces-reference-processor` and other apparatus identities | Retain until an apparatus-identity migration names producer, consumer, version, and conformance evidence | Manifest owners and manifest authority | -| `aces.lock.json`, `aces-trust.yaml`, OCI media types/annotations, evidence schema ids, auth headers, host labels, kernel parameters, and resource prefixes | Retain; each is an external config, artifact, security, guest, or host-ownership protocol | Owning registry, runtime, operations, or backend boundary | -| Participant `aces.*` values | Retain as wire `schema_name` and status-mapping vocabulary; they are not message-broker topics | Participant-runtime spec, DTOs, schemas, and fixtures | -| `ACES_REQUIREMENT_UID` and `ACES_REAL_LIBVIRT_URI` | Retain as the only actual environment-variable surfaces in the audit | Requirement governance and opt-in libvirt certification | -| `ACES_NATIVE` and `ACES_RELATIVE_TO_SOURCE` | Retain as Python enum member names for provenance wire values; they are not environment variables | Provenance DTO and lineage ledger | -| Accepted ADRs, changelog history, provenance ledger, pinned research/evidence, citations, and external URLs | Preserve as historical or immutable evidence | ADR-059, ADR-080, and the owning record | - -The current schema URI namespace is an identifier, not a request to resolve -schemas over the network. Repository validation uses local published schemas -and `schema_bundle()`; no HTTP resolver is present. A future RAES URI namespace -is eligible only after its target is owned and publishable, old identities -remain resolvable or are explicitly deprecated, and old and new lineages can -coexist. A global base-URL replacement is not a valid cutover. - -All current schema-publication entries are `draft`, but `draft` does not make -consumer-visible identity changes non-breaking. It permits governed evolution -under ADR-061; it does not waive migration evidence, fixture coordination, or -the ecosystem compatibility rules. - -No ADR amendment is required. ADR-093 already separates project identity from -contract identity, and `docs/migration/raes-rename.md` is the surface map for -the retained decisions above. - -## Canonical Incumbents - -Implementation must reuse these owners rather than introduce a rename layer: - -- Authority and publication: ADR-009, ADR-019, - `specs/authority/authority-boundary.yaml`, ADR-061, - `contracts/schema-publication-manifest.json`, - `contracts/schema-publication/entries/`, - `tools/check_schema_publication.py`, `schema_bundle()`, and - `tools/check_generated_schemas.py`. -- Compatibility and lifecycle: ADR-075, - `specs/evolution/versioning-deprecation-and-migration.md`, - `specs/evolution/deprecation-records.yaml`, - `tools/check_deprecation_lifecycle.py`, and - `docs/migration/raes-rename.md`. -- Contract shape and validation: `raes_contracts._base.ContractModel`, - `raes._base.SDLModel`, the existing Pydantic models, local Draft 2020-12 - JSON Schema validation, normative fixtures, `tools/check_json_artifacts.py`, - and `raes_contracts.corpus`. -- Semantic annotations: the existing `schema_constraints.py`, - `schema_invariants.py`, and `contracts/schemas/profiles/` - `aces-semantic-invariants-v1.json` profile. Internal helper names may change; - the published `x-aces-*` protocol may not change accidentally with them. -- Module/config security: `raes.module_registry` constants and loaders, - `yaml.safe_load`, closed `TrustPolicy`/`Lockfile` models, digest/signature - checks, capped OCI reads, safe archive extraction, and `raes_cli.sdl`. -- Apparatus and runtime artifacts: the existing processor/backend manifest - renderers and closed models; evidence builders and their shared redaction - gate; participant-runtime DTOs/spec/fixtures; and the current artifact - validators. Do not add a second identifier catalog. -- Host ownership: `provider_resource_name()`, the libvirt fixed UUID namespace, - OCI ownership labels and inspection join, libvirt ownership checks, - structured XML builders, fixed argv execution, bounded timeouts, and - ownership-confined teardown. -- Runtime security and persistence: - `ControlPlaneSecurityConfig.strict_defaults()`, `ControlPlaneRole`, - bearer/trusted-proxy authentication, `request_size_guard_response()`, - request fingerprints/idempotency, append-only audit records, redacted - unexpected-error responses, `RuntimeSnapshot`, and - `LocalControlPlaneStore`. -- Errors and diagnostics: existing Pydantic `ValidationError`, - `SDLParseError`/`SDLValidationError`, Typer `BadParameter`/`Exit`, - package-local `Diagnostic`, MCP structured JSON, and bounded HTTP JSON - envelopes. There is no rename-specific exception hierarchy. -- Workflow: ADR-014, `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, - `.github/workflows/ci.yml`, `tools/check_repo_policy.py`, - `tools/check_requirement_governance.py`, `tools/verify_all.py`, gitleaks, - private-key detection, ADR pins, authority checks, publication checks, and - the owning focused tests. - -## Cross-Cutting Security And Whole-Path Gates - -- **Published contract ingress:** edit the normative schema/fixture/spec owner - first. JSON must remain locally parseable, closed DTOs keep - `extra="forbid"`, schema annotations keep their existing semantic validator, - publication records carry the current canonical hash and `last_change`, and - `schema_bundle()` remains byte-identical. Do not fetch remote `$ref` values or - make DNS/HTTP availability part of validation. -- **Auth surface:** `x-aces-client-verified` and - `x-aces-client-identity` are trusted-proxy protocol fields, not branding. - This issue leaves them unchanged. Any future dual-name adapter belongs in - `ControlPlaneSecurityConfig`, which already parameterizes the header names; - it must remain disabled under strict defaults, accept values only behind the - trusted proxy boundary, reject conflicting old/new headers, enforce - request-size limits, and audit denials. Role checks remain mandatory. The - current bearer-token branch returns before the proxy branch's target check; - do not claim bearer target scoping or build a header migration on that gap - without first unifying the post-authentication target check. -- **Secret surface:** no rename needs a secret, token, credential, private key, - environment dump, raw backend object, or real payload. Do not read secret - files or copy real values into fixtures, docs, logs, schema examples, or - migration records. Module trust keeps signature and digest validation. - Evidence keeps the shared redaction gate. -- **Environment/config shape:** `ACES_REQUIREMENT_UID` remains an external - Ground Control/workflow input. `ACES_REAL_LIBVIRT_URI` remains an opt-in test - input and must never carry credentials. The real-libvirt tests currently read - it directly; a naming cleanup must not add another alias or call path that - widens that pre-existing validation gap. Do not add dotenv or a generic - environment binder. -- **OS/host exposure:** OCI labels and libvirt names are passed by fixed argv; - kernel parameters are visible in the guest and host process boundary; guest - paths and evidence ids are persisted. The challenge kernel parameter must - remain a non-secret correlation value. Preserve the fixed libvirt UUID - namespace even if private Python symbols are renamed: changing its value - would make existing owned objects look foreign, create duplicates, or block - safe teardown. Do not use shell interpolation or place secrets in argv. -- **Error envelopes and logging:** a naming failure must flow through the - owning parser/DTO/CLI/diagnostic/HTTP envelope. Do not include raw input, - environment values, host paths, native stdout/stderr, schema bodies, or - tracebacks. Existing audit and logs are evidence, not a second migration - ledger; do not rewrite old records or add a rename logger. -- **Persistence:** lockfiles, trust policies, contract fixtures, evidence - artifacts, runtime snapshot histories, append-only audit data, and native - ownership markers may contain retained identifiers. Do not silently rewrite - them, normalize them on read, or add a migration database/service. A future - versioned reader adapter stays at the owning boundary and must preserve the - source artifact. -- **Workflow:** the branch is requirement-free. Do not invent a requirement UID - or bypass repository policy. Reuse nox and the existing owner-specific gates; - do not add a global “zero ACES tokens” rule because retained contracts and - historical evidence make zero both impossible and incorrect. - -## Extensibility Seams - -The parameter is the **surface class and version/lineage**, not a global -old-name/new-name pair. - -- A future schema namespace or contract-id migration extends the existing - per-contract `schema_bundle()` metadata path and schema-publication entry so - old and new lineages coexist. `_schema_id_for_contract_id()` is the existing - implementation seam; it must become lineage-aware rather than substitute one - global base URL. -- A future auth-header variation uses the already parameterized - `ControlPlaneSecurityConfig` fields. -- A future module artifact variation stays behind `LOCKFILE_NAME`, - `TRUST_POLICY_NAME`, media-type constants, and their existing loaders. -- A future provider naming variation uses the existing `name_prefix` input and - `provider_resource_name()` while preserving the separate ownership UUID - invariant. -- A future deprecation adds its surface class/record to the existing ecosystem - lifecycle policy and owning checker. It does not create a universal alias - registry, runtime lookup service, endpoint, store, or exception tree. - -## Gotchas And Anti-Patterns - -Avoid: - -- blind replacement, substring matching, or treating audit counts as a design; -- changing normative schemas only in Python or hand-editing generated copies; -- equating package names, schema `$id`, contract ids, `$defs` class names, - annotation keywords, apparatus ids, artifact ids, auth headers, config keys, - filenames, OCI labels, resource names, and prose; -- treating participant `schema_name` values as broker topics or introducing an - event bus/topic registry; -- treating provenance enum members as environment variables; -- renaming `_aces_uuid` while also changing its fixed namespace value; -- changing OCI labels or provider prefixes without preserving discovery, - ownership joins, rollback, and teardown of already-created resources; -- accepting both old and new config/header values with last-one-wins behavior; -- renaming a valid fixture string merely because the fixture is under - `contracts/fixtures/`; -- rewriting accepted ADRs, `CHANGELOG.md`, the normative lineage ledger, - pinned research/evidence, external URLs, or negative legacy-import tests; -- weakening closed-model, signature, digest, redaction, auth, request-size, - path-containment, schema-publication, ADR-pin, gitleaks, or private-key gates; -- adding duplicate schemas, validators, compatibility tables, workflow logic, - exception hierarchies, logs, stores, or migration services. - -## Non-Goals And Boundaries - -- Implementing issue #908 or reducing the occurrence count in this preflight. -- Retiring `aces.dev`, selecting an unverified RAES domain, or adding network - schema resolution. -- Renaming current contract lineages, wire fields, apparatus identities, - runtime artifacts, auth/config protocols, host ownership markers, or - persisted records. -- Changing SDL, contract, runtime, security, observability, persistence, - backend, or conformance semantics. -- Adding compatibility aliases for the already removed Python, CLI, or MCP - surfaces. -- Rewriting historical records solely to erase the former project name. diff --git a/docs/decisions/issue-908-raes-identity-cutover-preflight.md b/docs/decisions/issue-908-raes-identity-cutover-preflight.md new file mode 100644 index 000000000..1470c1625 --- /dev/null +++ b/docs/decisions/issue-908-raes-identity-cutover-preflight.md @@ -0,0 +1,143 @@ +# Issue 908 RAES Identity Cutover Preflight + +Date: 2026-07-26 + +Issue: #908. Requirement: GOV-944. + +## Binding Scope + +RAES is the only identity for current repository-owned surfaces. The cutover +covers published contracts, schemas, wire values, source identifiers, runtime +artifacts, environment and workflow inputs, host-visible names, current design +guidance, tools, examples, and configuration. + +The cutover is semantic rather than lexical. Each occurrence is migrated +through its owning boundary. No compatibility alias, fallback read, dual-name +parser, redirect, wrapper, or runtime rename service remains. + +Historical retention is limited to an exact record whose purpose is to +preserve an immutable or dated fact. Accepted pre-cutover ADR content, release +history, provenance records, and dated research or design evidence qualify +only when their exact path, content digest, record class, rationale, and +occurrence count are registered in the historical-record manifest. Directory +exemptions, globs, generated-file exclusions, and general policy waivers do +not qualify. + +ADR-095 is the terminal architecture authority. ADR-093 becomes a superseded +historical decision after ADR-095 is accepted. + +## Surface Decisions + +| Surface | Cutover decision | Owning authority | +|---|---|---| +| Published schema URI root | Use `https://raes.dev/schemas/` | ADR-061, schema bundle, publication records | +| Contract and profile identifiers | Rename in their current draft lineages and record removal/replacement evidence | Normative schemas, publication entries, fixtures | +| Extension keywords and wire properties | Rename atomically with models, validators, schemas, fixtures, and consumers | Closed DTO and schema owners | +| Authentication headers | Expose only the RAES header family | `ControlPlaneSecurityConfig` and runtime API | +| Participant and runtime vocabulary | Rename schema names, statuses, event values, and artifact identifiers together | Runtime DTOs, specs, fixtures | +| Module artifacts | Rename lockfile, trust policy, cache, media type, and OCI annotation values | Module registry | +| Host ownership | Rename OCI labels, libvirt prefixes, guest markers, paths, and ownership namespace | Backend drivers and guest appliance | +| Environment and workflow inputs | Rename repository-owned keys in policy, CI, tests, and guidance together | Requirement governance and real-libvirt certification | +| Source and prose | Use RAES in identifiers, comments, docstrings, current docs, tools, skills, and examples | Owning package or document | +| Historical evidence | Preserve only exact content-bound records | Historical-record manifest and owning immutability controls | + +## Canonical Incumbents + +- Contract shape and generation: + `raes_contracts._base.ContractModel`, `raes._base.SDLModel`, + `schema_bundle()`, `tools/generate_contract_schemas.py`, and + `tools/check_generated_schemas.py`. +- Publication: + `contracts/schema-publication-manifest.json`, entries and tombstones under + `contracts/schema-publication/`, and + `tools/check_schema_publication.py`. +- Contract lifecycle: + ADR-061, ADR-075, + `specs/evolution/versioning-deprecation-and-migration.md`, and + `specs/evolution/deprecation-records.yaml`. +- Semantic annotations: + `schema_constraints.py`, `schema_invariants.py`, and the published RAES + semantic-invariant profile. +- Security: + `ControlPlaneSecurityConfig.strict_defaults()`, closed request models, + request-size guards, role checks, denial audit events, and redacted error + envelopes. +- Module security: + closed trust and lockfile models, safe YAML/JSON loading, digest and + signature verification, bounded registry reads, and safe archive extraction. +- Host boundaries: + `provider_resource_name()`, OCI ownership inspection, libvirt ownership UUID + checks, structured XML construction, fixed argv, bounded timeouts, and + ownership-confined teardown. +- Workflow: + `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, + `.github/workflows/ci.yml`, repository policy, requirement governance, + schema/publication gates, gitleaks, and private-key detection. + +## Cross-Cutting Requirements + +### Contracts and persistence + +Change reference-model sources before generated schemas. Regenerate the +published tree and update every publication entry with its current canonical +hash and contract-facing `last_change` record. A renamed schema path is a +removal plus replacement and carries a tombstone even though runtime +compatibility is intentionally absent. + +Lockfiles, trust policies, runtime snapshots, evidence artifacts, OCI labels, +libvirt names, and guest paths are breaking persistence or host boundaries. +The repository does not silently rewrite existing host state or accept an old +artifact as a fallback. + +### Authentication and errors + +Trusted-proxy headers change through the existing security configuration. +Strict defaults, proxy trust, authentication, role checks, request-size +limits, conflict rejection, denial auditing, and redacted failures remain +unchanged. No request accepts both identity families. + +Renamed validation failures continue through existing Pydantic, SDL, CLI, MCP, +runtime diagnostic, and HTTP error envelopes. No rename-specific exception +hierarchy or raw payload logging is introduced. + +### Secrets and operating-system exposure + +The cutover needs no credential, token, private key, environment dump, or real +payload. Connection URIs may contain credentials and therefore remain absent +from logs and evidence. + +Guest writer and parser changes land together. Fixed argv, no-shell execution, +bounded reads, timeouts, safe path handling, archive containment, file modes, +redaction, and resource ownership checks remain intact. + +### Whole-tree verification + +The naming gate enumerates Git-tracked files directly and scans bytes so +hidden, generated, and non-text artifacts cannot evade it. A token-aware +matcher prevents incidental substrings from becoming false positives. + +Historical entries are exact and content-bound. A content or occurrence-count +change invalidates the entry. The gate does not consume +`tools/policy/exceptions.yaml`. + +## External Identity Preconditions + +The repository targets these final identities: + +- schema namespace: `https://raes.dev/schemas/` +- Sonar project: `Brad-Edwards_raes` +- Ground Control project: `raes-sdl` + +Their remote resources must exist before the corresponding repository +configuration is considered verified. OBL-908-REMOTE-IDENTITIES-1 records that +provisioning dependency. + +## Non-Goals + +- changing SDL, runtime, authorization, validation, or evidence semantics + beyond identity-bearing values +- accepting old and new names together +- adding automatic host cleanup or data conversion +- creating a central identity registry, migration service, persistence store, + endpoint, or exception family +- editing historical records merely to reduce an occurrence count diff --git a/docs/explain/reference/normative-artifact-authority.md b/docs/explain/reference/normative-artifact-authority.md index 615ad9a46..4a1506484 100644 --- a/docs/explain/reference/normative-artifact-authority.md +++ b/docs/explain/reference/normative-artifact-authority.md @@ -82,7 +82,7 @@ but it still passes through these gates: (`schema_bundle()`) still generates an identical bundle (into a throwaway directory — it never overwrites the published authority). - Requirement governance: changed governed paths must carry the `ASR-517` - context through the branch name or `ACES_REQUIREMENT_UID`, and Ground Control + context through the branch name or `RAES_REQUIREMENT_UID`, and Ground Control traceability must stay aligned. - Secret and host exposure: authority docs, fixtures, diagnostics, command-line examples, and generated artifacts must not include bearer tokens, @@ -129,7 +129,6 @@ Avoid: artifacts). `docs/`, `implementations/`, `examples/`, `research/`, `notes/`, and `tools/` are non-normative roots per the authority manifest and may not host authority artifacts -- recreating the retired `implementations/python/src/aces/` namespace - preserving legacy or transitional path names as current authority when `ADR-009` already defines the target model - making invalid fixtures multi-concern when one focused fixture can prove the diff --git a/docs/explain/reference/realization-envelopes.md b/docs/explain/reference/realization-envelopes.md index 5b8ff3b00..7bcc5b424 100644 --- a/docs/explain/reference/realization-envelopes.md +++ b/docs/explain/reference/realization-envelopes.md @@ -161,7 +161,7 @@ raes libvirt techvault guest-certify \ --project-dir . --run-id guest-proof-1 --yes ``` -The equivalent opt-in pytest is gated on `ACES_REAL_LIBVIRT_URI`, and +The equivalent opt-in pytest is gated on `RAES_REAL_LIBVIRT_URI`, and `tools/real-daemon/run_aws_guest_certify.sh` runs the whole thing on an ephemeral, self-cleaning host. A committed real-daemon evidence report lives under `tools/real-daemon/evidence/`. diff --git a/docs/explain/reference/reference-emulation-backend.md b/docs/explain/reference/reference-emulation-backend.md index ae2e07dfb..dc26bd742 100644 --- a/docs/explain/reference/reference-emulation-backend.md +++ b/docs/explain/reference/reference-emulation-backend.md @@ -77,7 +77,7 @@ and diagnostic surfaces and provide evidence for the runtime effect it claims. from raes_reference_backend import create_reference_backend_target from raes_reference_backend.drivers.oci import OciDeploymentDriver -driver = OciDeploymentDriver(runtime="docker", workspace="aces-ref") +driver = OciDeploymentDriver(runtime="docker", workspace="raes-ref") target = create_reference_backend_target(driver=driver) ``` diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index e87668c2a..40927def2 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -122,7 +122,7 @@ literature. The cyber-range and V&V sources are adjacent methodological support: Russo/Costa/Armando, Swiler, Oberkampf/Roy, and Sargent are citable proceedings, technical-report, or book sources; Garg et al. is used as a current survey preprint rather than as settled normative authority. The working Zotero library -tracks these identity-authority references under `aces-sdl-identity-authority` +tracks these identity-authority references under `raes-sdl-identity-authority` and the adjacent V&V subset under `adjacent-vv-lineage`; because that library is private, the Garg et al. preprint citation is also snapshotted in-repo under [`docs/research/primary/`](../../research/primary/literature/cyber-range-scenario-survey.md) @@ -847,7 +847,7 @@ which dynamic queue/log/config details remain evidence or bounded settings. and source audit remain unchanged. - DSL-142 composes the existing orchestration-inject, participant-observation, SEM-230 information-flow, ACT-617 mixed-control, shared-time, and evidence - lineages without introducing another external derivation. The exact ACES + lineages without introducing another external derivation. The exact RAES mapping is `ParticipantBehaviorSpecification.participant_inject_deliveries`: one participant, the original inject and event/script/story occurrence @@ -1075,7 +1075,7 @@ which dynamic queue/log/config details remain evidence or bounded settings. - RUN-310 composes the same participant-interface, append-only event-history, mixed-control, information-flow, and access-control lineage into live supervisory mediation; it introduces no new external semantic source. The - exact RAES/ACES SDL mapping is trusted + exact RAES/RAES SDL mapping is trusted `ParticipantBehaviorSpecificationRuntime.controller_states` and `control_transitions` for policy authority, closed `Participant*ControlIntent` models for caller-owned intent, diff --git a/docs/explain/sdl/parser.md b/docs/explain/sdl/parser.md index b97d57047..5f26a0231 100644 --- a/docs/explain/sdl/parser.md +++ b/docs/explain/sdl/parser.md @@ -203,8 +203,8 @@ Top-level composition supports: - `imports` using backward-compatible `path:` or canonical `source:` - `source:` classes `local:`, `oci:`, and `locked:` - repo-owned trust and resolution files: - - `aces.lock.json` - - `aces-trust.yaml` + - `raes.lock.json` + - `raes-trust.yaml` Import `source:` values are not treated as ordinary SDL package-source shorthand. They are resolved by the composition layer, not expanded into diff --git a/docs/explain/sdl/runtime-architecture.md b/docs/explain/sdl/runtime-architecture.md index 39fa471fb..95b41b0ff 100644 --- a/docs/explain/sdl/runtime-architecture.md +++ b/docs/explain/sdl/runtime-architecture.md @@ -403,7 +403,7 @@ Composition is registry-ready as well: - local imports remain supported through `path:` and `source: local:...` - reusable remote modules use `source: oci:...` - concrete resolved imports may be pinned via `source: locked:...` -- `raes sdl resolve` writes `aces.lock.json` +- `raes sdl resolve` writes `raes.lock.json` - `raes sdl verify-imports` verifies lockfile, trust, digests, and signatures - `raes sdl publish` packages a publishable SDL module as an OCI image layout diff --git a/docs/explain/sdl/scientific-scenario-completeness.md b/docs/explain/sdl/scientific-scenario-completeness.md index 1e979e120..325cff32c 100644 --- a/docs/explain/sdl/scientific-scenario-completeness.md +++ b/docs/explain/sdl/scientific-scenario-completeness.md @@ -15,7 +15,7 @@ The current assessment is deliberately conservative. Only blocking concerns directly, including authored/observed-state binding, specificity, teardown, credentials, time and clocks, participant budgets, verifiers, hidden assets, and trajectories. Behavioral-relation semantics are -now implemented as `aces-behavioral-relations@rev1`, while the stronger formal +now implemented as `raes-behavioral-relations@rev1`, while the stronger formal relations it defines retain their honest unproved or future assurance states. These profiles are scope contracts, not validators that silently strengthen diff --git a/docs/explain/sdl/sections.md b/docs/explain/sdl/sections.md index 0a18742e5..1e07a712a 100644 --- a/docs/explain/sdl/sections.md +++ b/docs/explain/sdl/sections.md @@ -1387,7 +1387,7 @@ propositions: predicate: kind: boolean property: service-alive - semantic_ref: urn:aces:observable:service-alive + semantic_ref: urn:raes:observable:service-alive operator: equals expected: true evidence_requirements: [web-health-evidence] diff --git a/docs/lessons/README.md b/docs/lessons/README.md index 2c87c10d9..8c9e23a25 100644 --- a/docs/lessons/README.md +++ b/docs/lessons/README.md @@ -68,7 +68,7 @@ Body sections (use the ones that apply, omit the rest): claim, or the conformance suite's assumption going in. - **What we found** — the actual behavior or shape from the integrating backend. -- **Decision** — what landed in the current PR. Use one of: `fix-in-aces`, +- **Decision** — what landed in the current PR. Use one of: `fix-in-raes`, `fix-in-backend`, `cross-repo-coordination`, `accept`, `escalate`. Do not use `defer` — record a `follow_ups` issue instead. - **Why this side** — when fix could have landed on either repo, why we diff --git a/docs/migration/README.md b/docs/migration/README.md index 2071d1120..96416a9fe 100644 --- a/docs/migration/README.md +++ b/docs/migration/README.md @@ -5,10 +5,9 @@ established the current layout. The paths below describe historical moves, not current uncertainty about where authoritative material lives. The current project identity migration is recorded separately in -[RAES Rename Migration Map](raes-rename.md). That map records the hard cutover -to RAES names and identifies the narrow ACES identifiers that remain only as -source import paths, governed contract identifiers, historical records, or -external references. +[RAES Identity Cutover](raes-rename.md). That note records the final hard +cutover across repository-owned live surfaces and the exact historical-record +boundary. The reorganization moved existing material into the current long-term buckets: diff --git a/docs/migration/raes-rename.md b/docs/migration/raes-rename.md index ac81cae5c..c4bb5cf16 100644 --- a/docs/migration/raes-rename.md +++ b/docs/migration/raes-rename.md @@ -1,83 +1,54 @@ -# RAES Rename Migration Map - -Issue #866 hard-cuts the current project identity from Agentic Cyber -Environment System (ACES) to Reproducible Agentic Environments System (RAES). -Repository-owned current prose, package distribution metadata, command examples, -MCP discovery, MCP public tool names, emitted public titles, and -machine-readable guidance identifiers use RAES. - -Old ACES public command and MCP aliases are removed, not preserved as -compatibility surfaces. - -## Cutover Rule - -Use RAES for current user-facing and machine-readable surfaces owned by this -repository. Do not add new ACES aliases for current public commands, MCP tools, -package distribution metadata, or guidance profile identifiers. - -Keep ACES only when the identifier is one of these historical or governed -surfaces: - -- governed SDL, schema, profile, fixture, provenance, or wire identifier whose - rename requires a separate versioned contract migration -- retained documentation path or asset-inventory reference that still has - tests, links, or downstream evidence attached to the old path -- workflow or quality-service key owned by external automation -- accepted historical decision record, changelog history, research snapshot, - archived citation, external URL, or third-party reference - -## Surface Map - -| Surface class | Old identifier | Current identifier | Owner | Status | Verification evidence | -|---|---|---|---|---|---| -| Project prose | Agentic Cyber Environment System (ACES), ACES SDL | Reproducible Agentic Environments System (RAES), RAES SDL | Repository docs | Migrated for current-state prose | Docs build and prose review | -| Documentation links | `Brad-Edwards/aces` current README/docs links | `RAESystem/rae` | Current README and docs config | Migrated for current user-facing links | Docs build | -| Documentation paths | `docs/aces/` | `docs/raes/` | Documentation tree | Hard cut to the current project name | Docs build and existing path tests | -| Python distribution | `aces-sdl` | `raes` | `implementations/python/pyproject.toml` | Renamed for new PyPI publication | Version and corpus packaging tests | -| Canonical Python SDL import | `aces_sdl` | `raes` | SDL package owner | Hard cut by #884; no alias or shim | Source import tests plus isolated wheel/sdist tests | -| Other Python packages | `aces_mcp`, `aces_runtime`, `aces_contracts`, other owning `aces_*` packages | `raes_mcp`, `raes_runtime`, `raes_contracts`, and corresponding `raes_*` owners | Python package owners | Hard cut; no alias packages | Module-boundary and installed-wheel negative import tests | -| CLI command | `aces` | `raes` | `raes_cli` | Old console script removed | CLI version/help and installed-wheel tests | -| MCP server command | `aces-mcp` | `raes-mcp` | `raes_mcp` | Old console script removed | Packaging and MCP construction tests | -| MCP server id | `aces-sdl` | `raes` | `raes_mcp.server` | Migrated emitted server name | MCP server construction tests | -| MCP discovery tool | `aces_tool_surface` | `raes_tool_surface` | `raes_mcp.tools.operations` | Old tool removed | MCP tool-surface tests | -| MCP guidance tool | `aces_agent_guidance` | `raes_agent_guidance` | `raes_mcp.tools.operations` | Old tool removed | MCP guidance tests | -| MCP intended-use tool | `aces_intended_use_profiles` | `raes_intended_use_profiles` | `raes_mcp.tools.completeness` | Old tool removed | MCP intended-use tests | -| MCP reference-manifest tool | `aces_reference_manifests` | `raes_reference_manifests` | `raes_mcp.tools.operations` | Old tool removed | MCP advertised-tool tests | -| Agent guidance profile id | `aces-agent-guidance` | `raes-agent-guidance` | `specs/agent-guidance/agent-guidance.yaml` | Migrated canonical profile id | `tools/check_agent_guidance.py` and MCP guidance tests | -| Intended-use scope string | `aces-delivery-capability` | `raes-delivery-capability` | `raes_mcp.tools.completeness` | Migrated emitted scope; `legacy_scope` removed | MCP intended-use tests | -| Runtime OpenAPI title | ACES Runtime Control Plane | RAES Runtime Control Plane | `raes_runtime.control_plane_api` | Migrated emitted title | Version-classification tests | -| HTTP security headers | `x-aces-client-verified`, `x-aces-client-identity` | Retained security-protocol keys | `ControlPlaneSecurityConfig` and the trusted-proxy boundary | Not aliases for project prose; a future migration must preserve fail-closed auth and reject conflicting old/new values | Runtime auth, role, request-size, denial-audit, and redacted-error tests | -| Environment and external workflow keys | `ACES_REQUIREMENT_UID`, `ACES_REAL_LIBVIRT_URI` | Retained external/workflow keys | Requirement governance and opt-in real-libvirt certification | Outside the Python namespace cut; uppercase provenance enum members such as `ACES_NATIVE` are not environment variables | Repo-policy and opt-in integration-test gates | -| Published schema URI namespace | `https://aces.dev/schemas/...` | Retained for every current schema lineage | `contracts/schemas/`, schema-publication records, and `schema_bundle()` | Not retired by #908; a future namespace must coexist with old identities and prove target ownership/redirect behavior | Contract, generated-schema parity, and schema-publication checks | -| Published schemas and wire ids | `io.aces.*`, `x-aces-*`, `aces-*`, contract discriminators and wire field names | Retained governed contract identifiers | `contracts/`, closed DTOs, and ADR-061 schema publication | Current lineages are not renamed in place; a replacement requires its owning versioned contract migration | Contract models, fixtures, JSON Schema validation, and publication checks | -| Module registry artifacts | `aces.lock.json`, `aces-trust.yaml`, `.aces/module-cache`, `application/vnd.aces.*`, `io.aces.module.id` | Retained module/config protocol | `raes.module_registry` | User-authored, persisted, signed, or registry-visible values; not prose cleanup | Closed SDL models, safe YAML/JSON parsing, digest/signature, registry, archive, and CLI tests | -| Runtime and evidence artifact ids | `aces.libvirt.*`, `aces.cross-backend-*`, `aces.participant_runtime.*`, `aces.lifecycle.*` and related values | Retained artifact/wire vocabularies | Owning evidence builders, participant-runtime spec, DTOs, and fixtures | These are schema/discriminator/status values, not a broker topic registry; migrate only with the owning artifact lineage | Artifact validators, redaction gates, DTO/fixture tests, and persistence round trips | -| Host/runtime ownership names | `aces.workspace`, `aces.address`, `aces.*` kernel parameters, libvirt/OCI name prefixes, fixed UUID namespace, guest paths | Retained host ownership and guest-protocol values | OCI/libvirt drivers and guest appliance | A cosmetic rename can orphan resources or defeat ownership proof; requires a separately bounded host migration | Fixed-argv, ownership-conflict, teardown, guest-probe, and real-daemon tests | -| Processor/backend identities | `aces-reference-processor`, package-specific manifest ids | Retained apparatus identities | Manifest owners | Outside the Python namespace cut in issue #884 | Manifest and conformance tests | -| Accepted ADRs, changelog, research snapshots | Historical ACES references | Retained history | Historical record owners | Not rewritten solely to erase the old name | ADR immutability and docs checks | - -## User Guidance - -Use `raes` and `raes-mcp` in command examples and automation. The old `aces` -and `aces-mcp` console scripts are not installed by the current package. - -New MCP clients should start with `raes_tool_surface`, then call -`raes_agent_guidance` and `raes_intended_use_profiles`. Clients must update off -`aces_tool_surface`, `aces_agent_guidance`, `aces_intended_use_profiles`, and -`aces_reference_manifests`; those tools are no longer registered. - -Use `raes` for PyPI publication, downstream package pins, and the canonical SDL -import: - -```python -from raes import parse_sdl_file -``` - -The old `aces`, `aces_sdl`, and `aces_*` namespaces are not installed and have -no compatibility aliases, shims, fallback imports, or namespace-package -residue. Code using those imports must change before upgrading. This -package-boundary cut is released as a breaking change. - -Do not rename SDL fields, schema ids, wire discriminators, or published -contract identifiers as part of ordinary prose cleanup. Those surfaces require -separate contract migrations with their own fixtures and publication evidence. +# RAES Identity Cutover + +RAES is the sole current identity of the Reproducible Agentic Environments +System and its repository-owned ecosystem surfaces. + +Issue #908 and GOV-944 complete the hard cut begun by issue #866. The cutover +does not retain public aliases or dual-name compatibility. + +## Current Surface Map + +| Surface | Current identity | Verification | +|---|---|---| +| Python distribution and imports | `raes` and the owning `raes_*` packages | installed wheel and source-boundary tests | +| CLI and MCP commands | `raes`, `raes-mcp` | CLI and MCP construction tests | +| MCP server and tools | RAES server metadata and `raes_*` tool identifiers | advertised-tool and guidance tests | +| Published schema namespace | `https://raes.dev/schemas/` | generated parity and schema-publication checks | +| Contract/profile identities | RAES contract, profile, annotation, and wire identifiers | contract models, fixtures, JSON Schema validation | +| Module artifacts | `raes.lock.json`, `raes-trust.yaml`, `.raes/module-cache`, RAES OCI media types and labels | registry, digest, signature, archive, and CLI tests | +| Runtime and evidence artifacts | RAES schema names, event/status values, evidence ids, and resource names | DTO, fixture, persistence, and backend tests | +| Authentication | RAES trusted-proxy headers | strict-default auth, role, denial-audit, and redacted-error tests | +| Environment/workflow | RAES requirement-governance and real-libvirt inputs | repository policy and opt-in integration tests | +| Host ownership | RAES OCI labels, libvirt names, guest markers, paths, and UUID namespace | conflict, discovery, teardown, guest-probe, and real-daemon tests | +| Documentation and examples | RAES terminology and paths | documentation build and whole-tree naming policy | + +## Compatibility Rule + +Current readers, writers, commands, and configuration accept only RAES +identities. Existing consumers must update before adopting the cutover release. +There are no aliases, shims, fallback imports, redirects, dual-read paths, or +last-one-wins resolution rules. + +Published contract changes still carry their owning schema-publication and +lifecycle evidence. That evidence records what changed; it does not keep the +retired value executable. + +## Historical Records + +Accepted pre-cutover decisions, release history, provenance, and dated research +or design evidence remain accurate historical records. The whole-tree naming +policy permits such content only through exact path, content digest, record +class, rationale, and occurrence-count entries. It does not exempt a +documentation directory or allow historical wording to become current +guidance. + +## Operator Guidance + +Use `raes` and `raes-mcp` in automation. Use RAES contract, schema, module, +runtime, header, environment, and host identifiers in newly created artifacts +and deployments. + +The hard cut does not automatically rewrite persisted artifacts or clean +resources created under an earlier identity. Operators should complete any +required environment cleanup before deploying the cutover release; the +repository does not discover or destroy old-name resources. diff --git a/docs/raes/inventory/ad-preflight.md b/docs/raes/inventory/ad-preflight.md index 4ff755a90..77dd21c0c 100644 --- a/docs/raes/inventory/ad-preflight.md +++ b/docs/raes/inventory/ad-preflight.md @@ -17,7 +17,7 @@ the issue phase marker, but the tool did not create a repo-local note file. - Do not create an APTL-local schema, parser, validator, Pydantic model, or runtime exception hierarchy for AD inventory facts. - Reuse `docs/raes/inventory/asset-inventory-methodology.md`, - `src/aptl/core/aces_inventory.py`, `src/aptl/cli/aces_inventory.py`, the + `src/aptl/core/raes_inventory.py`, `src/aptl/cli/raes_inventory.py`, the existing webapp and db inventory bundles, and `docs/raes/parity-inventory.yaml`. - Historical note: issue #516 supersedes the original blanket-redaction rule for source inventory bundles. Preserve AD administrator credentials, diff --git a/docs/raes/inventory/asset-inventory-issue-template.md b/docs/raes/inventory/asset-inventory-issue-template.md index 1ddcdf4a4..d346d63ad 100644 --- a/docs/raes/inventory/asset-inventory-issue-template.md +++ b/docs/raes/inventory/asset-inventory-issue-template.md @@ -88,7 +88,7 @@ For every captured fact, record one mapping disposition in - `encoded` - `encoded_with_caveat` -- `blocked_by_aces_gap` +- `blocked_by_raes_gap` - `blocked_by_aptl_gap` - `needs_gap_triage` only while actively triaging @@ -97,7 +97,7 @@ Before completion: - [ ] No `needs_gap_triage` mapping remains. - [ ] Every evidence file is referenced by a fact, provenance entry, correspondence check, or capture-limit fact. -- [ ] Every `blocked_by_aces_gap` row links an RAES issue. +- [ ] Every `blocked_by_raes_gap` row links an RAES issue. - [ ] Every `blocked_by_aptl_gap` row links a downstream backend issue. - [ ] Correspondence checks describe how later encoding work will compare RAES surfaces against fresh realized evidence. @@ -107,9 +107,9 @@ Before completion: Run the downstream ledger validator before closing: ```shell -aptl aces-inventory schema -aptl aces-inventory validate -aptl aces-inventory gaps +aptl raes-inventory schema +aptl raes-inventory validate +aptl raes-inventory gaps ``` Also run the backend repository's normal test and documentation checks for any diff --git a/docs/raes/inventory/asset-inventory-methodology.md b/docs/raes/inventory/asset-inventory-methodology.md index 57759f4b7..f1d2632f2 100644 --- a/docs/raes/inventory/asset-inventory-methodology.md +++ b/docs/raes/inventory/asset-inventory-methodology.md @@ -229,16 +229,16 @@ instead of pretending a local build recipe exists. - `encoded` when current RAES can represent the fact directly; - `encoded_with_caveat` when current RAES can represent the fact but the later encoding issue must preserve a stated limitation; - - `blocked_by_aces_gap` when RAES lacks a semantically correct surface; + - `blocked_by_raes_gap` when RAES lacks a semantically correct surface; - `blocked_by_aptl_gap` when RAES can express the fact but APTL cannot yet realize or consume that SDL; - `needs_gap_triage` only as a temporary local state before filing or linking the required issue. The ledger itself is a schema-governed artifact. The current reference - implementation is APTL's `aptl aces-inventory` CLI: `aptl aces-inventory + implementation is APTL's `aptl raes-inventory` CLI: `aptl raes-inventory schema` prints the current JSON Schema generated from the Pydantic model, - and `aptl aces-inventory validate ` fails schema, evidence-path, + and `aptl raes-inventory validate ` fails schema, evidence-path, and mapping-accountability violations. 11. Handle RAES gaps immediately. @@ -293,11 +293,11 @@ Required local tools: - `sha256sum` or equivalent coreutils for evidence checksums. - Trivy for image scanning, CycloneDX SBOM output, and vulnerability JSON. - A downstream ledger validator. The current reference implementation is - `aptl aces-inventory validate ` to validate the mapping ledger + `aptl raes-inventory validate ` to validate the mapping ledger and its evidence references. -- `aptl aces-inventory gaps ` to list the RAES and downstream +- `aptl raes-inventory gaps ` to list the RAES and downstream implementation issues later encoding work must resolve or consume. -- `aptl aces-inventory schema` to inspect the reference ledger schema. +- `aptl raes-inventory schema` to inspect the reference ledger schema. - Docker Buildx `imagetools inspect`, build-time `--sbom`, and `--provenance` or equivalent in-toto/SLSA attestation tooling for provenance capture. diff --git a/docs/raes/inventory/issue-516-redaction-boundary-preflight.md b/docs/raes/inventory/issue-516-redaction-boundary-preflight.md index ae8249ee4..aaddbdd97 100644 --- a/docs/raes/inventory/issue-516-redaction-boundary-preflight.md +++ b/docs/raes/inventory/issue-516-redaction-boundary-preflight.md @@ -38,7 +38,7 @@ canonical methodology, skill, or runtime ADRs. omission. `name_indicates_secret()` is advisory only and must not be copied into capture-time source evidence policy. - Ledger accountability: `mapping-ledger.yaml`, `capture-limits.txt`, evidence - checksums, and the downstream `aptl aces-inventory validate/gaps/schema` + checksums, and the downstream `aptl raes-inventory validate/gaps/schema` commands remain the evidence accountability surface. ## Security And Validation Layers diff --git a/docs/raes/inventory/kali-preflight.md b/docs/raes/inventory/kali-preflight.md index 8ed358436..6ba90cb37 100644 --- a/docs/raes/inventory/kali-preflight.md +++ b/docs/raes/inventory/kali-preflight.md @@ -13,7 +13,7 @@ asset-inventorying methodology. - The completion artifact is an RAES inventory bundle under `docs/raes/inventory/kali/`, using the existing `mapping-ledger.yaml` - schema, `aptl aces-inventory validate`, and the evidence/checksum shape + schema, `aptl raes-inventory validate`, and the evidence/checksum shape already used by `shuffle-backend` and `webapp`. - The inventory describes the realized `aptl-kali` container at one post-`aptl lab start` steady-state snapshot. If the capture is not from a @@ -39,8 +39,8 @@ asset-inventorying methodology. - Inventory methodology and ledger validation: `docs/raes/inventory/asset-inventory-methodology.md`, - `src/aptl/core/aces_inventory.py`, `src/aptl/cli/aces_inventory.py`, and - `tests/test_aces_inventory_methodology.py`. + `src/aptl/core/raes_inventory.py`, `src/aptl/cli/raes_inventory.py`, and + `tests/test_raes_inventory_methodology.py`. - Prior asset patterns: `docs/raes/inventory/shuffle-backend/`, `docs/raes/inventory/webapp/`, @@ -71,7 +71,7 @@ asset-inventorying methodology. `raes.parse_sdl_file` and compile through the RAES runtime compiler. Do not add local structural validators for RAES fields. - **Inventory ledger:** every captured fact needs an existing - `AcesSurface` mapping, caveat, or linked RAES issue in + `RaesSurface` mapping, caveat, or linked RAES issue in `mapping-ledger.yaml`. No `needs_gap_triage` rows should remain at review. - **Secret classification:** ADR-057 is canonical for scenario-target values, with ADR-029 still governing operator/control-plane handling. Preserve diff --git a/docs/raes/inventory/methodology-assurance-report.md b/docs/raes/inventory/methodology-assurance-report.md index b4a2f7ce4..47b7c80a3 100644 --- a/docs/raes/inventory/methodology-assurance-report.md +++ b/docs/raes/inventory/methodology-assurance-report.md @@ -12,14 +12,14 @@ captures and later RAES/downstream gap issues do the full encoding work. The methodology is defensible as a capture-to-specification workflow if it is used with explicit ledger gates. The current reference implementation is APTL's -`aptl aces-inventory` CLI: +`aptl raes-inventory` CLI: - `mapping-ledger.yaml` is the accountability artifact for every captured fact. -- `aptl aces-inventory validate ` validates the ledger schema, +- `aptl raes-inventory validate ` validates the ledger schema, evidence references, and mapping disposition requirements. -- `aptl aces-inventory gaps ` emits the actionable gap list later +- `aptl raes-inventory gaps ` emits the actionable gap list later issues must consume or fix. -- `aptl aces-inventory schema` emits the current JSON Schema generated from the +- `aptl raes-inventory schema` emits the current JSON Schema generated from the Pydantic ledger model. The methodology is not yet sufficient to claim final equivalence between a @@ -87,8 +87,8 @@ Each follow-on asset issue should meet this minimum: 4. For custom builds, emit or capture build-time SBOM/provenance attestations using Docker Buildx `--sbom` and `--provenance` or equivalent in-toto/SLSA tooling. -5. Validate the mapping ledger with `aptl aces-inventory validate`. -6. Run `aptl aces-inventory gaps` and file/link gap issues before encoding +5. Validate the mapping ledger with `aptl raes-inventory validate`. +6. Run `aptl raes-inventory gaps` and file/link gap issues before encoding unsupported facts through semantically wrong RAES fields. 7. Add or update correspondence checks so the future RAES encoding can be verified against realized evidence. diff --git a/docs/raes/inventory/scn010-expressivity-gap-analysis.md b/docs/raes/inventory/scn010-expressivity-gap-analysis.md index 7c64c447f..d9cfe4544 100644 --- a/docs/raes/inventory/scn010-expressivity-gap-analysis.md +++ b/docs/raes/inventory/scn010-expressivity-gap-analysis.md @@ -281,10 +281,10 @@ Filed as RAES expressivity issues, ordered by dependency. Each ships its ADR (ad **Open questions for the user.** (a) Confirm the rename-migration consult gate before executing the id-field/class renames. (b) Confirm whether `analytics_dashboard` ships as a platform_kind (with the mandated worked wazuh.dashboard defense) or is folded out to the four content-bearing kinds. (c) Confirm the deferred relational→`datastore_services` fold timeline (named coherence debt while `database_services` and `datastore_services` co-exist). Key source paths (all absolute): -- `/home/atomik/src/aces4/implementations/python/packages/aces_sdl/runtime_database_vocab.py` (closed `DatabaseObjectType`, REDIS/MARIADB enums) -- `/home/atomik/src/aces4/implementations/python/packages/aces_sdl/runtime_mounts.py` (`RuntimeControlInterface`, no id) -- `/home/atomik/src/aces4/implementations/python/packages/aces_sdl/runtime_application.py` (`RuntimeApplicationRoute`, no upstream) -- `/home/atomik/src/aces4/implementations/python/packages/aces_sdl/runtime_directory_identity.py` (relationship kinds, policy kind) -- `/home/atomik/src/aces4/implementations/python/packages/aces_sdl/runtime_security_monitoring.py` (listener roles, agent roster) -- `/home/atomik/src/aces4/implementations/python/packages/aces_sdl/_runtime_service_families.py` (registry shape) -- `/home/atomik/src/aces4/implementations/python/packages/aces_sdl/runtime_ssh_server.py:303`, `runtime_configuration.py:358-359`, `runtime_mail_service.py:111-128` (reconciliation targets) +- `implementations/python/packages/raes/runtime_database_vocab.py` (closed `DatabaseObjectType`, REDIS/MARIADB enums) +- `implementations/python/packages/raes/runtime_mounts.py` (`RuntimeControlInterface`, no id) +- `implementations/python/packages/raes/runtime_application.py` (`RuntimeApplicationRoute`, no upstream) +- `implementations/python/packages/raes/runtime_directory_identity.py` (relationship kinds, policy kind) +- `implementations/python/packages/raes/runtime_security_monitoring.py` (listener roles, agent roster) +- `implementations/python/packages/raes/_runtime_service_families.py` (registry shape) +- `implementations/python/packages/raes/runtime_ssh_server.py:303`, `runtime_configuration.py:358-359`, `runtime_mail_service.py:111-128` (reconciliation targets) diff --git a/docs/raes/inventory/webapp-preflight.md b/docs/raes/inventory/webapp-preflight.md index e9a447e00..298b664ca 100644 --- a/docs/raes/inventory/webapp-preflight.md +++ b/docs/raes/inventory/webapp-preflight.md @@ -35,8 +35,8 @@ methodology. - Inventory methodology and ledger validator: `docs/raes/inventory/asset-inventory-methodology.md`, - `src/aptl/core/aces_inventory.py`, and - `tests/test_aces_inventory_methodology.py`. + `src/aptl/core/raes_inventory.py`, and + `tests/test_raes_inventory_methodology.py`. - Existing evidence-bundle shape: `docs/raes/inventory/shuffle-backend/README.md`, `docs/raes/inventory/shuffle-backend/mapping-ledger.yaml`, and the @@ -45,7 +45,7 @@ methodology. `docker-compose.yml` service `webapp`, `containers/webapp/Dockerfile`, `containers/webapp/entrypoint.sh`, `containers/webapp/supervisord.conf`, `containers/webapp/requirements.txt`, and `containers/webapp/app/`. -- RAES SDL authority: sibling `../aces-sdl` parser/model documentation and +- RAES SDL authority: sibling `../raes-sdl` parser/model documentation and the closed RAES #354 runtime-surface gap. Do not parse `techvault.sdl.yaml` with `aptl.core.sdl`. - Secret and evidence safety: ADR-057, ADR-029, `aptl.utils.redaction`, the @@ -61,7 +61,7 @@ methodology. - Runtime evidence must cite the command/source that produced each claim: Docker inspect/history/network/volume/top, in-container runtime baseline, package manifests, filesystem hashes, and scanner output when available. -- `mapping-ledger.yaml` must validate through `aptl aces-inventory validate` +- `mapping-ledger.yaml` must validate through `aptl raes-inventory validate` with every captured fact assigned an RAES/APTL disposition and no temporary `needs_gap_triage` rows. - Tests must fail if the webapp bundle omits required evidence, if evidence diff --git a/examples/library/catalog.yaml b/examples/library/catalog.yaml index dc296bb3d..f82ca5019 100644 --- a/examples/library/catalog.yaml +++ b/examples/library/catalog.yaml @@ -1,4 +1,4 @@ -library: aces-example-pattern-library +library: raes-example-pattern-library version: 1 requirement_refs: [AUT-806] source_refs: diff --git a/examples/library/patterns/observable-study-conditions.yaml b/examples/library/patterns/observable-study-conditions.yaml index cd77ba449..f429f15bc 100644 --- a/examples/library/patterns/observable-study-conditions.yaml +++ b/examples/library/patterns/observable-study-conditions.yaml @@ -1,4 +1,4 @@ -pattern: aces-library-pattern +pattern: raes-library-pattern version: 1 id: observable-study-conditions surface: study diff --git a/examples/library/patterns/participant-behavior-contract-binding.yaml b/examples/library/patterns/participant-behavior-contract-binding.yaml index 3c10b2b5b..ab30f860a 100644 --- a/examples/library/patterns/participant-behavior-contract-binding.yaml +++ b/examples/library/patterns/participant-behavior-contract-binding.yaml @@ -1,4 +1,4 @@ -pattern: aces-library-pattern +pattern: raes-library-pattern version: 1 id: participant-behavior-contract-binding surface: participant_behavior diff --git a/examples/library/patterns/run-window-with-evidence.yaml b/examples/library/patterns/run-window-with-evidence.yaml index 8bc3fe748..bd4b9b0e0 100644 --- a/examples/library/patterns/run-window-with-evidence.yaml +++ b/examples/library/patterns/run-window-with-evidence.yaml @@ -1,4 +1,4 @@ -pattern: aces-library-pattern +pattern: raes-library-pattern version: 1 id: run-window-with-evidence surface: run diff --git a/examples/library/patterns/scenario-reference-integrity.yaml b/examples/library/patterns/scenario-reference-integrity.yaml index d28bb9ed9..6fae93132 100644 --- a/examples/library/patterns/scenario-reference-integrity.yaml +++ b/examples/library/patterns/scenario-reference-integrity.yaml @@ -1,4 +1,4 @@ -pattern: aces-library-pattern +pattern: raes-library-pattern version: 1 id: scenario-reference-integrity surface: scenario diff --git a/examples/library/patterns/task-as-objective-contract.yaml b/examples/library/patterns/task-as-objective-contract.yaml index cf644ebbb..6b6a6d04a 100644 --- a/examples/library/patterns/task-as-objective-contract.yaml +++ b/examples/library/patterns/task-as-objective-contract.yaml @@ -1,4 +1,4 @@ -pattern: aces-library-pattern +pattern: raes-library-pattern version: 1 id: task-as-objective-contract surface: task diff --git a/examples/library/patterns/workflow-explicit-control-graph.yaml b/examples/library/patterns/workflow-explicit-control-graph.yaml index 26df9d7d2..ba60128a5 100644 --- a/examples/library/patterns/workflow-explicit-control-graph.yaml +++ b/examples/library/patterns/workflow-explicit-control-graph.yaml @@ -1,4 +1,4 @@ -pattern: aces-library-pattern +pattern: raes-library-pattern version: 1 id: workflow-explicit-control-graph surface: workflow diff --git a/examples/library/templates/participant_behavior/action-contract-observation-boundary.yaml b/examples/library/templates/participant_behavior/action-contract-observation-boundary.yaml index edcbc199d..a07068dde 100644 --- a/examples/library/templates/participant_behavior/action-contract-observation-boundary.yaml +++ b/examples/library/templates/participant_behavior/action-contract-observation-boundary.yaml @@ -1,4 +1,4 @@ -template: aces-library-template +template: raes-library-template version: 1 id: action-contract-observation-boundary surface: participant_behavior diff --git a/examples/library/templates/run/timed-run-control.yaml b/examples/library/templates/run/timed-run-control.yaml index de312c903..34243ac32 100644 --- a/examples/library/templates/run/timed-run-control.yaml +++ b/examples/library/templates/run/timed-run-control.yaml @@ -1,4 +1,4 @@ -template: aces-library-template +template: raes-library-template version: 1 id: timed-run-control surface: run @@ -19,7 +19,7 @@ body: conditions: telemetry-ready: proposition: telemetry-ready - command: test -f /var/lib/aces/telemetry-ready + command: test -f /var/lib/raes/telemetry-ready interval: 30 injects: start-run: @@ -54,7 +54,7 @@ body: predicate: kind: boolean property: telemetry-ready - semantic_ref: urn:aces:observable:telemetry-ready + semantic_ref: urn:raes:observable:telemetry-ready operator: equals expected: true evidence_requirements: [objective-truth-evidence] diff --git a/examples/library/templates/scenario/minimal-validated-scenario.yaml b/examples/library/templates/scenario/minimal-validated-scenario.yaml index 81196c547..61a21bbe5 100644 --- a/examples/library/templates/scenario/minimal-validated-scenario.yaml +++ b/examples/library/templates/scenario/minimal-validated-scenario.yaml @@ -1,4 +1,4 @@ -template: aces-library-template +template: raes-library-template version: 1 id: minimal-validated-scenario surface: scenario @@ -36,7 +36,7 @@ body: predicate: kind: boolean property: app-healthy - semantic_ref: urn:aces:observable:app-healthy + semantic_ref: urn:raes:observable:app-healthy operator: equals expected: true evidence_requirements: [objective-truth-evidence] diff --git a/examples/library/templates/study/observational-study-protocol.yaml b/examples/library/templates/study/observational-study-protocol.yaml index 4a7c75547..62c2ec638 100644 --- a/examples/library/templates/study/observational-study-protocol.yaml +++ b/examples/library/templates/study/observational-study-protocol.yaml @@ -1,4 +1,4 @@ -template: aces-library-template +template: raes-library-template version: 1 id: observational-study-protocol surface: study @@ -19,7 +19,7 @@ body: conditions: study-task-success: proposition: study-task-success - command: test -f /var/lib/aces/study-task-success + command: test -f /var/lib/raes/study-task-success interval: 30 entities: participant-team: @@ -36,7 +36,7 @@ body: predicate: kind: boolean property: study-task-success - semantic_ref: urn:aces:observable:study-task-success + semantic_ref: urn:raes:observable:study-task-success operator: equals expected: true evidence_requirements: [objective-truth-evidence] diff --git a/examples/library/templates/task/single-objective-task.yaml b/examples/library/templates/task/single-objective-task.yaml index 0655882eb..d1e31b1e5 100644 --- a/examples/library/templates/task/single-objective-task.yaml +++ b/examples/library/templates/task/single-objective-task.yaml @@ -1,4 +1,4 @@ -template: aces-library-template +template: raes-library-template version: 1 id: single-objective-task surface: task @@ -19,7 +19,7 @@ body: conditions: task-complete: proposition: task-complete - command: test -f /tmp/aces-task-complete + command: test -f /tmp/raes-task-complete interval: 15 entities: participant: @@ -36,7 +36,7 @@ body: predicate: kind: boolean property: task-complete - semantic_ref: urn:aces:observable:task-complete + semantic_ref: urn:raes:observable:task-complete operator: equals expected: true evidence_requirements: [objective-truth-evidence] diff --git a/examples/library/templates/workflow/parallel-objective-workflow.yaml b/examples/library/templates/workflow/parallel-objective-workflow.yaml index 5432b7cbe..e090a3d34 100644 --- a/examples/library/templates/workflow/parallel-objective-workflow.yaml +++ b/examples/library/templates/workflow/parallel-objective-workflow.yaml @@ -1,4 +1,4 @@ -template: aces-library-template +template: raes-library-template version: 1 id: parallel-objective-workflow surface: workflow @@ -19,7 +19,7 @@ body: conditions: patch-applied: proposition: patch-applied - command: test -f /var/lib/aces/patch-applied + command: test -f /var/lib/raes/patch-applied interval: 30 service-verified: proposition: service-verified @@ -40,7 +40,7 @@ body: predicate: kind: boolean property: patch-applied - semantic_ref: urn:aces:observable:patch-applied + semantic_ref: urn:raes:observable:patch-applied operator: equals expected: true evidence_requirements: [objective-truth-evidence] @@ -52,7 +52,7 @@ body: predicate: kind: boolean property: service-verified - semantic_ref: urn:aces:observable:service-verified + semantic_ref: urn:raes:observable:service-verified operator: equals expected: true evidence_requirements: [objective-truth-evidence] diff --git a/examples/scenarios/enterprise-participant-evidence-loop.README.md b/examples/scenarios/enterprise-participant-evidence-loop.README.md index 4b24471b6..571cd797f 100644 --- a/examples/scenarios/enterprise-participant-evidence-loop.README.md +++ b/examples/scenarios/enterprise-participant-evidence-loop.README.md @@ -89,9 +89,9 @@ a new backend manifest shape, or APTL-private keys inside the scenario body. ## Libvirt Scenario Evidence Artifact (#615) -`raes_operations.libvirt_evidence_run.run_libvirt_evidence_run` (CLI: `aces +`raes_operations.libvirt_evidence_run.run_libvirt_evidence_run` (CLI: `raes libvirt evidence validate`) produces a stable, validated evaluator-evidence -run artifact for this scenario — `aces.libvirt.scenario-evidence-run/v1`, written to +run artifact for this scenario — `raes.libvirt.scenario-evidence-run/v1`, written to `runs//scenario-evidence/libvirt-scenario-evidence-run.json`. It composes the existing RAES surfaces (libvirt deterministic participant runtime #614, native substrate realization #601, backend manifest/capability contracts, and the diff --git a/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml b/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml index 8211b506b..e3309db05 100644 --- a/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml +++ b/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml @@ -572,7 +572,7 @@ propositions: predicate: kind: boolean property: boundary-checks-recorded - semantic_ref: urn:aces:observable:boundary-checks-recorded + semantic_ref: urn:raes:observable:boundary-checks-recorded operator: equals expected: true quantifier: all @@ -586,7 +586,7 @@ propositions: predicate: kind: boolean property: participant-observation-recorded - semantic_ref: urn:aces:observable:participant-observation-recorded + semantic_ref: urn:raes:observable:participant-observation-recorded operator: equals expected: true quantifier: all @@ -600,7 +600,7 @@ propositions: predicate: kind: boolean property: policy-decision-recorded - semantic_ref: urn:aces:observable:policy-decision-recorded + semantic_ref: urn:raes:observable:policy-decision-recorded operator: equals expected: true quantifier: all @@ -614,7 +614,7 @@ propositions: predicate: kind: boolean property: wazuh-evidence-recorded - semantic_ref: urn:aces:observable:wazuh-evidence-recorded + semantic_ref: urn:raes:observable:wazuh-evidence-recorded operator: equals expected: true quantifier: all diff --git a/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml b/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml index ed3cfe99f..f27fefd95 100644 --- a/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml +++ b/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml @@ -713,7 +713,7 @@ propositions: predicate: kind: boolean property: backup-catalog-intact - semantic_ref: urn:aces:observable:backup-catalog-intact + semantic_ref: urn:raes:observable:backup-catalog-intact operator: equals expected: true quantifier: all @@ -727,7 +727,7 @@ propositions: predicate: kind: boolean property: clinical-disruption-observed - semantic_ref: urn:aces:observable:clinical-disruption-observed + semantic_ref: urn:raes:observable:clinical-disruption-observed operator: equals expected: true quantifier: all @@ -741,7 +741,7 @@ propositions: predicate: kind: boolean property: ehr-api-healthy - semantic_ref: urn:aces:observable:ehr-api-healthy + semantic_ref: urn:raes:observable:ehr-api-healthy operator: equals expected: true quantifier: all @@ -755,7 +755,7 @@ propositions: predicate: kind: boolean property: pacs-archive-healthy - semantic_ref: urn:aces:observable:pacs-archive-healthy + semantic_ref: urn:raes:observable:pacs-archive-healthy operator: equals expected: true quantifier: all @@ -769,7 +769,7 @@ propositions: predicate: kind: boolean property: phi-staging-observed - semantic_ref: urn:aces:observable:phi-staging-observed + semantic_ref: urn:raes:observable:phi-staging-observed operator: equals expected: true quantifier: all @@ -783,7 +783,7 @@ propositions: predicate: kind: boolean property: phish-mail-delivered - semantic_ref: urn:aces:observable:phish-mail-delivered + semantic_ref: urn:raes:observable:phish-mail-delivered operator: equals expected: true quantifier: all @@ -797,7 +797,7 @@ propositions: predicate: kind: boolean property: radiology-workflow-delayed - semantic_ref: urn:aces:observable:radiology-workflow-delayed + semantic_ref: urn:raes:observable:radiology-workflow-delayed operator: equals expected: true quantifier: all @@ -811,7 +811,7 @@ propositions: predicate: kind: boolean property: soc-pipeline-up - semantic_ref: urn:aces:observable:soc-pipeline-up + semantic_ref: urn:raes:observable:soc-pipeline-up operator: equals expected: true quantifier: all @@ -825,7 +825,7 @@ propositions: predicate: kind: boolean property: triage-case-submitted - semantic_ref: urn:aces:observable:triage-case-submitted + semantic_ref: urn:raes:observable:triage-case-submitted operator: equals expected: true quantifier: all @@ -839,7 +839,7 @@ propositions: predicate: kind: boolean property: vendor-session-established - semantic_ref: urn:aces:observable:vendor-session-established + semantic_ref: urn:raes:observable:vendor-session-established operator: equals expected: true quantifier: all diff --git a/examples/scenarios/initial-service-state.sdl.yaml b/examples/scenarios/initial-service-state.sdl.yaml index 42b23e1b2..6bfb8a123 100644 --- a/examples/scenarios/initial-service-state.sdl.yaml +++ b/examples/scenarios/initial-service-state.sdl.yaml @@ -40,7 +40,7 @@ propositions: predicate: kind: boolean property: service-content-visible - semantic_ref: urn:aces:observable:service-content-visible + semantic_ref: urn:raes:observable:service-content-visible expected: true evidence_requirements: [company-mail-readback] diff --git a/examples/scenarios/port-authority-surge-response.sdl.yaml b/examples/scenarios/port-authority-surge-response.sdl.yaml index f73a81d38..19041f517 100644 --- a/examples/scenarios/port-authority-surge-response.sdl.yaml +++ b/examples/scenarios/port-authority-surge-response.sdl.yaml @@ -570,7 +570,7 @@ propositions: predicate: kind: boolean property: crane-safe-mode - semantic_ref: urn:aces:observable:crane-safe-mode + semantic_ref: urn:raes:observable:crane-safe-mode operator: equals expected: true quantifier: all @@ -584,7 +584,7 @@ propositions: predicate: kind: boolean property: customs-link-healthy - semantic_ref: urn:aces:observable:customs-link-healthy + semantic_ref: urn:raes:observable:customs-link-healthy operator: equals expected: true quantifier: all @@ -598,7 +598,7 @@ propositions: predicate: kind: boolean property: historian-replication-ok - semantic_ref: urn:aces:observable:historian-replication-ok + semantic_ref: urn:raes:observable:historian-replication-ok operator: equals expected: true quantifier: all @@ -612,7 +612,7 @@ propositions: predicate: kind: boolean property: manifest-integrity-ok - semantic_ref: urn:aces:observable:manifest-integrity-ok + semantic_ref: urn:raes:observable:manifest-integrity-ok operator: equals expected: true quantifier: all @@ -626,7 +626,7 @@ propositions: predicate: kind: boolean property: tampered-manifest-observed - semantic_ref: urn:aces:observable:tampered-manifest-observed + semantic_ref: urn:raes:observable:tampered-manifest-observed operator: equals expected: true quantifier: all @@ -640,7 +640,7 @@ propositions: predicate: kind: boolean property: yard-telemetry-flowing - semantic_ref: urn:aces:observable:yard-telemetry-flowing + semantic_ref: urn:raes:observable:yard-telemetry-flowing operator: equals expected: true quantifier: all diff --git a/examples/scenarios/satcom-release-poisoning.sdl.yaml b/examples/scenarios/satcom-release-poisoning.sdl.yaml index c92bebef3..ec0811246 100644 --- a/examples/scenarios/satcom-release-poisoning.sdl.yaml +++ b/examples/scenarios/satcom-release-poisoning.sdl.yaml @@ -626,7 +626,7 @@ propositions: predicate: kind: boolean property: edge-east-poisoned - semantic_ref: urn:aces:observable:edge-east-poisoned + semantic_ref: urn:raes:observable:edge-east-poisoned operator: equals expected: true quantifier: all @@ -640,7 +640,7 @@ propositions: predicate: kind: boolean property: release-signature-valid - semantic_ref: urn:aces:observable:release-signature-valid + semantic_ref: urn:raes:observable:release-signature-valid operator: equals expected: true quantifier: all @@ -654,7 +654,7 @@ propositions: predicate: kind: boolean property: rogue-release-promoted - semantic_ref: urn:aces:observable:rogue-release-promoted + semantic_ref: urn:raes:observable:rogue-release-promoted operator: equals expected: true quantifier: all @@ -668,7 +668,7 @@ propositions: predicate: kind: boolean property: rollback-package-ready - semantic_ref: urn:aces:observable:rollback-package-ready + semantic_ref: urn:raes:observable:rollback-package-ready operator: equals expected: true quantifier: all @@ -682,7 +682,7 @@ propositions: predicate: kind: boolean property: telemetry-flowing - semantic_ref: urn:aces:observable:telemetry-flowing + semantic_ref: urn:raes:observable:telemetry-flowing operator: equals expected: true quantifier: all @@ -696,7 +696,7 @@ propositions: predicate: kind: boolean property: tenant-isolation-healthy - semantic_ref: urn:aces:observable:tenant-isolation-healthy + semantic_ref: urn:raes:observable:tenant-isolation-healthy operator: equals expected: true quantifier: all diff --git a/examples/scenarios/techvault-guest-certified.sdl.yaml b/examples/scenarios/techvault-guest-certified.sdl.yaml index 3fa8cbb38..918ea7a1e 100644 --- a/examples/scenarios/techvault-guest-certified.sdl.yaml +++ b/examples/scenarios/techvault-guest-certified.sdl.yaml @@ -19,7 +19,7 @@ content: guest-marker: type: file target: guest-vm - path: /etc/aces/marker + path: /etc/raes/marker text: "guest-certified realization marker\n" sensitive: false infrastructure: diff --git a/implementations/python/packages/raes/_base.py b/implementations/python/packages/raes/_base.py index dfc0a342a..0ab643965 100644 --- a/implementations/python/packages/raes/_base.py +++ b/implementations/python/packages/raes/_base.py @@ -34,7 +34,7 @@ class SDLModel(BaseModel): VARIABLE_TOKEN_PATTERN = r"\$\{(" + _VARIABLE_TOKEN_NAME_PATTERN + r")\}" VARIABLE_TOKEN_RE = re.compile(VARIABLE_TOKEN_PATTERN) _VARIABLE_REF_RE = re.compile(r"^" + VARIABLE_TOKEN_PATTERN + r"$") -VARIABLE_REFERENCE_SCHEMA_MARKER = "x-aces-variable-reference" +VARIABLE_REFERENCE_SCHEMA_MARKER = "x-raes-variable-reference" def is_variable_ref(v: Any) -> bool: diff --git a/implementations/python/packages/raes/_realization_envelope_engine.py b/implementations/python/packages/raes/_realization_envelope_engine.py index 342228781..059292c21 100644 --- a/implementations/python/packages/raes/_realization_envelope_engine.py +++ b/implementations/python/packages/raes/_realization_envelope_engine.py @@ -148,7 +148,7 @@ def _model_present_children(model: BaseModel) -> set[str]: present: set[str] = set() for name, info in type(model).model_fields.items(): schema_extra = info.json_schema_extra - if isinstance(schema_extra, Mapping) and schema_extra.get("x-aces-realization-dimension") is False: + if isinstance(schema_extra, Mapping) and schema_extra.get("x-raes-realization-dimension") is False: continue child = getattr(model, name) if not _is_nonempty(child): diff --git a/implementations/python/packages/raes/_source_profile.py b/implementations/python/packages/raes/_source_profile.py index c7f1e0117..1f88f166d 100644 --- a/implementations/python/packages/raes/_source_profile.py +++ b/implementations/python/packages/raes/_source_profile.py @@ -10,7 +10,7 @@ from yaml.nodes import ScalarNode SDL_SOURCE_FORMAT = "sdl-yaml/v1" -SDL_CANONICAL_PROFILE = "aces-sdl-semantic/v1" +SDL_CANONICAL_PROFILE = "raes-sdl-semantic/v1" class SDLMigrationPolicy(str, Enum): diff --git a/implementations/python/packages/raes/canonical.py b/implementations/python/packages/raes/canonical.py index bbd6a8266..afee72c7f 100644 --- a/implementations/python/packages/raes/canonical.py +++ b/implementations/python/packages/raes/canonical.py @@ -14,7 +14,7 @@ from ._source_profile import SDL_CANONICAL_PROFILE from .scenario import ExpandedScenario, InstantiatedScenario, Scenario -INSTANTIATED_SNAPSHOT_PROFILE = "aces-sdl-instantiated-snapshot/v1" +INSTANTIATED_SNAPSHOT_PROFILE = "raes-sdl-instantiated-snapshot/v1" @dataclass(frozen=True) @@ -73,10 +73,10 @@ class InstantiatedScenarioSnapshot(SDLModel): title="SDL Instantiated Scenario Snapshot v1", extra="forbid", frozen=True, - json_schema_extra={"x-aces-document-phase": "canonical-instantiated-snapshot"}, + json_schema_extra={"x-raes-document-phase": "canonical-instantiated-snapshot"}, ) - profile: Literal["aces-sdl-instantiated-snapshot/v1"] + profile: Literal["raes-sdl-instantiated-snapshot/v1"] scenario: InstantiatedScenario diff --git a/implementations/python/packages/raes/module_registry.py b/implementations/python/packages/raes/module_registry.py index c3e53e0ca..df245fbdd 100644 --- a/implementations/python/packages/raes/module_registry.py +++ b/implementations/python/packages/raes/module_registry.py @@ -34,14 +34,14 @@ if TYPE_CHECKING: from .parser import SDLSourceDocument -LOCKFILE_NAME = "aces.lock.json" -TRUST_POLICY_NAME = "aces-trust.yaml" +LOCKFILE_NAME = "raes.lock.json" +TRUST_POLICY_NAME = "raes-trust.yaml" OCI_LAYOUT_MEDIA_TYPE = "application/vnd.oci.image.manifest.v1+json" -OCI_CONFIG_MEDIA_TYPE = "application/vnd.aces.module.config.v1+json" -OCI_BUNDLE_MEDIA_TYPE = "application/vnd.aces.module.bundle.v1+tar+gzip" -LOCKFILE_SCHEMA_VERSION = "aces-lock/v1" -TRUST_POLICY_SCHEMA_VERSION = "aces-trust/v1" -OCI_LAYOUT_SCHEMA_VERSION = "aces-module-oci/v1" +OCI_CONFIG_MEDIA_TYPE = "application/vnd.raes.module.config.v1+json" +OCI_BUNDLE_MEDIA_TYPE = "application/vnd.raes.module.bundle.v1+tar+gzip" +LOCKFILE_SCHEMA_VERSION = "raes-lock/v1" +TRUST_POLICY_SCHEMA_VERSION = "raes-trust/v1" +OCI_LAYOUT_SCHEMA_VERSION = "raes-module-oci/v1" def _sha256_digest(data: bytes) -> str: @@ -352,7 +352,7 @@ def _select_tag(tags: list[str], requested_version: str) -> str: def _oci_cache_dir(base_dir: Path) -> Path: - return base_dir / ".aces" / "module-cache" + return base_dir / ".raes" / "module-cache" def _validate_tar_member_shape( @@ -852,7 +852,7 @@ def publish_module_to_oci_layout( ], "annotations": { "org.opencontainers.image.ref.name": descriptor.version, - "io.aces.module.id": descriptor.id, + "io.raes.module.id": descriptor.id, }, } manifest_bytes = json.dumps(manifest_payload, sort_keys=True, separators=(",", ":")).encode("utf-8") @@ -875,7 +875,7 @@ def publish_module_to_oci_layout( "size": len(manifest_bytes), "annotations": { "org.opencontainers.image.ref.name": descriptor.version, - "io.aces.module.id": descriptor.id, + "io.raes.module.id": descriptor.id, }, } ], diff --git a/implementations/python/packages/raes/observability_plane_semantics.py b/implementations/python/packages/raes/observability_plane_semantics.py index dd3679957..c1add0abf 100644 --- a/implementations/python/packages/raes/observability_plane_semantics.py +++ b/implementations/python/packages/raes/observability_plane_semantics.py @@ -11,7 +11,7 @@ infers nothing from arbitrary text. The plane separation each carrier enforces lives in the existing experiment-core, participant-runtime, and apparatus contracts; this module is the single source of plane ownership and the source of -the portable ``x-aces-plane`` annotation published on the claim-bearing +the portable ``x-raes-plane`` annotation published on the claim-bearing contracts. """ @@ -51,7 +51,7 @@ class ObservabilityEvidencePlane(str, Enum): "evidence_requirements": ObservabilityEvidencePlane.AUTHORED_EVIDENCE_REQUIREMENT, } -# Contracts whose ``x-aces-plane`` annotation is published as portable +# Contracts whose ``x-raes-plane`` annotation is published as portable # traceability (the three experiment-core carriers that map 1:1 to a plane). PLANE_ANNOTATED_CONTRACT_IDS: tuple[str, ...] = ( "experiment-capture-spec-v1", diff --git a/implementations/python/packages/raes/phase_contracts.py b/implementations/python/packages/raes/phase_contracts.py index 22afb004a..f7d40ac94 100644 --- a/implementations/python/packages/raes/phase_contracts.py +++ b/implementations/python/packages/raes/phase_contracts.py @@ -28,7 +28,7 @@ class FrozenPhaseModel(SDLModel): class SemanticDigest(FrozenPhaseModel): """Profile-labelled digest of one expanded authoring scenario.""" - profile: Literal["aces-sdl-semantic/v1"] + profile: Literal["raes-sdl-semantic/v1"] algorithm: Literal["sha256"] value: Annotated[str, Field(pattern=_DIGEST_PATTERN)] diff --git a/implementations/python/packages/raes/scenario.py b/implementations/python/packages/raes/scenario.py index 7f4bf3c96..d3cd89a14 100644 --- a/implementations/python/packages/raes/scenario.py +++ b/implementations/python/packages/raes/scenario.py @@ -352,9 +352,9 @@ class Scenario(ScenarioContent): model_config = ConfigDict( title="SDL Normalized Authoring Object v1", json_schema_extra={ - "x-aces-document-phase": "normalized-authoring-object", - "x-aces-source-profile": "sdl-yaml/v1", - "x-aces-validates-raw-source": False, + "x-raes-document-phase": "normalized-authoring-object", + "x-raes-source-profile": "sdl-yaml/v1", + "x-raes-validates-raw-source": False, }, ) @@ -362,7 +362,7 @@ class Scenario(ScenarioContent): imports: list[ImportDecl] = Field(default_factory=list) realization: RealizationDesignation | None = Field( default=None, - json_schema_extra={"x-aces-realization-dimension": False}, + json_schema_extra={"x-raes-realization-dimension": False}, ) variables: VariableDefinitions = Field( default_factory=dict, @@ -386,7 +386,7 @@ class ExpandedScenario(ScenarioContent): model_config = ConfigDict( title="SDL Expanded Authoring Object v1", - json_schema_extra={"x-aces-document-phase": "expanded-authoring-object"}, + json_schema_extra={"x-raes-document-phase": "expanded-authoring-object"}, ) variables: dict[str, Variable] = Field( @@ -431,13 +431,13 @@ class InstantiatedScenario(ScenarioContent): model_config = ConfigDict( title="SDL Instantiated Scenario v1", json_schema_extra={ - "x-aces-document-phase": "instantiated-scenario", - "x-aces-authored-identity-profile": "aces-sdl-semantic/v1", + "x-raes-document-phase": "instantiated-scenario", + "x-raes-authored-identity-profile": "raes-sdl-semantic/v1", }, ) instantiation_provenance: InstantiationProvenance = Field( - json_schema_extra={"x-aces-realization-dimension": False}, + json_schema_extra={"x-raes-realization-dimension": False}, ) @property diff --git a/implementations/python/packages/raes_backend_libvirt/cloudinit.py b/implementations/python/packages/raes_backend_libvirt/cloudinit.py index cd1d6d011..090ea7cdc 100644 --- a/implementations/python/packages/raes_backend_libvirt/cloudinit.py +++ b/implementations/python/packages/raes_backend_libvirt/cloudinit.py @@ -30,7 +30,7 @@ def safe_path_component(value: str, *, fallback: str) -> str: """Reduce a plan-controlled value to one safe path-filename component. Descriptor filenames interpolate plan-controlled identifiers (account name, - feature/content name). A value such as ``../../cron.d/aces`` must never let an + feature/content name). A value such as ``../../cron.d/raes`` must never let an interpolated ``write_files`` path escape its intended directory, so this maps anything outside ``[A-Za-z0-9._-]`` to ``_`` and strips leading/trailing ``.``/``_``/``-`` (neutralizing ``.``/``..``). The result is always a single, @@ -133,7 +133,7 @@ def render_meta_data(spec: CloudInitSpec) -> str: """ digest = hashlib.sha256(render_user_data(spec).encode("utf-8")).hexdigest()[:16] - prefix = spec.hostname or "aces" + prefix = spec.hostname or "raes" meta: dict[str, object] = {"instance-id": f"{prefix}-{digest}"} if spec.hostname: meta["local-hostname"] = spec.hostname diff --git a/implementations/python/packages/raes_backend_libvirt/dialects.py b/implementations/python/packages/raes_backend_libvirt/dialects.py index 8561d7ae2..cb170d56e 100644 --- a/implementations/python/packages/raes_backend_libvirt/dialects.py +++ b/implementations/python/packages/raes_backend_libvirt/dialects.py @@ -46,12 +46,12 @@ class GuestDialect: def enable_feature(self, package: str) -> GuestEmit: safe = safe_path_component(package, fallback="feature") body = {"os_family": self.os_family, "service": package} - return GuestEmit(write_files=(_descriptor(f"/etc/aces/features/{safe}.json", body),)) + return GuestEmit(write_files=(_descriptor(f"/etc/raes/features/{safe}.json", body),)) def mail_alias(self, username: str, mail: str) -> GuestEmit: safe = safe_path_component(username, fallback="user") body = {"os_family": self.os_family, "user": username, "mail": mail} - return GuestEmit(write_files=(_descriptor(f"/etc/aces/mail/{safe}.json", body),)) + return GuestEmit(write_files=(_descriptor(f"/etc/raes/mail/{safe}.json", body),)) class LinuxDialect(GuestDialect): @@ -63,7 +63,7 @@ def enable_feature(self, package: str) -> GuestEmit: def mail_alias(self, username: str, mail: str) -> GuestEmit: safe = safe_path_component(username, fallback="user") return GuestEmit( - write_files=(CloudInitFile(path=f"/etc/aliases.d/aces-{safe}", content=f"{username}: {mail}\n"),), + write_files=(CloudInitFile(path=f"/etc/aliases.d/raes-{safe}", content=f"{username}: {mail}\n"),), runcmd=(("newaliases",),), ) @@ -80,7 +80,7 @@ def enable_feature(self, package: str) -> GuestEmit: def mail_alias(self, username: str, mail: str) -> GuestEmit: safe = safe_path_component(username, fallback="user") return GuestEmit( - write_files=(CloudInitFile(path=f"/etc/aces/mail/{safe}", content=f"{username}: {mail}\n"),), + write_files=(CloudInitFile(path=f"/etc/raes/mail/{safe}", content=f"{username}: {mail}\n"),), runcmd=(("newaliases",),), ) diff --git a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py index 849922bbd..f5062a7b9 100644 --- a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py +++ b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py @@ -32,12 +32,12 @@ _CODE_UNAVAILABLE = "libvirt-backend.driver.unavailable" _CODE_OWNERSHIP_CONFLICT = "libvirt-backend.driver.ownership-conflict" _DEFAULT_CONNECTION_URI = "qemu:///system" -_WORKSPACE_PREFIX = "aces-libvirt-" +_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("ace50000-0000-5000-8000-000000000001") +_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 @@ -141,7 +141,7 @@ def __init__( connection: object | None = None, connection_uri: str = _DEFAULT_CONNECTION_URI, connector: Connector | None = None, - name_prefix: str = "aces", + name_prefix: str = "raes", workspace: str | Path | None = None, seed_builder: SeedBuilder | None = None, ) -> None: @@ -152,7 +152,7 @@ def __init__( self._connection = connection self._connection_uri = connection_uri self._connector = connector or _default_connector - self._name_prefix = _safe_name(name_prefix, fallback="aces", prefix="") + self._name_prefix = _safe_name(name_prefix, fallback="raes", prefix="") self._workspace = Path(workspace) if workspace is not None else None self._seed_builder = seed_builder if seed_builder is not None else GenisoimageSeedBuilder() self._names: dict[str, str] = {} diff --git a/implementations/python/packages/raes_backend_libvirt/guest_appliance.py b/implementations/python/packages/raes_backend_libvirt/guest_appliance.py index 2e3ffbb61..30951eee9 100644 --- a/implementations/python/packages/raes_backend_libvirt/guest_appliance.py +++ b/implementations/python/packages/raes_backend_libvirt/guest_appliance.py @@ -45,7 +45,7 @@ class GuestObservingInitramfsBuilder: busybox_path: Path = Path("/usr/bin/busybox") def build(self, *, domain: Mapping[str, object], target: Path) -> Path: - with tempfile.TemporaryDirectory(prefix="aces-guest-initramfs-") as tmp: + with tempfile.TemporaryDirectory(prefix="raes-guest-initramfs-") as tmp: root = Path(tmp) _write_guest_root(root, self.busybox_path, domain) target.parent.mkdir(parents=True, exist_ok=True) @@ -57,7 +57,7 @@ def build(self, *, domain: Mapping[str, object], target: Path) -> Path: def _write_guest_root(root: Path, busybox_path: Path, domain: Mapping[str, object]) -> None: bin_dir = root / "bin" etc_dir = root / "etc" - guest_dir = etc_dir / "aces" / "guest" + guest_dir = etc_dir / "raes" / "guest" files_dir = guest_dir / "files" for directory in ( bin_dir, @@ -120,7 +120,7 @@ def _init_script(domain: Mapping[str, object]) -> str: "mount -t proc proc /proc", "mount -t sysfs sysfs /sys", "mount -t devtmpfs devtmpfs /dev 2>/dev/null || mdev -s", - f"hostname {_shell_quote(str(domain.get('name', 'aces-node')))}", + f"hostname {_shell_quote(str(domain.get('name', 'raes-node')))}", "ip link set lo up", "for iface_path in /sys/class/net/*; do", " iface=${iface_path##*/}", @@ -142,7 +142,7 @@ def _init_script(domain: Mapping[str, object]) -> str: lines.extend([" esac", "done"]) lines.extend(_REALIZE_SNIPPET) lines.append("sleep 1") - lines.append("challenge=$(cat /proc/cmdline | tr ' ' '\\n' | sed -n 's/^aces.challenge=//p')") + lines.append("challenge=$(cat /proc/cmdline | tr ' ' '\\n' | sed -n 's/^raes.challenge=//p')") lines.extend(_REPORT_SNIPPET) lines.append("while true; do sleep 3600; done") lines.append("") @@ -150,14 +150,14 @@ def _init_script(domain: Mapping[str, object]) -> str: _REALIZE_SNIPPET = [ - "if [ -f /etc/aces/guest/accounts ]; then", + "if [ -f /etc/raes/guest/accounts ]; then", "while IFS='|' read name groups shell home disabled; do", _SKIP_IF_NO_NAME, ' [ -n "$home" ] || home=/home/$name', ' [ -n "$shell" ] || shell=/bin/sh', ' mkdir -p "$home"', " uid=$(awk -F: 'BEGIN{m=1000}$3>=m{m=$3+1}END{print m}' /etc/passwd)", - ' echo "$name:x:$uid:$uid:aces:$home:$shell" >> /etc/passwd', + ' echo "$name:x:$uid:$uid:raes:$home:$shell" >> /etc/passwd', ' echo "$name:x:$uid:" >> /etc/group', ' if [ "$disabled" = 1 ]; then echo "$name:!:19000:0:99999:7:::" >> /etc/shadow;', ' else echo "$name:*:19000:0:99999:7:::" >> /etc/shadow; fi', @@ -174,29 +174,29 @@ def _init_script(domain: Mapping[str, object]) -> str: " IFS=,", " done", " IFS=$oldifs", - "done < /etc/aces/guest/accounts", + "done < /etc/raes/guest/accounts", "fi", - "if [ -f /etc/aces/guest/content ]; then", + "if [ -f /etc/raes/guest/content ]; then", "while IFS='|' read path mode idx; do", ' [ -n "$path" ] || continue', ' mkdir -p "$(dirname "$path")"', - ' cp "/etc/aces/guest/files/$idx" "$path"', + ' cp "/etc/raes/guest/files/$idx" "$path"', ' chmod "$mode" "$path"', - "done < /etc/aces/guest/content", + "done < /etc/raes/guest/content", "fi", - "if [ -f /etc/aces/guest/services ]; then", + "if [ -f /etc/raes/guest/services ]; then", "while IFS='|' read name port; do", _SKIP_IF_NO_NAME, - ' ( while true; do echo aces-guest-service | nc -l -p "$port" >/dev/null 2>&1 || sleep 1; done ) &', - " echo $! > /run/aces-svc-$name.pid", - "done < /etc/aces/guest/services", + ' ( while true; do echo raes-guest-service | nc -l -p "$port" >/dev/null 2>&1 || sleep 1; done ) &', + " echo $! > /run/raes-svc-$name.pid", + "done < /etc/raes/guest/services", "fi", ] _REPORT_SNIPPET = [ "FC=/dev/ttyS1", "{", - "echo 'ACES-GUEST-FACTS v1'", + "echo 'RAES-GUEST-FACTS v1'", 'echo "challenge $challenge"', 'echo "architecture $(uname -m)"', 'echo "vcpus $(nproc)"', @@ -208,16 +208,16 @@ def _init_script(domain: Mapping[str, object]) -> str: ' up=0; [ "$(cat "$iface_path/operstate" 2>/dev/null)" = up ] && up=1', ' echo "iface $mac ${ip4:-none} $up"', "done", - "if [ -f /etc/aces/guest/content ]; then", + "if [ -f /etc/raes/guest/content ]; then", "while IFS='|' read path mode idx; do", ' [ -n "$path" ] || continue', ' [ -f "$path" ] || continue', " d=$(sha256sum \"$path\" | cut -d' ' -f1)", " m=$(stat -c '%a' \"$path\" 2>/dev/null)", ' echo "content $path $d $m"', - "done < /etc/aces/guest/content", + "done < /etc/raes/guest/content", "fi", - "if [ -f /etc/aces/guest/accounts ]; then", + "if [ -f /etc/raes/guest/accounts ]; then", "while IFS='|' read name groups shell home disabled; do", _SKIP_IF_NO_NAME, ' entry=$(grep "^$name:" /etc/passwd) || continue', @@ -229,15 +229,15 @@ def _init_script(domain: Mapping[str, object]) -> str: ' spw=$(grep "^$name:" /etc/shadow | cut -d: -f2)', " dis=0; case \"$spw\" in '!'*|'*'*) dis=1;; esac", ' echo "account $name $uid $h $sh $dis $grps"', - "done < /etc/aces/guest/accounts", + "done < /etc/raes/guest/accounts", "fi", - "if [ -f /etc/aces/guest/services ]; then", + "if [ -f /etc/raes/guest/services ]; then", "while IFS='|' read name port; do", _SKIP_IF_NO_NAME, ' lis=0; netstat -ln 2>/dev/null | grep -q ":$port " && lis=1', - ' pid=0; [ -f "/run/aces-svc-$name.pid" ] && kill -0 "$(cat /run/aces-svc-$name.pid)" 2>/dev/null && pid=1', + ' pid=0; [ -f "/run/raes-svc-$name.pid" ] && kill -0 "$(cat /run/raes-svc-$name.pid)" 2>/dev/null && pid=1', ' echo "service $name $port $lis $pid"', - "done < /etc/aces/guest/services", + "done < /etc/raes/guest/services", "fi", "echo 'init complete'", '} > "$FC" 2>/dev/null', diff --git a/implementations/python/packages/raes_backend_libvirt/guest_transport.py b/implementations/python/packages/raes_backend_libvirt/guest_transport.py index 761e96489..0ef3c4957 100644 --- a/implementations/python/packages/raes_backend_libvirt/guest_transport.py +++ b/implementations/python/packages/raes_backend_libvirt/guest_transport.py @@ -20,7 +20,7 @@ _Handler = Callable[[dict[str, object], list[str]], None] -FACT_HEADER = "ACES-GUEST-FACTS v1" +FACT_HEADER = "RAES-GUEST-FACTS v1" INIT_COMPLETE_MARKER = "init complete" # Bounds keep a hostile or malfunctioning guest from flooding host memory or the diff --git a/implementations/python/packages/raes_backend_libvirt/manifest.py b/implementations/python/packages/raes_backend_libvirt/manifest.py index 90e4fa3dc..65b85ee3c 100644 --- a/implementations/python/packages/raes_backend_libvirt/manifest.py +++ b/implementations/python/packages/raes_backend_libvirt/manifest.py @@ -162,7 +162,7 @@ def create_libvirt_manifest(**config: object) -> BackendManifest: name=LIBVIRT_BACKEND_NAME, version=_current_backend_version(), supported_contract_versions=supported_contract_versions, - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), concept_bindings=( ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"), ConceptBinding(scope="capabilities.provisioner.supported_os_families", family="assets"), diff --git a/implementations/python/packages/raes_backend_libvirt/provisioner.py b/implementations/python/packages/raes_backend_libvirt/provisioner.py index 06bc450dc..c907089eb 100644 --- a/implementations/python/packages/raes_backend_libvirt/provisioner.py +++ b/implementations/python/packages/raes_backend_libvirt/provisioner.py @@ -55,7 +55,7 @@ def __init__( raise ValueError("libvirt provisioner realization envelope does not match driver mode") self._provisioner_capabilities = expected_capabilities self._mode = mode - self._name_prefix = str(getattr(self._driver, "name_prefix", "aces-techvault")) + self._name_prefix = str(getattr(self._driver, "name_prefix", "raes-techvault")) self._backend_realization_envelope = load_libvirt_realization_envelope(mode) self._realization_envelope = expected_envelope diff --git a/implementations/python/packages/raes_backend_libvirt/realization.py b/implementations/python/packages/raes_backend_libvirt/realization.py index e4d882490..f5f099314 100644 --- a/implementations/python/packages/raes_backend_libvirt/realization.py +++ b/implementations/python/packages/raes_backend_libvirt/realization.py @@ -6,7 +6,7 @@ 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/aces/spn`` (spn) files; + 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 @@ -290,7 +290,7 @@ def _realize_account(accumulator: _CloudInitAccumulator, payload: Mapping[str, o # 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/aces/spn/{safe_user}", content=f"{spn}\n", permissions="0600") + CloudInitFile(path=f"/etc/raes/spn/{safe_user}", content=f"{spn}\n", permissions="0600") ) @@ -342,7 +342,7 @@ def _realize_feature( accumulator.runcmd.append(("mkdir", "-p", _dirname(destination))) accumulator.write_files.append( CloudInitFile( - path=f"/etc/aces/features/{safe_path_component(name, fallback='feature')}.json", + path=f"/etc/raes/features/{safe_path_component(name, fallback='feature')}.json", content=_descriptor_body({"feature": name, "type": feature_type, "destination": destination}), permissions="0644", ) @@ -362,7 +362,7 @@ def _content_descriptor( "location": location or "", } safe_name = safe_path_component(name, fallback="content") - return CloudInitFile(path=f"/etc/aces/content/{safe_name}.json", content=_descriptor_body(descriptor)) + return CloudInitFile(path=f"/etc/raes/content/{safe_name}.json", content=_descriptor_body(descriptor)) def _descriptor_body(descriptor: Mapping[str, object]) -> str: @@ -410,7 +410,7 @@ def _resource_name(resource: PlannedResource, payload: Mapping[str, object]) -> name = payload.get("name") or payload.get("node_name") if isinstance(name, str) and name: return name - return provider_resource_name(resource.address, prefix="aces") + return provider_resource_name(resource.address, prefix="raes") def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_appliance.py b/implementations/python/packages/raes_backend_libvirt/techvault_appliance.py index bc47a2860..6773ffc38 100644 --- a/implementations/python/packages/raes_backend_libvirt/techvault_appliance.py +++ b/implementations/python/packages/raes_backend_libvirt/techvault_appliance.py @@ -30,7 +30,7 @@ class BusyboxInitramfsBuilder: busybox_path: Path = Path("/usr/bin/busybox") def build(self, *, domain: Mapping[str, object], target: Path) -> Path: - with tempfile.TemporaryDirectory(prefix="aces-initramfs-") as tmp: + with tempfile.TemporaryDirectory(prefix="raes-initramfs-") as tmp: root = Path(tmp) _write_appliance_root(root, self.busybox_path, domain) target.parent.mkdir(parents=True, exist_ok=True) @@ -60,7 +60,7 @@ def make_libvirt_readable(path: Path) -> None: def _write_appliance_root(root: Path, busybox_path: Path, domain: Mapping[str, object]) -> None: bin_dir = root / "bin" - etc_dir = root / "etc" / "aces" + etc_dir = root / "etc" / "raes" for directory in (bin_dir, etc_dir, root / "proc", root / "sys", root / "dev", root / "tmp", root / "run"): directory.mkdir(parents=True, exist_ok=True) shutil.copy2(busybox_path, bin_dir / "busybox") @@ -79,7 +79,7 @@ def _init_script(domain: Mapping[str, object]) -> str: "mount -t proc proc /proc", "mount -t sysfs sysfs /sys", "mount -t devtmpfs devtmpfs /dev 2>/dev/null || mdev -s", - f"hostname {_shell_quote(str(domain.get('name', 'aces-node')))}", + f"hostname {_shell_quote(str(domain.get('name', 'raes-node')))}", "ip link set lo up", "for iface_path in /sys/class/net/*; do", " iface=${iface_path##*/}", diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py b/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py index b1353bfee..6bc86d539 100644 --- a/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py +++ b/implementations/python/packages/raes_backend_libvirt/techvault_matrix.py @@ -228,9 +228,9 @@ def as_sequence(value: object) -> Sequence[object]: def _domain_cmdline(appliance: str, challenge: str | None) -> str: - parts = ["console=ttyS0", "panic=-1", f"aces.appliance={appliance}"] + parts = ["console=ttyS0", "panic=-1", f"raes.appliance={appliance}"] if challenge: - parts.append(f"aces.challenge={challenge}") + parts.append(f"raes.challenge={challenge}") return " ".join(parts) diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_native.py b/implementations/python/packages/raes_backend_libvirt/techvault_native.py index 68089355b..648b655b7 100644 --- a/implementations/python/packages/raes_backend_libvirt/techvault_native.py +++ b/implementations/python/packages/raes_backend_libvirt/techvault_native.py @@ -116,7 +116,7 @@ class TechVaultNativeLibvirtDriver: connection: object | None = None connection_uri: str = _DEFAULT_CONNECTION_URI connector: Connector | None = None - name_prefix: str = "aces-techvault" + name_prefix: str = "raes-techvault" kernel_path: Path | None = None initramfs_builder: InitramfsBuilder = field(default_factory=BusyboxInitramfsBuilder) define_only: bool = False @@ -147,7 +147,7 @@ def _validate_appliance_flags(self) -> None: raise ValueError("TechVaultNativeLibvirtDriver define-only mode cannot make realization claims.") if self.clean_existing: raise ValueError("TechVaultNativeLibvirtDriver refuses unsafe prefix-wide cleanup.") - safe_prefix = _safe_name(self.name_prefix, fallback="aces-techvault", prefix="") + safe_prefix = _safe_name(self.name_prefix, fallback="raes-techvault", prefix="") if safe_prefix != self.name_prefix: raise ValueError("TechVaultNativeLibvirtDriver name_prefix must already be libvirt-safe.") diff --git a/implementations/python/packages/raes_backend_stubs/manifest.py b/implementations/python/packages/raes_backend_stubs/manifest.py index 6dd2f9258..3603de0a8 100644 --- a/implementations/python/packages/raes_backend_stubs/manifest.py +++ b/implementations/python/packages/raes_backend_stubs/manifest.py @@ -334,7 +334,7 @@ def create_stub_manifest( with_observation=with_observation, with_time=with_time, ), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), concept_bindings=_stub_concept_bindings( with_participant_runtime=with_participant_runtime, with_observation=with_observation, diff --git a/implementations/python/packages/raes_cli/corpus.py b/implementations/python/packages/raes_cli/corpus.py index 93b638b53..015a7ccc1 100644 --- a/implementations/python/packages/raes_cli/corpus.py +++ b/implementations/python/packages/raes_cli/corpus.py @@ -45,7 +45,7 @@ def build( ), ) -> None: """Build the cross-backend evidence corpus and write it to ``--output``.""" - project_dir = work_dir or Path(tempfile.mkdtemp(prefix="aces-cross-backend-corpus-")) + project_dir = work_dir or Path(tempfile.mkdtemp(prefix="raes-cross-backend-corpus-")) report = build_cross_backend_corpus( scenario_path=scenario.resolve(), project_dir=project_dir.resolve(), diff --git a/implementations/python/packages/raes_cli/libvirt.py b/implementations/python/packages/raes_cli/libvirt.py index 818ae8d50..9295c9c3d 100644 --- a/implementations/python/packages/raes_cli/libvirt.py +++ b/implementations/python/packages/raes_cli/libvirt.py @@ -76,7 +76,7 @@ def validate_live( if not typer.confirm("Continue?", default=False): typer.echo("Aborted.") raise typer.Exit(code=0) - resolved_run_id = run_id or datetime.now(UTC).strftime("aces_libvirt_techvault_%Y%m%dT%H%M%SZ") + resolved_run_id = run_id or datetime.now(UTC).strftime("raes_libvirt_techvault_%Y%m%dT%H%M%SZ") report = validate_techvault_live( scenario_path=scenario.resolve(), project_dir=project_dir.resolve(), @@ -119,7 +119,7 @@ def guest_certify( if not typer.confirm("Continue?", default=False): typer.echo("Aborted.") raise typer.Exit(code=0) - resolved_run_id = run_id or datetime.now(UTC).strftime("aces_libvirt_guest_%Y%m%dT%H%M%SZ") + resolved_run_id = run_id or datetime.now(UTC).strftime("raes_libvirt_guest_%Y%m%dT%H%M%SZ") report = run_libvirt_evidence_run( scenario_path=scenario.resolve(), project_dir=project_dir.resolve(), @@ -166,7 +166,7 @@ def validate_evidence( ) -> None: """Produce the libvirt evidence-run evaluator-evidence artifact for a scenario.""" - resolved_run_id = run_id or datetime.now(UTC).strftime("aces_libvirt_evidence_%Y%m%dT%H%M%SZ") + resolved_run_id = run_id or datetime.now(UTC).strftime("raes_libvirt_evidence_%Y%m%dT%H%M%SZ") report = run_libvirt_evidence_run( scenario_path=scenario.resolve(), project_dir=project_dir.resolve(), diff --git a/implementations/python/packages/raes_conformance/conformance/report.py b/implementations/python/packages/raes_conformance/conformance/report.py index 7a810e73e..6e7844a7f 100644 --- a/implementations/python/packages/raes_conformance/conformance/report.py +++ b/implementations/python/packages/raes_conformance/conformance/report.py @@ -122,7 +122,7 @@ def _bounded_conformance_claim( evidence_refs = [f"conformance-case:{case.contract_name}:{case.name}" for case in cases] binding = BehavioralClaimBindingModel( - taxonomy_id="aces-behavioral-relations", + taxonomy_id="raes-behavioral-relations", taxonomy_revision="rev2", relation_id="bounded-probe-success", subject=f"Backend conformance for profile {profile}", diff --git a/implementations/python/packages/raes_conformance/conformance/target.py b/implementations/python/packages/raes_conformance/conformance/target.py index 49745ebaf..9914b2a09 100644 --- a/implementations/python/packages/raes_conformance/conformance/target.py +++ b/implementations/python/packages/raes_conformance/conformance/target.py @@ -136,7 +136,7 @@ class _TargetConformanceOptions: realization_harness: RealizationConformanceHarness | None = None execution_basis: ExecutionBasis = ExecutionBasis.HERMETIC_LIVE realization_envelope: BackendRealizationEnvelopeModel | None = None - observer_version: str = "aces-realization-observer/v1" + observer_version: str = "raes-realization-observer/v1" native_conformance: bool = False diff --git a/implementations/python/packages/raes_conformance/conformance/target_probes.py b/implementations/python/packages/raes_conformance/conformance/target_probes.py index b2d3b7e1f..5f778fe20 100644 --- a/implementations/python/packages/raes_conformance/conformance/target_probes.py +++ b/implementations/python/packages/raes_conformance/conformance/target_probes.py @@ -60,7 +60,7 @@ predicate: kind: presence property: node - semantic_ref: urn:aces:declared-property:node + semantic_ref: urn:raes:declared-property:node operator: exists assertions: health: diff --git a/implementations/python/packages/raes_conformance/realization.py b/implementations/python/packages/raes_conformance/realization.py index f6860a06f..c96e35f10 100644 --- a/implementations/python/packages/raes_conformance/realization.py +++ b/implementations/python/packages/raes_conformance/realization.py @@ -438,7 +438,7 @@ def run_realization_conformance( harness: RealizationConformanceHarness | None, execution_basis: ExecutionBasis, envelope: BackendRealizationEnvelopeModel | None = None, - observer_version: str = "aces-realization-observer/v1", + observer_version: str = "raes-realization-observer/v1", native_conformance: bool = False, ) -> RealizationConformanceRun: """Return realization-honesty cases for one exact target configuration.""" diff --git a/implementations/python/packages/raes_contracts/behavioral_relations.py b/implementations/python/packages/raes_contracts/behavioral_relations.py index 6c4026710..bd1726ca5 100644 --- a/implementations/python/packages/raes_contracts/behavioral_relations.py +++ b/implementations/python/packages/raes_contracts/behavioral_relations.py @@ -166,7 +166,7 @@ class BehavioralWorkedExampleModel(ContractModel): class BehavioralRelationCatalogModel(ContractModel): schema_version: Literal[BEHAVIORAL_RELATIONS_SCHEMA_VERSION] = BEHAVIORAL_RELATIONS_SCHEMA_VERSION - taxonomy_id: Literal["aces-behavioral-relations"] = "aces-behavioral-relations" + taxonomy_id: Literal["raes-behavioral-relations"] = "raes-behavioral-relations" taxonomy_revision: BehavioralTaxonomyRevision bibliography: list[BehavioralBibliographySourceModel] = Field(min_length=1) relations: dict[BehavioralRelationId, BehavioralRelationDefinitionModel] = Field(min_length=1) diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index 5a9715c2d..d311f44ab 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -315,11 +315,11 @@ RuntimeFactVisibilityModel, ) from .schema_constraints import ( - AcesSemanticInvariantEntryModel, - AcesSemanticInvariantInputModel, - AcesSemanticInvariantProfileModel, - AcesSemanticInvariantProfileReferenceModel, - validate_aces_semantic_invariant_annotations, + RaesSemanticInvariantEntryModel, + RaesSemanticInvariantInputModel, + RaesSemanticInvariantProfileModel, + RaesSemanticInvariantProfileReferenceModel, + validate_raes_semantic_invariant_annotations, ) from .semantic_profiles import ( SemanticBehaviorAssumptionModel, @@ -358,8 +358,8 @@ # fmt: off __all__ = [ - "AcesSemanticInvariantEntryModel", "AcesSemanticInvariantInputModel", "AcesSemanticInvariantProfileModel", - "AcesSemanticInvariantProfileReferenceModel", "ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION", + "RaesSemanticInvariantEntryModel", "RaesSemanticInvariantInputModel", "RaesSemanticInvariantProfileModel", + "RaesSemanticInvariantProfileReferenceModel", "ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION", "ATLAS_TACTICS_SOURCE_SCHEMA_VERSION", "AttackEnterpriseTacticSourceTermModel", "AttackEnterpriseTacticsSourceModel", "AtlasTacticSourceTermModel", "AtlasTacticsSourceModel", "ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION", "AssociatedArtifactManifestModel", @@ -474,7 +474,7 @@ "UcoFamilyAlignmentModel", "UCO_ALIGNMENT_SCHEMA_VERSION", "WorkflowCancellationRequestModel", "WORKFLOW_CANCELLATION_REQUEST_SCHEMA_VERSION", "WorkflowExecutionStateModel", "WorkflowFeature", "WorkflowHistoryEventModel", "WorkflowStatePredicateFeature", "WorkflowStepStateModel", - "WORKFLOW_STATE_SCHEMA_VERSION", "validate_aces_semantic_invariant_annotations", + "WORKFLOW_STATE_SCHEMA_VERSION", "validate_raes_semantic_invariant_annotations", "validate_experiment_apparatus_context_against_manifests", "validate_experiment_apparatus_context_archival_datetimes", "validate_experiment_run_against_task", "validate_experiment_run_archival_datetimes", "validate_experiment_study_against_tasks_and_runs", diff --git a/implementations/python/packages/raes_contracts/contracts/associated_artifacts.py b/implementations/python/packages/raes_contracts/contracts/associated_artifacts.py index d167f757c..47e71f3fd 100644 --- a/implementations/python/packages/raes_contracts/contracts/associated_artifacts.py +++ b/implementations/python/packages/raes_contracts/contracts/associated_artifacts.py @@ -13,7 +13,7 @@ from .base import ContractModel, NonEmptyString from .experiment_artifacts import ExperimentArtifactRefModel from .experiment_references import AssociatedArtifactParentReferenceModel -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant AssociatedArtifactSetDigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] @@ -163,7 +163,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "associated-artifact-parent-set-and-byte-binding", "Full conformance requires matching the concrete parent, recomputing the canonical set digest, " diff --git a/implementations/python/packages/raes_contracts/contracts/base.py b/implementations/python/packages/raes_contracts/contracts/base.py index d6fcc91ba..9c7fd631a 100644 --- a/implementations/python/packages/raes_contracts/contracts/base.py +++ b/implementations/python/packages/raes_contracts/contracts/base.py @@ -258,7 +258,7 @@ def _validate_claim_strength(self) -> BehavioralClaimBindingModel: ) -_ACES_SEMANTIC_INVARIANT_PROFILE_URI = "https://aces.dev/schemas/semantic-invariants/v1" +_RAES_SEMANTIC_INVARIANT_PROFILE_URI = "https://raes.dev/schemas/semantic-invariants/v1" def _canonical_digest(digest: str | None) -> str | None: diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index 724a0e248..797929ba2 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -76,19 +76,19 @@ ) from .runtime_facts import RuntimeFactBindingPlaneModel from .schema_constraints import ( - _aces_semantic_invariant_profile_schema_for_bundle, _attach_compiled_address_map_constraints, _attach_instantiation_invariants, _attach_json_schema_metadata, _attach_plan_identity_constraints, _attach_sdl_identifier_constraints, - _validate_aces_semantic_invariant_annotations, + _raes_semantic_invariant_profile_schema_for_bundle, + _validate_raes_semantic_invariant_annotations, ) from .schema_invariants import ( - _add_aces_invariant, - _attach_aces_semantic_profile, + _add_raes_invariant, _attach_experiment_datetime_invariants, _attach_initial_service_state_invariants, + _attach_raes_semantic_profile, _attach_stateful_resource_invariants, ) from .semantic_profiles import SemanticProfileModel @@ -117,7 +117,7 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: from ..validation_profiles import ValidationProfileCatalogModel return { - "aces-semantic-invariants-v1": _aces_semantic_invariant_profile_schema_for_bundle(), + "raes-semantic-invariants-v1": _raes_semantic_invariant_profile_schema_for_bundle(), "sdl-authoring-input-v1": Scenario.model_json_schema(), "instantiated-scenario-v1": InstantiatedScenario.model_json_schema(), "instantiated-scenario-snapshot-v1": InstantiatedScenarioSnapshot.model_json_schema(), @@ -212,7 +212,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: """Build the immutable-in-practice template used by :func:`schema_bundle`.""" bundle = _raw_schema_bundle() - _add_aces_invariant( + _add_raes_invariant( bundle["behavioral-relations-v1"], "behavioral-relations-reference-resolution", "Relation map keys, bibliography references, claim-surface relation references, and worked-example keys " @@ -220,7 +220,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: validator="raes_contracts.behavioral_relations.BehavioralRelationCatalogModel", inputs=[{"contract_id": "behavioral-relations-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( bundle["experiment-study-v1"], "study-behavioral-claim-catalog-resolution", "Every study behavioral claim must resolve against the canonical taxonomy revision, include a required " @@ -231,7 +231,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: {"contract_id": "behavioral-relations-v1", "instance_path": "#"}, ], ) - _add_aces_invariant( + _add_raes_invariant( bundle["scientific-completeness-taxonomy-v1"], "scientific-completeness-taxonomy-rectangular", "Concern and profile ids must be unique, and every profile disposition " @@ -239,7 +239,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: validator="raes_contracts.scientific_completeness.ScientificCompletenessTaxonomyModel", inputs=[{"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( bundle["scientific-completeness-taxonomy-v1"], "scientific-completeness-behavioral-claim-resolution", "Each profile must carry resolved behavioral claim bindings and disjoint, catalog-resolved nonclaimed " @@ -250,7 +250,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: {"contract_id": "behavioral-relations-v1", "instance_path": "#"}, ], ) - _add_aces_invariant( + _add_raes_invariant( bundle["scientific-completeness-assessment-v1"], "scientific-completeness-assessment-status-evidence", "Concern ids must be unique and each delivery status must carry its " @@ -259,7 +259,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: validator="raes_contracts.scientific_completeness.ScientificCompletenessAssessmentModel", inputs=[{"contract_id": "scientific-completeness-assessment-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( bundle["runtime-fact-binding-plane-v1"], "runtime-fact-binding-references-resolve", "Every fact version resolves to a declaration, every binding event resolves to its compiled sink and " @@ -268,7 +268,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: validator="raes_contracts.contracts.runtime_facts.RuntimeFactBindingPlaneModel._validate_references", inputs=[{"contract_id": "runtime-fact-binding-plane-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( bundle["scientific-completeness-assessment-v1"], "scientific-completeness-taxonomy-assessment-join", "Assessment family, taxonomy revision, and concern ids must exactly " @@ -279,7 +279,7 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: {"contract_id": "scientific-completeness-assessment-v1", "instance_path": "#"}, ], ) - _add_aces_invariant( + _add_raes_invariant( bundle["validation-profile-catalog-v1"], "validation-profile-catalog-reference-integrity", "Strength ranks and term ids must be unique, profile identities must " @@ -302,10 +302,10 @@ def _schema_bundle_template() -> dict[str, dict[str, Any]]: _attach_json_schema_metadata(contract_id, json_schema) _attach_compiled_address_map_constraints(contract_id, json_schema) _attach_plan_identity_constraints(contract_id, json_schema) - _attach_aces_semantic_profile(contract_id, json_schema) + _attach_raes_semantic_profile(contract_id, json_schema) known_contract_ids = frozenset(bundle) for contract_id, json_schema in bundle.items(): - _validate_aces_semantic_invariant_annotations( + _validate_raes_semantic_invariant_annotations( contract_id=contract_id, json_schema=json_schema, known_contract_ids=known_contract_ids, diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_apparatus.py b/implementations/python/packages/raes_contracts/contracts/experiment_apparatus.py index 6e259909a..4ff3ffa93 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_apparatus.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_apparatus.py @@ -41,7 +41,7 @@ from .manifests import ProcessorManifestV2Model from .participant_manifests import BackendManifestV2Model from .random_stream import RandomStreamControlBindingModel -from .schema_invariants import _add_aces_invariant, _add_carrier_validation_basis_disclosure_invariant +from .schema_invariants import _add_carrier_validation_basis_disclosure_invariant, _add_raes_invariant from .validation_disclosure import ValidationBasisDisclosureModel, validate_carrier_validation_basis_disclosures _ManifestReferenceKey = tuple[ @@ -199,7 +199,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "canonical-apparatus-manifest-selected", "The canonical processor and backend component manifest_ref values must be present in selected_manifests; " @@ -207,7 +207,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ExperimentApparatusContextModel._validate_instrument_context", inputs=[{"contract_id": "experiment-apparatus-context-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "apparatus-manifest-payload-identity-valid", "Canonical processor and backend manifest_ref values must resolve to manifest payloads with matching " diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py b/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py index dbce5f081..0f6e71d9e 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_bindings.py @@ -26,7 +26,7 @@ ) from .base import ContractModel, NonEmptyString, PrefixedDigestString from .capabilities import ApparatusIdentityModel -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant class BindingScalarType(str, Enum): @@ -275,7 +275,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "binding-descriptors-canonical-targets-injective", "Binding ids must be unique and target resolution must be injective within each source condition.", @@ -367,7 +367,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "participant-configuration-digest-valid", "The configuration digest must be the RFC 8785/JCS digest of the complete normalized configuration.", diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_capture.py b/implementations/python/packages/raes_contracts/contracts/experiment_capture.py index 428878667..ee6e2fed5 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_capture.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_capture.py @@ -16,7 +16,7 @@ ExperimentMeasurementChannelReferenceModel, ) from .experiment_references import ExperimentReferenceModel -from .schema_invariants import _add_aces_invariant, _add_aces_plane +from .schema_invariants import _add_raes_invariant, _add_raes_plane from .validators import _validate_unique_string_values __all__ = [ @@ -81,7 +81,7 @@ def __get_pydantic_json_schema__( {"required": ["trigger_ref"], "properties": {"trigger_ref": {"not": {"type": "null"}}}}, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "capture-window-interval-valid", "Capture window ends_at must not precede starts_at when both timestamps are present.", @@ -163,7 +163,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "capture-requirement-key-matches-requirement-id", "Every capture_requirements object key must match the embedded requirement_id value, and window_refs " @@ -171,7 +171,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ExperimentCaptureSpecModel._validate_capture_spec", inputs=[{"contract_id": "experiment-capture-spec-v1", "instance_path": "#"}], ) - _add_aces_plane(json_schema, "experiment-capture-spec-v1") + _add_raes_plane(json_schema, "experiment-capture-spec-v1") return json_schema diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py b/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py index c1439fd05..eb1b3c1a7 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_disclosure.py @@ -19,7 +19,7 @@ ExperimentProcessorReferenceModel, ) from .experiment_references import ExperimentReferenceModel -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant from .validators import _validate_unique_string_values __all__ = [ @@ -193,7 +193,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "augmentation-disclosure-semantics-valid", "Augmentation disclosures must keep environment-visible, participant-visible, and " @@ -254,7 +254,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "metric-definition-key-matches-metric-id", "Every metric_definitions object key must match the embedded metric_id value.", @@ -442,7 +442,7 @@ def __get_pydantic_json_schema__( {"required": ["notes"], "properties": {"notes": {"minItems": 1}}}, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "apparatus-constraint-identity-manifest-resolves", "Every allowed processor/backend identity reference must have a matching required manifest ref_id " diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_evidence.py b/implementations/python/packages/raes_contracts/contracts/experiment_evidence.py index 155ad536b..6aa00404e 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_evidence.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_evidence.py @@ -28,8 +28,8 @@ ExperimentTaskReferenceModel, ) from .schema_invariants import ( - _add_aces_invariant, - _add_aces_plane, + _add_raes_invariant, + _add_raes_plane, _extend_reported_value_status_schema, _validate_reported_value_status, ) @@ -70,7 +70,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "evidence-record-raw-content-present", "Evidence records must carry raw content as an artifact reference, content URI with checksum, or bounded " @@ -78,7 +78,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record", inputs=[{"contract_id": "experiment-evidence-record-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "evidence-record-captured-at-valid", "captured_at must be a valid RFC 3339 date-time.", @@ -105,7 +105,7 @@ def __get_pydantic_json_schema__( }, } ) - _add_aces_plane(json_schema, "experiment-evidence-record-v1") + _add_raes_plane(json_schema, "experiment-evidence-record-v1") return json_schema @@ -156,21 +156,21 @@ def __get_pydantic_json_schema__( json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) _extend_reported_value_status_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "derived-measure-reported-value-present", "Reported derived measures must include a value; missing/withheld/not-applicable measures must not.", validator="raes_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure", inputs=[{"contract_id": "experiment-derived-measure-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "derived-measure-generated-at-valid", "generated_at must be a valid RFC 3339 date-time.", validator="raes_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure", inputs=[{"contract_id": "experiment-derived-measure-v1", "instance_path": "#/generated_at"}], ) - _add_aces_plane(json_schema, "experiment-derived-measure-v1") + _add_raes_plane(json_schema, "experiment-derived-measure-v1") return json_schema @@ -201,7 +201,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "run-traceability-refs-unique", "Run provenance traceability references must be duplicate-free, and claim refs must be grounded by " @@ -290,7 +290,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "realized-form-disclosure-substantive", "Every realized-form disclosure must name a realized reference or value summary and use the right " diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_manifest_references.py b/implementations/python/packages/raes_contracts/contracts/experiment_manifest_references.py index b9ca6bd00..9247913d6 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_manifest_references.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_manifest_references.py @@ -10,7 +10,7 @@ from ..versions import BACKEND_MANIFEST_V2_SCHEMA_VERSION, PROCESSOR_MANIFEST_V2_SCHEMA_VERSION from .experiment_references import ExperimentReferenceModel, PrunedReferenceFieldsMixin -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant _MANIFEST_REFERENCE_DEFS_POINTER = "#/$defs/ExperimentManifestReferenceModel" @@ -134,7 +134,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "manifest-reference-digest-scope-valid", "Manifest digest qualifiers are limited to processor/backend manifest refs that can be checked " diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_run.py b/implementations/python/packages/raes_contracts/contracts/experiment_run.py index 974415650..ab78e1f5c 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_run.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_run.py @@ -51,8 +51,8 @@ from .participant_manifests import ParticipantImplementationProvenanceModel from .random_stream import RandomStreamDrawRecordModel from .schema_invariants import ( - _add_aces_invariant, _add_carrier_validation_basis_disclosure_invariant, + _add_raes_invariant, _extend_reported_value_status_schema, _validate_reported_value_status, ) @@ -184,35 +184,35 @@ def __get_pydantic_json_schema__( }, } ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "ended-at-not-before-started-at", "ended_at must be greater than or equal to started_at.", validator=_ARCHIVAL_RUN_VALIDATOR, inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "result-evidence-ref-resolves", "Every result_summaries evidence_refs ref_id must match an evidence_artifacts artifact_id.", validator=_ARCHIVAL_RUN_VALIDATOR, inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "participant-implementation-provenance-resolves", "Participant implementation apparatus components must resolve to run-level participant provenance.", validator=_ARCHIVAL_RUN_VALIDATOR, inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "realized-form-evidence-refs-traced", "Every realized-form disclosure evidence ref must also appear in the run traceability evidence refs.", validator=_ARCHIVAL_RUN_VALIDATOR, inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "augmentation-disclosure-evidence-refs-traced", "Every augmentation disclosure evidence ref must also appear in the run traceability evidence refs, " @@ -223,7 +223,7 @@ def __get_pydantic_json_schema__( _add_carrier_validation_basis_disclosure_invariant( json_schema, contract_id="experiment-run-v1", subject_kind="experiment_run" ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "stochastic-draws-control-ref-resolves", "Every stochastic_draws control_id must resolve to a stochastic_controls control_id with an " @@ -232,14 +232,14 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.validate_experiment_run_against_task", inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/stochastic_draws"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "realized-time-model-bound-to-run", "When present, realized-time provenance must match the run id and its declared model digest.", validator=_ARCHIVAL_RUN_VALIDATOR, inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/realized_time_model"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "task-run-protocol-binding-valid", "Run apparatus, result metric ids, and concrete evidence artifacts must satisfy the " diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_spec.py b/implementations/python/packages/raes_contracts/contracts/experiment_spec.py index ba8d45d38..58f68b184 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_spec.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_spec.py @@ -27,7 +27,7 @@ ExperimentStudyFactorModel, ExperimentStudyMembershipModel, ) -from .schema_invariants import _add_aces_invariant, _add_carrier_validation_basis_disclosure_invariant +from .schema_invariants import _add_carrier_validation_basis_disclosure_invariant, _add_raes_invariant from .validation_disclosure import ValidationBasisDisclosureModel, validate_carrier_validation_basis_disclosures _STUDY_AGAINST_TASKS_AND_RUNS_VALIDATOR = "raes_contracts.contracts.validate_experiment_study_against_tasks_and_runs" @@ -158,7 +158,7 @@ def __get_pydantic_json_schema__( }, } ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "claim-bearing-study-analysis-plan-required", "Study and benchmark records must include research questions, revisioned behavioral claim bindings, run " @@ -166,7 +166,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ExperimentStudyModel._validate_claim_bearing_study", inputs=[{"contract_id": "experiment-study-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "study-analysis-metrics-grounded-in-task-protocols", "Study analysis_plan metrics must be declared by included experiment task protocols.", @@ -177,7 +177,7 @@ def __get_pydantic_json_schema__( {"contract_id": "experiment-run-v1", "instance_path": "#"}, ], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "study-analysis-metrics-covered-by-evaluation-run-results", "Study analysis_plan metrics must have result_summaries, including explicit missing/withheld " @@ -189,7 +189,7 @@ def __get_pydantic_json_schema__( {"contract_id": "experiment-run-v1", "instance_path": "#"}, ], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "study-analysis-runs-eligible", "Study analysis_plan evaluation-run members must resolve unambiguously and exclude invalidated, " @@ -200,7 +200,7 @@ def __get_pydantic_json_schema__( {"contract_id": "experiment-run-v1", "instance_path": "#"}, ], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "study-run-allocation-covered-by-evaluation-run-members", "Study run_allocation compared_conditions must be represented by eligible included evaluation-run " @@ -216,7 +216,7 @@ def __get_pydantic_json_schema__( _add_carrier_validation_basis_disclosure_invariant( json_schema, contract_id="experiment-study-v1", subject_kind="experiment_study" ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "study-run-allocation-stochastic-control-consistency", "When run_allocation compares evaluation runs, every shared stochastic_controls control_id across " @@ -307,7 +307,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "run-plan-exactly-one-run-count-source", "A run plan must declare exactly one of allocation or target_run_count, and every red-variant " @@ -442,7 +442,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "experiment-spec-blocking-factors-declared", "When a run plan declares an allocation with blocking factors, every blocking factor must be a " @@ -450,7 +450,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ExperimentSpecModel._validate_experiment_spec", inputs=[{"contract_id": "experiment-authoring-input-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "experiment-binding-source-joins-valid", "Explicit bindings must cover every compared condition and resolve exact declared factor levels.", diff --git a/implementations/python/packages/raes_contracts/contracts/experiment_study.py b/implementations/python/packages/raes_contracts/contracts/experiment_study.py index a12b7a436..688d2875d 100644 --- a/implementations/python/packages/raes_contracts/contracts/experiment_study.py +++ b/implementations/python/packages/raes_contracts/contracts/experiment_study.py @@ -14,7 +14,7 @@ ExperimentConditionAssignmentParameterModel, ExperimentReferenceModel, ) -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant class ExperimentStudyMembershipModel(ContractModel): @@ -230,7 +230,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "run-allocation-condition-assignments-valid", "Run-allocation compared_conditions, condition_assignments keys, embedded condition ids, blocking " @@ -301,7 +301,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "analysis-plan-substantive-methods-required", "Analysis plans must name metrics plus structured statistical, uncertainty, multiplicity, " diff --git a/implementations/python/packages/raes_contracts/contracts/participant_context.py b/implementations/python/packages/raes_contracts/contracts/participant_context.py index f4e22f876..1e0059472 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_context.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_context.py @@ -9,7 +9,7 @@ from pydantic_core import CoreSchema from .base import ContractModel, NonEmptyString, Rfc3339DateTimeString -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant ParticipantContextAudienceScope = Literal[ "participant_visible", @@ -203,7 +203,7 @@ def _validate_sem216_audience_boundary(self) -> ParticipantContextViewModel: # # The required-ref clauses are also published as a schema allOf so schema-only consumers # enforce them; the relational mediation and payload-aliasing clauses cannot be expressed - # in JSON Schema and are published as x-aces-invariants (see __get_pydantic_json_schema__). + # in JSON Schema and are published as x-raes-invariants (see __get_pydantic_json_schema__). if self.audience_scope != "participant_visible": return self archival_layers = [ @@ -272,8 +272,8 @@ def __get_pydantic_json_schema__( ) # SEM-216 relational obligations that standard JSON Schema cannot express are published # as RAES semantic invariants so schema-only consumers see the full portable contract and - # the validator that enforces it (mirrors the experiment-core x-aces-invariants pattern). - _add_aces_invariant( + # the validator that enforces it (mirrors the experiment-core x-raes-invariants pattern). + _add_raes_invariant( json_schema, "context-view-sem216-archival-source-mediated", "Participant-visible context views drawing on an archival evidence_record or derived_measure " @@ -281,7 +281,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary", inputs=[{"contract_id": "participant-context-view-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "context-view-sem216-payload-not-raw-archival", "Participant-visible context views must not set payload_ref to a raw archival evidence_record or " diff --git a/implementations/python/packages/raes_contracts/contracts/participant_control.py b/implementations/python/packages/raes_contracts/contracts/participant_control.py index 65cc7951e..3d309d36d 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_control.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_control.py @@ -18,7 +18,7 @@ PrefixedDigestString, ) from .participant_envelopes import ParticipantRuntimeBaseEnvelopeModel -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant class ParticipantControlDisposition(str, Enum): @@ -293,7 +293,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "participant-control-occurrence-context-agreement", "Every occurrence must resolve one matching compiled ACT-617 declaration and preserve participant, " @@ -301,7 +301,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.validate_participant_control_occurrence_context", inputs=[{"contract_id": "participant-control-occurrence-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "participant-control-occurrence-not-lifecycle-evidence", "Proposal, approval, denial, direction, intervention, handoff, override, and cancellation remain " diff --git a/implementations/python/packages/raes_contracts/contracts/participant_crossing.py b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py index 6b32c7c11..4d6aa9999 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_crossing.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py @@ -27,7 +27,7 @@ ) from .participant_envelopes import ParticipantRuntimeBaseEnvelopeModel from .participant_runtime import ParticipantRuntimeOrderingBasis -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant _TRANSFORMATION_OPERATIONS = frozenset( { @@ -411,7 +411,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "participant-crossing-context-agreement", "Every crossing fact must resolve typed subjects, exact policy revisions, predecessor stages, " @@ -419,7 +419,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.validate_participant_crossing_occurrence_context", inputs=[{"contract_id": "participant-crossing-occurrence-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "participant-crossing-stage-separation", "Requested, decided, transformed, disclosed, attempted, delivered, observed, and audited facts " diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py index 3d7b3818a..a1f4f9c64 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py @@ -14,7 +14,7 @@ from .participant_decision_surface_exposure import ( ParticipantDecisionSurfaceExposureBindingModel, ) -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant ParticipantDecisionSurfaceVisibility = Literal[ "observable", @@ -379,7 +379,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "decision-surface-entry-reference-agreement", "Candidate, constrained-form, and open-ended action references must resolve to action entries and " @@ -387,7 +387,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ParticipantDecisionSurfaceModel._validate_surface_relations", inputs=[{"contract_id": "participant-decision-surface-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "decision-surface-presentation-not-lifecycle-evidence", "Surface membership or presentation carries no selection, admission, execution, result, or outcome fact; " @@ -395,7 +395,7 @@ def __get_pydantic_json_schema__( validator="raes_contracts.contracts.ParticipantDecisionSurfaceModel", inputs=[{"contract_id": "participant-decision-surface-v1", "instance_path": "#"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "decision-surface-sem226-item-exposure-agreement", "Every serialized context, action, and affordance reference must have one participant-, audience-, " diff --git a/implementations/python/packages/raes_contracts/contracts/random_stream.py b/implementations/python/packages/raes_contracts/contracts/random_stream.py index cfaf03468..46b322ec0 100644 --- a/implementations/python/packages/raes_contracts/contracts/random_stream.py +++ b/implementations/python/packages/raes_contracts/contracts/random_stream.py @@ -33,7 +33,7 @@ from ..versions import RANDOM_STREAM_PROFILE_SCHEMA_VERSION, RANDOM_STREAM_VECTOR_SCHEMA_VERSION from .base import ContractModel, NonEmptyString, NonNegativeInteger, PositiveInteger, SemanticProfileId from .experiment_references import ExperimentReferenceModel -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant from .validators import _validate_controlled_vocabulary_terms RANDOM_STREAM_DRAW_PURPOSE_SCOPE = "random_streams.draw_purpose" @@ -147,7 +147,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "random-stream-address-draw-purpose-governed", "draw_purpose must be a term from the random_streams.draw_purpose controlled vocabulary.", @@ -249,7 +249,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "random-stream-profile-transform-keys-match", "transforms dict keys must match each entry's embedded transform_id.", @@ -298,7 +298,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - _add_aces_invariant( + _add_raes_invariant( json_schema, "random-stream-draw-record-local-coordinate-matches-address", "local_coordinate must match address.local_coordinate.", @@ -308,7 +308,7 @@ def __get_pydantic_json_schema__( ), inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/stochastic_draws"}], ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "random-stream-draw-record-outcome-matches-exhaustion", "outcome must be present exactly when rejection_exhausted is false, and absent when true.", diff --git a/implementations/python/packages/raes_contracts/contracts/reusable_assets.py b/implementations/python/packages/raes_contracts/contracts/reusable_assets.py index be979bd16..2fbda722f 100644 --- a/implementations/python/packages/raes_contracts/contracts/reusable_assets.py +++ b/implementations/python/packages/raes_contracts/contracts/reusable_assets.py @@ -98,7 +98,7 @@ class ReusableAssetEvidenceRequirementModel(ContractModel): """One evidence-class expectation an asset family must satisfy. ``mechanism_ref`` names the *existing* RAES mechanism that carries the - evidence (e.g. ``aces.lock.json`` digest pins, ``ExperimentChecksumModel``, + evidence (e.g. ``raes.lock.json`` digest pins, ``ExperimentChecksumModel``, ``controlled-vocabularies-v1.source``). GOV-913 declares policy over the incumbent mechanisms; it does not introduce a parallel evidence store, so this contract never carries the evidence payload itself — only the diff --git a/implementations/python/packages/raes_contracts/contracts/schema_constraints.py b/implementations/python/packages/raes_contracts/contracts/schema_constraints.py index 9e1881ba8..7278aea38 100644 --- a/implementations/python/packages/raes_contracts/contracts/schema_constraints.py +++ b/implementations/python/packages/raes_contracts/contracts/schema_constraints.py @@ -20,10 +20,10 @@ from ..addressing import COMPILED_ADDRESS_JSON_SCHEMA from ..planning import PLAN_ADDRESS_ROOT_BY_DOMAIN, PLAN_RESOURCE_TYPES_BY_DOMAIN, RuntimeDomain from .base import ( - _ACES_SEMANTIC_INVARIANT_PROFILE_URI, _BACKEND_CONCEPT_BINDING_SCOPES, _PARTICIPANT_IMPLEMENTATION_CONCEPT_BINDING_SCOPES, _PROCESSOR_CONCEPT_BINDING_SCOPES, + _RAES_SEMANTIC_INVARIANT_PROFILE_URI, ContractModel, JsonInstancePathString, NonEmptyString, @@ -268,9 +268,9 @@ def _attach_instantiation_invariants(contract_id: str, json_schema: dict[str, An def _schema_id_for_contract_id(contract_id: str) -> str: - if contract_id == "aces-semantic-invariants-v1": - return _ACES_SEMANTIC_INVARIANT_PROFILE_URI - return f"https://aces.dev/schemas/{contract_id}.json" + if contract_id == "raes-semantic-invariants-v1": + return _RAES_SEMANTIC_INVARIANT_PROFILE_URI + return f"https://raes.dev/schemas/{contract_id}.json" def _attach_json_schema_metadata(contract_id: str, json_schema: dict[str, Any]) -> None: @@ -325,71 +325,71 @@ def _attach_plan_identity_constraints(contract_id: str, json_schema: dict[str, A _JSON_SCHEMA_DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema" -_ACES_SEMANTIC_INVARIANTS_SCHEMA_VERSION = "aces-semantic-invariants/v1" +_RAES_SEMANTIC_INVARIANTS_SCHEMA_VERSION = "raes-semantic-invariants/v1" -class AcesSemanticInvariantInputModel(ContractModel): +class RaesSemanticInvariantInputModel(ContractModel): """Input contract and instance path required by one RAES semantic invariant.""" contract_id: NonEmptyString instance_path: JsonInstancePathString -class AcesSemanticInvariantEntryModel(ContractModel): +class RaesSemanticInvariantEntryModel(ContractModel): """Machine-readable semantic invariant annotation entry.""" id: NonEmptyString description: NonEmptyString level: Literal["error"] validator: NonEmptyString - inputs: list[AcesSemanticInvariantInputModel] = Field(min_length=1) + inputs: list[RaesSemanticInvariantInputModel] = Field(min_length=1) -class AcesSemanticInvariantProfileModel(ContractModel): +class RaesSemanticInvariantProfileModel(ContractModel): """Published shape for RAES semantic-invariant annotations.""" - schema_version: Literal[_ACES_SEMANTIC_INVARIANTS_SCHEMA_VERSION] - profile_id: Literal["aces-semantic-invariants-v1"] - uri: Literal["https://aces.dev/schemas/semantic-invariants/v1"] - keyword: Literal["x-aces-invariants"] - invariant_entry_schema: Literal["#/$defs/AcesSemanticInvariantEntryModel"] - profile_reference_schema: Literal["#/$defs/AcesSemanticInvariantProfileReferenceModel"] - invariants: list[AcesSemanticInvariantEntryModel] + schema_version: Literal[_RAES_SEMANTIC_INVARIANTS_SCHEMA_VERSION] + profile_id: Literal["raes-semantic-invariants-v1"] + uri: Literal["https://raes.dev/schemas/semantic-invariants/v1"] + keyword: Literal["x-raes-invariants"] + invariant_entry_schema: Literal["#/$defs/RaesSemanticInvariantEntryModel"] + profile_reference_schema: Literal["#/$defs/RaesSemanticInvariantProfileReferenceModel"] + invariants: list[RaesSemanticInvariantEntryModel] -class AcesSemanticInvariantProfileReferenceModel(ContractModel): +class RaesSemanticInvariantProfileReferenceModel(ContractModel): """Host-schema reference to the RAES semantic-invariant profile.""" - id: Literal["aces-semantic-invariants-v1"] - uri: Literal["https://aces.dev/schemas/semantic-invariants/v1"] + id: Literal["raes-semantic-invariants-v1"] + uri: Literal["https://raes.dev/schemas/semantic-invariants/v1"] contract_id: NonEmptyString - keyword: Literal["x-aces-invariants"] + keyword: Literal["x-raes-invariants"] required: Literal[True] - entry_schema_contract_id: Literal["aces-semantic-invariants-v1"] - entry_schema_pointer: Literal["#/$defs/AcesSemanticInvariantEntryModel"] + entry_schema_contract_id: Literal["raes-semantic-invariants-v1"] + entry_schema_pointer: Literal["#/$defs/RaesSemanticInvariantEntryModel"] -def _aces_semantic_invariant_profile_schema_for_bundle() -> dict[str, Any]: - json_schema = AcesSemanticInvariantProfileModel.model_json_schema() - json_schema.setdefault(_DEFS_KEY, {})["AcesSemanticInvariantProfileReferenceModel"] = ( - AcesSemanticInvariantProfileReferenceModel.model_json_schema() +def _raes_semantic_invariant_profile_schema_for_bundle() -> dict[str, Any]: + json_schema = RaesSemanticInvariantProfileModel.model_json_schema() + json_schema.setdefault(_DEFS_KEY, {})["RaesSemanticInvariantProfileReferenceModel"] = ( + RaesSemanticInvariantProfileReferenceModel.model_json_schema() ) return json_schema -def _iter_aces_semantic_invariant_entries(schema_node: object) -> list[dict[str, Any]]: +def _iter_raes_semantic_invariant_entries(schema_node: object) -> list[dict[str, Any]]: entries: list[dict[str, Any]] = [] if isinstance(schema_node, dict): - invariants = schema_node.get("x-aces-invariants") + invariants = schema_node.get("x-raes-invariants") if invariants is not None: if not isinstance(invariants, list): - raise ValueError("x-aces-invariants must be an array") + raise ValueError("x-raes-invariants must be an array") entries.extend(invariants) for value in schema_node.values(): - entries.extend(_iter_aces_semantic_invariant_entries(value)) + entries.extend(_iter_raes_semantic_invariant_entries(value)) elif isinstance(schema_node, list): for value in schema_node: - entries.extend(_iter_aces_semantic_invariant_entries(value)) + entries.extend(_iter_raes_semantic_invariant_entries(value)) return entries @@ -415,26 +415,26 @@ def _resolve_semantic_validator(validator: str) -> object: raise ValueError(f"semantic invariant validator '{validator}' does not resolve to an importable object") -def _validate_aces_semantic_invariant_annotations( +def _validate_raes_semantic_invariant_annotations( *, contract_id: str, json_schema: dict[str, Any], known_contract_ids: frozenset[str], ) -> None: - invariant_entries = _iter_aces_semantic_invariant_entries(json_schema) - profile_payload = json_schema.get("x-aces-semantic-profile") + invariant_entries = _iter_raes_semantic_invariant_entries(json_schema) + profile_payload = json_schema.get("x-raes-semantic-profile") if not invariant_entries: if profile_payload is not None: raise ValueError(f"schema '{contract_id}' declares a semantic profile without semantic invariants") return - profile = AcesSemanticInvariantProfileReferenceModel.model_validate(profile_payload) + profile = RaesSemanticInvariantProfileReferenceModel.model_validate(profile_payload) if profile.contract_id != contract_id: raise ValueError(f"schema '{contract_id}' semantic profile contract_id must match the published contract id") seen_invariant_ids: set[str] = set() for invariant_payload in invariant_entries: - invariant = AcesSemanticInvariantEntryModel.model_validate(invariant_payload) + invariant = RaesSemanticInvariantEntryModel.model_validate(invariant_payload) if not callable(_resolve_semantic_validator(invariant.validator)): raise ValueError(f"semantic invariant validator '{invariant.validator}' must resolve to a callable") if invariant.id in seen_invariant_ids: @@ -455,10 +455,10 @@ def _published_contract_ids() -> frozenset[str]: return frozenset(_schema_bundle_template()) -def validate_aces_semantic_invariant_annotations(contract_id: str, json_schema: dict[str, Any]) -> None: +def validate_raes_semantic_invariant_annotations(contract_id: str, json_schema: dict[str, Any]) -> None: """Validate RAES semantic-invariant metadata on a published JSON Schema.""" - _validate_aces_semantic_invariant_annotations( + _validate_raes_semantic_invariant_annotations( contract_id=contract_id, json_schema=json_schema, known_contract_ids=_published_contract_ids(), diff --git a/implementations/python/packages/raes_contracts/contracts/schema_invariants.py b/implementations/python/packages/raes_contracts/contracts/schema_invariants.py index 307bf16ed..5a13e8ac0 100644 --- a/implementations/python/packages/raes_contracts/contracts/schema_invariants.py +++ b/implementations/python/packages/raes_contracts/contracts/schema_invariants.py @@ -7,7 +7,7 @@ from pydantic.json_schema import JsonSchemaValue from raes.observability_plane_semantics import classify_contract_plane -from .base import _ACES_SEMANTIC_INVARIANT_PROFILE_URI +from .base import _RAES_SEMANTIC_INVARIANT_PROFILE_URI # Shared instance_path for the ASR-515 carrier-embedded # validation_basis_disclosures invariant inputs. Defined once here and @@ -18,7 +18,7 @@ _CARRIER_DISCLOSURES_INSTANCE_PATH = "#/validation_basis_disclosures" -def _add_aces_invariant( +def _add_raes_invariant( json_schema: JsonSchemaValue, invariant_id: str, description: str, @@ -26,7 +26,7 @@ def _add_aces_invariant( validator: str, inputs: list[dict[str, str]], ) -> None: - invariants = json_schema.setdefault("x-aces-invariants", []) + invariants = json_schema.setdefault("x-raes-invariants", []) if isinstance(invariants, list): invariants.append( { @@ -39,37 +39,37 @@ def _add_aces_invariant( ) -def _add_aces_plane(json_schema: JsonSchemaValue, contract_id: str) -> None: +def _add_raes_plane(json_schema: JsonSchemaValue, contract_id: str) -> None: """Publish the carrier's single SEM-224 observability/evidence plane. Plane ownership is sourced from the carrier-oriented classifier so the - portable ``x-aces-plane`` annotation cannot drift from + portable ``x-raes-plane`` annotation cannot drift from ``raes.observability_plane_semantics`` (ADR-066 / SEM-224). """ - json_schema["x-aces-plane"] = classify_contract_plane(contract_id).value + json_schema["x-raes-plane"] = classify_contract_plane(contract_id).value -def _schema_contains_aces_invariants(schema_node: object) -> bool: +def _schema_contains_raes_invariants(schema_node: object) -> bool: if isinstance(schema_node, dict): - return "x-aces-invariants" in schema_node or any( - _schema_contains_aces_invariants(value) for value in schema_node.values() + return "x-raes-invariants" in schema_node or any( + _schema_contains_raes_invariants(value) for value in schema_node.values() ) if isinstance(schema_node, list): - return any(_schema_contains_aces_invariants(value) for value in schema_node) + return any(_schema_contains_raes_invariants(value) for value in schema_node) return False -def _attach_aces_semantic_profile(contract_id: str, json_schema: dict[str, Any]) -> None: - if _schema_contains_aces_invariants(json_schema): - json_schema["x-aces-semantic-profile"] = { - "id": "aces-semantic-invariants-v1", - "uri": _ACES_SEMANTIC_INVARIANT_PROFILE_URI, +def _attach_raes_semantic_profile(contract_id: str, json_schema: dict[str, Any]) -> None: + if _schema_contains_raes_invariants(json_schema): + json_schema["x-raes-semantic-profile"] = { + "id": "raes-semantic-invariants-v1", + "uri": _RAES_SEMANTIC_INVARIANT_PROFILE_URI, "contract_id": contract_id, - "keyword": "x-aces-invariants", + "keyword": "x-raes-invariants", "required": True, - "entry_schema_contract_id": "aces-semantic-invariants-v1", - "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "entry_schema_contract_id": "raes-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/RaesSemanticInvariantEntryModel", } @@ -119,7 +119,7 @@ def _attach_experiment_datetime_invariants(contract_id: str, json_schema: dict[s if invariant is None: return invariant_id, description, validator, inputs = invariant - _add_aces_invariant( + _add_raes_invariant( json_schema, invariant_id, description, @@ -136,7 +136,7 @@ def _attach_stateful_resource_invariants(contract_id: str, json_schema: dict[str }: return input_contract = [{"contract_id": contract_id, "instance_path": "#"}] - _add_aces_invariant( + _add_raes_invariant( json_schema, "stateful-generated-artifact-semantics", "Generated artifact output names and paths, consumers, and dependency entries must be unique, and " @@ -144,7 +144,7 @@ def _attach_stateful_resource_invariants(contract_id: str, json_schema: dict[str validator="raes.stateful_resources.GeneratedArtifact._unique_outputs_and_consumers", inputs=input_contract, ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "stateful-persistent-volume-semantics", "Persistent volume consumers and dependency entries must be unique and access cardinality must match " @@ -152,7 +152,7 @@ def _attach_stateful_resource_invariants(contract_id: str, json_schema: dict[str validator="raes.stateful_resources.PersistentVolume._unique_consumers", inputs=input_contract, ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "stateful-cross-resource-semantics", "Stateful resource consumers and dependencies must resolve unambiguously, use the POSIX v1 path dialect, " @@ -173,7 +173,7 @@ def _add_carrier_validation_basis_disclosure_invariant( ``validate_carrier_validation_basis_disclosures`` does. """ carrier_label = subject_kind.removeprefix("experiment_") - _add_aces_invariant( + _add_raes_invariant( json_schema, f"{carrier_label}-validation-basis-disclosure-identity-matches", f"Every validation_basis_disclosures entry must declare subject_kind={subject_kind!r} and a subject_ref " @@ -191,7 +191,7 @@ def _attach_initial_service_state_invariants(contract_id: str, json_schema: dict "scenario-satisfiability-evidence-v1", }: return - _add_aces_invariant( + _add_raes_invariant( json_schema, "initial-service-state-semantics", "Service-target content must resolve to one named service, retain exact tenant/reset ownership and " diff --git a/implementations/python/packages/raes_contracts/contracts/trial_cleanup.py b/implementations/python/packages/raes_contracts/contracts/trial_cleanup.py index c59e4dcc4..9044781dd 100644 --- a/implementations/python/packages/raes_contracts/contracts/trial_cleanup.py +++ b/implementations/python/packages/raes_contracts/contracts/trial_cleanup.py @@ -16,7 +16,7 @@ ) from ._trial_cleanup_validation import validate_reset_retry_obligations from .base import ContractModel, NonEmptyString, PositiveInteger -from .schema_invariants import _add_aces_invariant +from .schema_invariants import _add_raes_invariant CleanupTrigger = Literal["success", "failure", "cancellation", "timeout", "retry", "abort"] CleanupRequirement = Literal["required", "best-effort"] @@ -225,7 +225,7 @@ def visit(obligation_id: str) -> None: @classmethod def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: json_schema = handler.resolve_ref_schema(handler(core_schema)) - _add_aces_invariant( + _add_raes_invariant( json_schema, "trial-cleanup-plan-references-and-retry-safe", "Cleanup boundary and dependency references resolve, ordering is acyclic, required cleanup is " @@ -315,7 +315,7 @@ def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonS "then": {"properties": {"clean_state_claim": {"type": "null"}}}, } ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "trial-cleanup-receipt-binds-plan-and-required-outcomes", "A cleanup receipt keeps attempt identity distinct, reports cleanup independently from trial outcome, " @@ -450,7 +450,7 @@ def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonS "then": parallel_then, } ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "scheduler-isolation-serial-default-and-complete-parallel-proof", "Scheduling defaults to serial; bounded parallelism requires independent evidence for range, capacity, " diff --git a/implementations/python/packages/raes_contracts/contracts/validation_disclosure.py b/implementations/python/packages/raes_contracts/contracts/validation_disclosure.py index f29e62cdc..0c74e7871 100644 --- a/implementations/python/packages/raes_contracts/contracts/validation_disclosure.py +++ b/implementations/python/packages/raes_contracts/contracts/validation_disclosure.py @@ -32,7 +32,7 @@ from ..versions import VALIDATION_BASIS_DISCLOSURE_SCHEMA_VERSION from .base import ContractModel, NonEmptyString, Rfc3339DateTimeString, _parse_rfc3339_datetime from .experiment_manifest_references import ExperimentEvidenceRecordReferenceModel -from .schema_invariants import _CARRIER_DISCLOSURES_INSTANCE_PATH, _add_aces_invariant +from .schema_invariants import _CARRIER_DISCLOSURES_INSTANCE_PATH, _add_raes_invariant from .validation_disclosure_gates import ( _EVIDENCE_REF_REQUIRED_STRENGTHS, _STRENGTH_RANK, @@ -263,7 +263,7 @@ def __get_pydantic_json_schema__( {"contract_id": "experiment-study-v1", "instance_path": _CARRIER_DISCLOSURES_INSTANCE_PATH}, ] for invariant_id, description in _DISCLOSURE_INVARIANTS: - _add_aces_invariant(json_schema, invariant_id, description, validator=validator, inputs=inputs) + _add_raes_invariant(json_schema, invariant_id, description, validator=validator, inputs=inputs) return json_schema diff --git a/implementations/python/packages/raes_contracts/exploit_path.py b/implementations/python/packages/raes_contracts/exploit_path.py index d41712134..b27420e64 100644 --- a/implementations/python/packages/raes_contracts/exploit_path.py +++ b/implementations/python/packages/raes_contracts/exploit_path.py @@ -14,7 +14,7 @@ from raes.phase_contracts import SemanticDigest from .contracts.base import ContractModel, PrefixedDigestString -from .contracts.schema_invariants import _add_aces_invariant +from .contracts.schema_invariants import _add_raes_invariant from .diagnostics import DiagnosticModel from .satisfiability import SourceArtifactIdentityModel, canonical_contract_digest @@ -94,7 +94,7 @@ class AttackGraphBindingModel(ContractModel): binding_id: ExploitPathId concept_kind: str = Field(min_length=1, max_length=128, pattern=r"^[a-z][a-z0-9-]*$") - aces_address: JsonPointer + raes_address: JsonPointer target_kind: Literal["state-fact", "transition"] target_id: ExploitPathId @@ -102,9 +102,9 @@ class AttackGraphBindingModel(ContractModel): class NormalizedAttackGraphModel(ContractModel): """Closed normalized attack graph derived from governed RAES bindings.""" - profile: Literal["aces-attack-graph/v1"] - binding_profile: Literal["aces-sdl-snapshot-attack-binding/v1"] - transition_semantics_profile: Literal["aces-monotonic-attack-transition/v1"] + profile: Literal["raes-attack-graph/v1"] + binding_profile: Literal["raes-sdl-snapshot-attack-binding/v1"] + transition_semantics_profile: Literal["raes-monotonic-attack-transition/v1"] snapshot_digest: PrefixedDigestString state_facts: tuple[AttackStateFactModel, ...] = Field(min_length=1, max_length=512) transitions: tuple[AttackTransitionModel, ...] = Field(max_length=512) @@ -130,7 +130,7 @@ def _validate_graph_shape(self) -> NormalizedAttackGraphModel: class ExploitPathQueryModel(ContractModel): """Closed path query contract.""" - profile: Literal["aces-exploit-path-query/v1"] + profile: Literal["raes-exploit-path-query/v1"] query_id: ExploitPathId participant_perspective: str = Field(min_length=1, max_length=256, pattern=r"^[^\r\n]+$") start_facts: tuple[ExploitPathId, ...] = Field(min_length=1, max_length=64) @@ -151,7 +151,7 @@ class ExploitPathAnalysisInputModel(ContractModel): """Production input: an admitted snapshot plus normalized graph and query.""" profile: Literal["exploit-path-analysis-input/v1"] - analysis_profile: Literal["aces-exploit-path-analysis-v1"] + analysis_profile: Literal["raes-exploit-path-analysis-v1"] snapshot: InstantiatedScenarioSnapshot snapshot_digest: PrefixedDigestString normalized_graph: NormalizedAttackGraphModel @@ -170,7 +170,7 @@ def _validate_snapshot_join(self) -> ExploitPathAnalysisInputModel: class ExploitPathSearchConfigurationModel(ContractModel): """Complete output-affecting v1 search configuration.""" - profile: Literal["aces-deterministic-attack-graph-search/v1"] + profile: Literal["raes-deterministic-attack-graph-search/v1"] strategy: Literal["breadth-first-canonical"] transition_semantics: Literal["monotonic-additive/v1"] goal_check: Literal["before-and-after-transition"] @@ -199,7 +199,7 @@ def _validate_step_order(self) -> ExploitPathStepModel: class ValidPathWitnessModel(ContractModel): """Replayable valid path witness for the exact graph and query.""" - profile: Literal["aces-exploit-path-witness/v1"] + profile: Literal["raes-exploit-path-witness/v1"] initial_state: tuple[ExploitPathId, ...] = Field(min_length=1, max_length=512) goal_facts: tuple[ExploitPathId, ...] = Field(min_length=1, max_length=64) steps: tuple[ExploitPathStepModel, ...] = Field(max_length=64) @@ -234,7 +234,7 @@ def _validate_missing_order(self) -> BlockedTransitionModel: class InvalidPathEvidenceModel(ContractModel): """Structured evidence for an invalid path query.""" - profile: Literal["aces-exploit-path-invalid/v1"] + profile: Literal["raes-exploit-path-invalid/v1"] explored_state_count: int = Field(ge=1, le=MAX_EXPLOIT_PATH_EXPLORED_STATES) exhausted_depth: bool blocked_transitions: tuple[BlockedTransitionModel, ...] = Field(max_length=MAX_EXPLOIT_PATH_BLOCKED_TRANSITIONS) @@ -253,7 +253,7 @@ def _validate_failure_order(self) -> InvalidPathEvidenceModel: class UnsupportedExploitPathAnalysisModel(ContractModel): """Fail-closed unsupported reason set.""" - profile: Literal["aces-exploit-path-unsupported/v1"] + profile: Literal["raes-exploit-path-unsupported/v1"] reason_codes: tuple[str, ...] = Field(min_length=1, max_length=64) @model_validator(mode="after") @@ -266,7 +266,7 @@ class ExploitPathAnalysisEvidenceModel(ContractModel): """Closed evidence envelope binding source, snapshot, graph, query, and result.""" profile: Literal["exploit-path-analysis-evidence/v1"] - analysis_profile: Literal["aces-exploit-path-analysis-v1"] + analysis_profile: Literal["raes-exploit-path-analysis-v1"] source: SourceArtifactIdentityModel authored_digest: SemanticDigest snapshot_digest: PrefixedDigestString @@ -335,7 +335,7 @@ def __get_pydantic_json_schema__( }, ] ) - _add_aces_invariant( + _add_raes_invariant( json_schema, "exploit-path-evidence-digest-joins", "The snapshot, normalized graph, query, search configuration, witness final state, and unsupported " diff --git a/implementations/python/packages/raes_contracts/provenance.py b/implementations/python/packages/raes_contracts/provenance.py index 927522ca5..33da63d13 100644 --- a/implementations/python/packages/raes_contracts/provenance.py +++ b/implementations/python/packages/raes_contracts/provenance.py @@ -36,7 +36,7 @@ class LineageClassification(str, Enum): ADOPTED_SYNTAX = "adopted_syntax" ADOPTED_SEMANTICS = "adopted_semantics" ADAPTED = "adapted" - ACES_NATIVE = "aces_native" + RAES_NATIVE = "raes_native" class LineageDisposition(str, Enum): @@ -53,7 +53,7 @@ class CompatibilityStatus(str, Enum): class CompatibilityDirection(str, Enum): - ACES_RELATIVE_TO_SOURCE = "aces_relative_to_source" + RAES_RELATIVE_TO_SOURCE = "raes_relative_to_source" NOT_APPLICABLE = "not_applicable" @@ -130,7 +130,7 @@ class LineageClaimModel(ContractModel): plane: LineagePlane classification: LineageClassification source_refs: list[NonEmptyString] = Field(default_factory=list) - aces_boundaries: list[ArtifactBoundaryModel] = Field(min_length=1) + raes_boundaries: list[ArtifactBoundaryModel] = Field(min_length=1) source_boundaries: list[ArtifactBoundaryModel] = Field(default_factory=list) divergence: NonEmptyString compatibility: CompatibilityStatus @@ -140,7 +140,7 @@ class LineageClaimModel(ContractModel): @model_validator(mode="after") def validate_claim_dimensions(self) -> LineageClaimModel: - if self.classification is LineageClassification.ACES_NATIVE: + if self.classification is LineageClassification.RAES_NATIVE: self._validate_native_dimensions() else: self._validate_external_dimensions() @@ -163,7 +163,7 @@ def _validate_external_dimensions(self) -> None: raise ValueError("adopted_syntax is valid only on the syntax plane") if self.classification is LineageClassification.ADOPTED_SEMANTICS and self.plane is not LineagePlane.SEMANTICS: raise ValueError("adopted_semantics is valid only on the semantics plane") - if self.compatibility_direction is not CompatibilityDirection.ACES_RELATIVE_TO_SOURCE: + if self.compatibility_direction is not CompatibilityDirection.RAES_RELATIVE_TO_SOURCE: raise ValueError("non-native claims assess RAES relative to the named source") @@ -318,7 +318,7 @@ def _validate_artifact_code_claim( raise ValueError(f"subject {subject_id!r} artifact/code claim lacks notice disposition") if not claim_sources.issubset(resolved_disposition_sources): raise ValueError(f"subject {subject_id!r} artifact/code claim has unresolved notice disposition") - claim_artifacts = {boundary.artifact for boundary in claim.aces_boundaries} + claim_artifacts = {boundary.artifact for boundary in claim.raes_boundaries} for source_ref in claim.source_refs: uncovered = claim_artifacts - disposition_artifacts.get(source_ref, set()) if uncovered: diff --git a/implementations/python/packages/raes_contracts/random_stream_engine.py b/implementations/python/packages/raes_contracts/random_stream_engine.py index 56c005ed5..ae0e5fd3b 100644 --- a/implementations/python/packages/raes_contracts/random_stream_engine.py +++ b/implementations/python/packages/raes_contracts/random_stream_engine.py @@ -9,7 +9,7 @@ Construction, verbatim from ``docs/decisions/issue-274-exp-718-controlled-randomness-preflight.md`` and the implementation plan: -* ``stream_key = blake3.derive_key(context=f"aces-random-stream-v1|profile={profile_id}", +* ``stream_key = blake3.derive_key(context=f"raes-random-stream-v1|profile={profile_id}", key_material=root_entropy_bytes)`` -- one call per (profile, root entropy) pair; pure function, no shared state. (This binding's Python API exposes the same key-derivation mode via ``blake3.blake3(derive_key_context=...)``.) @@ -50,7 +50,7 @@ #: Key-derivation domain-separation context template fixed by the #: ``blake3-xof-v1`` profile. ``{profile_id}`` is substituted with the exact #: requested profile id -- changing this template mints a new profile id. -DERIVATION_CONTEXT_TEMPLATE = "aces-random-stream-v1|profile={profile_id}" +DERIVATION_CONTEXT_TEMPLATE = "raes-random-stream-v1|profile={profile_id}" BOUNDED_INTEGER_TRANSFORM_ID = "bounded-integer" BOUNDED_INTEGER_TRANSFORM_VERSION = "1" diff --git a/implementations/python/packages/raes_contracts/realization_envelope_carrier.py b/implementations/python/packages/raes_contracts/realization_envelope_carrier.py index ce688ee28..c3dc4fc0d 100644 --- a/implementations/python/packages/raes_contracts/realization_envelope_carrier.py +++ b/implementations/python/packages/raes_contracts/realization_envelope_carrier.py @@ -278,7 +278,7 @@ def __get_pydantic_json_schema__( } for concern in RealizationConcern ] - json_schema["x-aces-invariants"] = [ + json_schema["x-raes-invariants"] = [ { "id": "realization-envelope-canonical-semantics-valid", "description": ( diff --git a/implementations/python/packages/raes_contracts/satisfiability.py b/implementations/python/packages/raes_contracts/satisfiability.py index 4876bc624..559434adb 100644 --- a/implementations/python/packages/raes_contracts/satisfiability.py +++ b/implementations/python/packages/raes_contracts/satisfiability.py @@ -107,9 +107,9 @@ def _validate_values(self) -> ConstraintClauseModel: class NormalizedConstraintModel(ContractModel): """Solver-neutral normalized RAES finite-domain constraint model.""" - profile: Literal["aces-finite-domain-constraints/v1"] - theory_profile: Literal["aces-finite-domain-theory/v1"] - translation_profile: Literal["aces-sdl-authoring-translation/v1"] + profile: Literal["raes-finite-domain-constraints/v1"] + theory_profile: Literal["raes-finite-domain-theory/v1"] + translation_profile: Literal["raes-sdl-authoring-translation/v1"] source_digest: PrefixedDigestString authored_digest: SemanticDigest symbols: tuple[ConstraintSymbolModel, ...] = Field(max_length=128) @@ -137,7 +137,7 @@ def _validate_graph(self) -> NormalizedConstraintModel: class SolverConfigurationModel(ContractModel): """Complete output-affecting configuration for the pinned v1 Z3 adapter.""" - profile: Literal["aces-z3-finite-domain/v1"] + profile: Literal["raes-z3-finite-domain/v1"] engine: Literal["z3"] package: Literal["z3-solver"] package_version: Literal["4.16.0.0"] # NOSONAR -- pinned package version, not an IP address @@ -156,7 +156,7 @@ class SolverConfigurationModel(ContractModel): class SatisfiableWitnessModel(ContractModel): """Independently admitted canonical instantiation witness.""" - profile: Literal["aces-satisfiability-witness/v1"] + profile: Literal["raes-satisfiability-witness/v1"] snapshot: InstantiatedScenarioSnapshot snapshot_digest: PrefixedDigestString @@ -173,7 +173,7 @@ def _validate_snapshot(self) -> SatisfiableWitnessModel: class UnsatisfiableCoreModel(ContractModel): """Governed subset-minimal clause set, not a proof certificate.""" - profile: Literal["aces-unsatisfiable-core/v1"] + profile: Literal["raes-unsatisfiable-core/v1"] clause_ids: tuple[ClauseId, ...] = Field(min_length=1, max_length=512) minimality: Literal["subset-minimal"] @@ -187,7 +187,7 @@ def _validate_order(self) -> UnsatisfiableCoreModel: class UnsupportedAnalysisModel(ContractModel): """Stable fail-closed reason set for an unsupported analysis.""" - profile: Literal["aces-satisfiability-unsupported/v1"] + profile: Literal["raes-satisfiability-unsupported/v1"] reason_codes: tuple[str, ...] = Field(min_length=1, max_length=64) @model_validator(mode="after") @@ -201,7 +201,7 @@ class ScenarioSatisfiabilityEvidenceModel(ContractModel): """Closed evidence envelope binding source, model, solver, and result.""" profile: Literal["scenario-satisfiability-evidence/v1"] - analysis_profile: Literal["aces-finite-domain-satisfiability-v1"] + analysis_profile: Literal["raes-finite-domain-satisfiability-v1"] source: SourceArtifactIdentityModel authored_digest: SemanticDigest imports: tuple[ResolvedImportProvenance, ...] = () diff --git a/implementations/python/packages/raes_contracts/validation_profiles.py b/implementations/python/packages/raes_contracts/validation_profiles.py index b9cf94648..4748813e0 100644 --- a/implementations/python/packages/raes_contracts/validation_profiles.py +++ b/implementations/python/packages/raes_contracts/validation_profiles.py @@ -89,7 +89,7 @@ def validate_local_sets(self) -> ValidationProfileDefinitionModel: class ValidationProfileCatalogModel(ContractModel): schema_version: Literal[VALIDATION_PROFILE_CATALOG_SCHEMA_VERSION] = VALIDATION_PROFILE_CATALOG_SCHEMA_VERSION - profile_family: Literal["aces-validation"] = "aces-validation" + profile_family: Literal["raes-validation"] = "raes-validation" catalog_version: CatalogVersion strengths: list[ValidationStrengthDefinitionModel] = Field(min_length=1) subject_kinds: list[ValidationSubjectKindDefinitionModel] = Field(min_length=1) diff --git a/implementations/python/packages/raes_mcp/tools/authoring.py b/implementations/python/packages/raes_mcp/tools/authoring.py index 47fda5e3b..d7926bb34 100644 --- a/implementations/python/packages/raes_mcp/tools/authoring.py +++ b/implementations/python/packages/raes_mcp/tools/authoring.py @@ -410,7 +410,7 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: predicate: kind: boolean property: service-healthy - semantic_ref: urn:aces:observable:service-healthy + semantic_ref: urn:raes:observable:service-healthy operator: equals expected: true evidence_requirements: [web-health-evidence] @@ -556,7 +556,7 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: predicate: kind: boolean property: service-healthy - semantic_ref: urn:aces:observable:service-healthy + semantic_ref: urn:raes:observable:service-healthy operator: equals expected: true evidence_requirements: [web-health-evidence] diff --git a/implementations/python/packages/raes_mcp/tools/reference.py b/implementations/python/packages/raes_mcp/tools/reference.py index 15636cc6d..04523d62e 100644 --- a/implementations/python/packages/raes_mcp/tools/reference.py +++ b/implementations/python/packages/raes_mcp/tools/reference.py @@ -303,7 +303,7 @@ def sdl_validation_reference() -> str: predicate: kind: presence property: service - semantic_ref: urn:aces:declared-property:service + semantic_ref: urn:raes:declared-property:service operator: exists assertions: diff --git a/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py b/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py index 14f952a01..b7dbafea8 100644 --- a/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py +++ b/implementations/python/packages/raes_operations/_cross_backend_corpus_backend_runs.py @@ -12,7 +12,7 @@ inspect payloads, upstream Wazuh rule bodies) never enter a descriptor -- see the issue #600 preflight redaction gate. -The libvirt descriptor is extracted from the real ``aces.libvirt.scenario-evidence-run/v1`` +The libvirt descriptor is extracted from the real ``raes.libvirt.scenario-evidence-run/v1`` artifact (issue #615) and marked ``generated-in-repo``. The APTL descriptor is a bounded summary of the publicly documented APTL realization (``examples/scenarios/enterprise-participant-evidence-loop.README.md`` + Brad-Edwards/aptl#558) marked @@ -108,7 +108,7 @@ def build_libvirt_backend_run(artifact: Mapping[str, Any]) -> dict[str, Any]: Copies only portable, timestamp-free fields so the descriptor (and therefore the corpus) is byte-stable across runs; the full timestamped evidence stays in the - regenerable ``aces.libvirt.scenario-evidence-run/v1`` artifact. + regenerable ``raes.libvirt.scenario-evidence-run/v1`` artifact. """ backend = _mapping(artifact.get("backend")) compiled = _mapping(artifact.get("compiled_artifact")) @@ -116,7 +116,7 @@ def build_libvirt_backend_run(artifact: Mapping[str, Any]) -> dict[str, Any]: proof = _mapping(artifact.get("participant_action_proof")) return { "backend_id": "libvirt-reference", - "realization": "aces-libvirt-reference-backend", + "realization": "raes-libvirt-reference-backend", "evidence_source_mode": str(artifact.get("evidence_source_mode", "deterministic")), "evidence_provenance": "generated-in-repo", "evidence_locator": { diff --git a/implementations/python/packages/raes_operations/_cross_backend_corpus_validation.py b/implementations/python/packages/raes_operations/_cross_backend_corpus_validation.py index cf05f2c9a..21b9d3ddb 100644 --- a/implementations/python/packages/raes_operations/_cross_backend_corpus_validation.py +++ b/implementations/python/packages/raes_operations/_cross_backend_corpus_validation.py @@ -15,7 +15,7 @@ from raes_operations._evidence_run_validation import redaction_violations -CORPUS_SCHEMA = "aces.cross-backend-evidence-corpus/v1" +CORPUS_SCHEMA = "raes.cross-backend-evidence-corpus/v1" _REQUIRED_SECTIONS: tuple[str, ...] = ( "authored_scenario", diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact.py b/implementations/python/packages/raes_operations/_evidence_run_artifact.py index f617b7e02..9bc056b94 100644 --- a/implementations/python/packages/raes_operations/_evidence_run_artifact.py +++ b/implementations/python/packages/raes_operations/_evidence_run_artifact.py @@ -1,6 +1,6 @@ """Artifact assembly for the libvirt scenario-evidence producer. -Builds the ``aces.libvirt.scenario-evidence-run/v1`` payload from the compiled runtime +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 @@ -42,7 +42,7 @@ ) from raes_operations.run_artifacts import portable_artifact_ref -EVIDENCE_RUN_SCHEMA = "aces.libvirt.scenario-evidence-run/v1" +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 @@ -175,7 +175,7 @@ def _compiled_artifact_section(model: CompiledModel) -> dict[str, Any]: } fingerprint = hashlib.sha256(json.dumps(addresses, sort_keys=True).encode("utf-8")).hexdigest() return { - "processor": "aces-reference-processor", + "processor": "raes-reference-processor", "compiled_address_sets": addresses, "compiled_model_fingerprint": "sha256:" + fingerprint, } @@ -355,7 +355,7 @@ def _terminal_observation_section(snapshot: TerminalSnapshot) -> dict[str, Any]: return { "form": "participant-projected-history", "taxonomy": { - "taxonomy_id": "aces-behavioral-relations", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "non_claimed_relation_ids": [ "participant-projected-history-equivalence", diff --git a/implementations/python/packages/raes_operations/_evidence_run_native.py b/implementations/python/packages/raes_operations/_evidence_run_native.py index e97c726fe..1f7689b28 100644 --- a/implementations/python/packages/raes_operations/_evidence_run_native.py +++ b/implementations/python/packages/raes_operations/_evidence_run_native.py @@ -84,12 +84,12 @@ def factory() -> TechVaultNativeLibvirtDriver: return GuestCertifiedLibvirtDriver( state_dir=state_dir, connection_uri=settings.connection_uri, - name_prefix="aces-evidence", + name_prefix="raes-evidence", ) return TechVaultNativeLibvirtDriver( state_dir=state_dir, connection_uri=settings.connection_uri, - name_prefix="aces-evidence", + name_prefix="raes-evidence", ) return factory diff --git a/implementations/python/packages/raes_operations/cross_backend_corpus.py b/implementations/python/packages/raes_operations/cross_backend_corpus.py index 7290b2069..660f28769 100644 --- a/implementations/python/packages/raes_operations/cross_backend_corpus.py +++ b/implementations/python/packages/raes_operations/cross_backend_corpus.py @@ -3,10 +3,10 @@ Assembles the backend-paired demonstration corpus for the RAES reference scenario: one libvirt reference-backend realization and one APTL realization of the *same authored scenario*, compared through an inspectable cross-backend invariant -ledger (``aces.cross-backend-evidence-corpus/v1``). +ledger (``raes.cross-backend-evidence-corpus/v1``). The corpus is a thin **local** artifact that composes existing surfaces (issue #600 -preflight): it consumes the real ``aces.libvirt.scenario-evidence-run/v1`` artifact +preflight): it consumes the real ``raes.libvirt.scenario-evidence-run/v1`` artifact through ``run_libvirt_evidence_run`` (deterministic mode -- no libvirt daemon) and records the APTL realization as a bounded, honestly-labeled summary + link to Brad-Edwards/aptl#558 (or, when an operator supplies one, its allowlisted portable diff --git a/implementations/python/packages/raes_operations/deterministic_participant_fixtures.py b/implementations/python/packages/raes_operations/deterministic_participant_fixtures.py index 1ad8afcc2..d683d3221 100644 --- a/implementations/python/packages/raes_operations/deterministic_participant_fixtures.py +++ b/implementations/python/packages/raes_operations/deterministic_participant_fixtures.py @@ -65,7 +65,7 @@ def build_implementation_manifest() -> ParticipantImplementationManifestModel: ], "compatibility": { "participant_runtimes": ["libvirt-qemu"], - "processors": ["aces-reference-processor"], + "processors": ["raes-reference-processor"], "backends": ["libvirt-qemu"], }, "concept_bindings": [ diff --git a/implementations/python/packages/raes_operations/libvirt_evidence_run.py b/implementations/python/packages/raes_operations/libvirt_evidence_run.py index fb3f474f7..78a2cd741 100644 --- a/implementations/python/packages/raes_operations/libvirt_evidence_run.py +++ b/implementations/python/packages/raes_operations/libvirt_evidence_run.py @@ -3,7 +3,7 @@ Composes existing RAES surfaces — the libvirt participant runtime (issue #614, via the runtime control plane), the native libvirt substrate realization (issue #601), the backend manifest, and the experiment/evaluation contracts — into one stable, -validated run artifact (``aces.libvirt.scenario-evidence-run/v1``) that carries +validated run artifact (``raes.libvirt.scenario-evidence-run/v1``) that carries evaluator-facing evidence for the enterprise participant/evidence scenario. The artifact is a local proof-artifact wrapper that embeds validated published-contract payloads and bounded summaries; it is NOT a new published diff --git a/implementations/python/packages/raes_operations/techvault_live.py b/implementations/python/packages/raes_operations/techvault_live.py index e9c3de160..eede457d9 100644 --- a/implementations/python/packages/raes_operations/techvault_live.py +++ b/implementations/python/packages/raes_operations/techvault_live.py @@ -26,7 +26,7 @@ run_artifact_path, ) -_LIVE_SCHEMA = "aces.libvirt.techvault-native-live-gate/v1" +_LIVE_SCHEMA = "raes.libvirt.techvault-native-live-gate/v1" _SHA256_RE = re.compile(r"sha256:[a-f0-9]{64}") @@ -97,10 +97,10 @@ def validate_techvault_live( else TechVaultNativeLibvirtDriver( state_dir=run_dir / "libvirt", connection_uri=settings.connection_uri, - name_prefix="aces-techvault", + name_prefix="raes-techvault", ) ) - target = create_libvirt_target(driver=driver, name_prefix="aces-techvault") + target = create_libvirt_target(driver=driver, name_prefix="raes-techvault") scenario, plan_check = _plan_scenario(target, scenario_path) del scenario checks.append(plan_check) @@ -171,7 +171,7 @@ def _apply_plan(target: object, scenario_path: Path, driver: TechVaultNativeLibv diagnostics = ("native libvirt driver returned no domain snapshot",) else: passed = True - return LiveCheck("aces_libvirt_native_boot", passed, diagnostics) + return LiveCheck("raes_libvirt_native_boot", passed, diagnostics) def _substrate_independence_check(snapshot: Mapping[str, Any]) -> LiveCheck: @@ -205,7 +205,7 @@ def _write_manifest( checks: Sequence[LiveCheck], ) -> str | None: target = run_artifact_path(output_dir, run_id, "live-gate", "manifest.json") - native_succeeded = any(check.name == "aces_libvirt_native_boot" and check.passed for check in checks) + native_succeeded = any(check.name == "raes_libvirt_native_boot" and check.passed for check in checks) observed_snapshot = snapshot if native_succeeded else {} native_surface = expected_surface(observed_snapshot) cleanup_check = next((check for check in checks if check.name == "verified_native_cleanup"), None) diff --git a/implementations/python/packages/raes_processor/exploit_path/_preflight.py b/implementations/python/packages/raes_processor/exploit_path/_preflight.py index e83f22d60..1c9b1fdfe 100644 --- a/implementations/python/packages/raes_processor/exploit_path/_preflight.py +++ b/implementations/python/packages/raes_processor/exploit_path/_preflight.py @@ -65,11 +65,11 @@ def _binding_diagnostics( "The binding target does not resolve in the normalized attack graph.", ) ) - if not _pointer_resolves(snapshot_payload, binding.aces_address): + if not _pointer_resolves(snapshot_payload, binding.raes_address): diagnostics.append( diagnostic( BINDING_MISSING_CODE, - f"/normalized_graph/bindings/{pointer(binding.binding_id)}/aces_address", + f"/normalized_graph/bindings/{pointer(binding.binding_id)}/raes_address", "The binding address does not resolve in the admitted scenario snapshot.", ) ) diff --git a/implementations/python/packages/raes_processor/exploit_path/_search.py b/implementations/python/packages/raes_processor/exploit_path/_search.py index 037165b3e..22d88c1d1 100644 --- a/implementations/python/packages/raes_processor/exploit_path/_search.py +++ b/implementations/python/packages/raes_processor/exploit_path/_search.py @@ -266,7 +266,7 @@ def _invalid_search_result(goal: frozenset[str], accumulator: _SearchAccumulator outcome=ExploitPathOutcome.INVALID_PATH, witness=None, failure=InvalidPathEvidenceModel( - profile="aces-exploit-path-invalid/v1", + profile="raes-exploit-path-invalid/v1", explored_state_count=len(accumulator.visited), exhausted_depth=accumulator.exhausted_depth, blocked_transitions=tuple( @@ -323,7 +323,7 @@ def _witness( ) -> ValidPathWitnessModel: state = tuple(sorted(final_state if final_state is not None else initial_state)) return ValidPathWitnessModel( - profile="aces-exploit-path-witness/v1", + profile="raes-exploit-path-witness/v1", initial_state=tuple(sorted(initial_state)), goal_facts=tuple(sorted(goal_facts)), steps=steps, @@ -339,7 +339,7 @@ def _unsupported_search_result(address: str, message: str) -> SearchResult: witness=None, failure=None, unsupported=UnsupportedExploitPathAnalysisModel( - profile="aces-exploit-path-unsupported/v1", + profile="raes-exploit-path-unsupported/v1", reason_codes=(BOUND_EXCEEDED_CODE,), ), diagnostics=(diagnostic_item,), diff --git a/implementations/python/packages/raes_processor/exploit_path/_service.py b/implementations/python/packages/raes_processor/exploit_path/_service.py index 35e3d9a52..31a36420a 100644 --- a/implementations/python/packages/raes_processor/exploit_path/_service.py +++ b/implementations/python/packages/raes_processor/exploit_path/_service.py @@ -25,7 +25,7 @@ from ._preflight import preflight_diagnostics from ._search import search -ANALYSIS_PROFILE = "aces-exploit-path-analysis-v1" +ANALYSIS_PROFILE = "raes-exploit-path-analysis-v1" _MAX_INPUT_BYTES = 2 * 1024 * 1024 @@ -73,7 +73,7 @@ def analyze_exploit_path_input( diagnostics = preflight_diagnostics(request) search_configuration = ExploitPathSearchConfigurationModel( - profile="aces-deterministic-attack-graph-search/v1", + profile="raes-deterministic-attack-graph-search/v1", strategy="breadth-first-canonical", transition_semantics=SUPPORTED_TRANSITION_SEMANTICS, goal_check=request.query.goal_check, @@ -99,7 +99,7 @@ def analyze_exploit_path_input( outcome=ExploitPathOutcome.UNSUPPORTED, diagnostics=tuple(sorted(fail_closed, key=lambda item: (item.address, item.code))), unsupported=UnsupportedExploitPathAnalysisModel( - profile="aces-exploit-path-unsupported/v1", + profile="raes-exploit-path-unsupported/v1", reason_codes=tuple(sorted({item.code for item in fail_closed})), ), ) diff --git a/implementations/python/packages/raes_processor/manifest.py b/implementations/python/packages/raes_processor/manifest.py index e85d5d59c..357c97cd2 100644 --- a/implementations/python/packages/raes_processor/manifest.py +++ b/implementations/python/packages/raes_processor/manifest.py @@ -18,7 +18,7 @@ from raes_processor.capabilities import ProcessorCapabilitySet, ProcessorManifest -REFERENCE_PROCESSOR_NAME = "aces-reference-processor" +REFERENCE_PROCESSOR_NAME = "raes-reference-processor" REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2 = ( "processor-manifest-v2", "provisioning-plan-v1", diff --git a/implementations/python/packages/raes_processor/satisfiability/_service.py b/implementations/python/packages/raes_processor/satisfiability/_service.py index e07e6f334..3d677f9fd 100644 --- a/implementations/python/packages/raes_processor/satisfiability/_service.py +++ b/implementations/python/packages/raes_processor/satisfiability/_service.py @@ -29,7 +29,7 @@ from ._solver import SolverOperationalError, solve_model from ._translation import translate_scenario -ANALYSIS_PROFILE = "aces-finite-domain-satisfiability-v1" +ANALYSIS_PROFILE = "raes-finite-domain-satisfiability-v1" class SatisfiabilityEvidenceError(ValueError): @@ -71,7 +71,7 @@ def analyze_scenario_file( outcome=SatisfiabilityOutcome.UNSUPPORTED, diagnostics=translation.diagnostics, unsupported=UnsupportedAnalysisModel( - profile="aces-satisfiability-unsupported/v1", + profile="raes-satisfiability-unsupported/v1", reason_codes=tuple(sorted({item.code for item in translation.diagnostics})), ), ) @@ -95,7 +95,7 @@ def analyze_scenario_file( outcome=result.outcome, diagnostics=(), witness=SatisfiableWitnessModel( - profile="aces-satisfiability-witness/v1", + profile="raes-satisfiability-witness/v1", snapshot=snapshot, snapshot_digest=canonical_instantiated_sdl_digest(instantiated).value, ), @@ -106,7 +106,7 @@ def analyze_scenario_file( outcome=result.outcome, diagnostics=(), unsat_core=UnsatisfiableCoreModel( - profile="aces-unsatisfiable-core/v1", + profile="raes-unsatisfiable-core/v1", clause_ids=result.core, minimality="subset-minimal", ), @@ -134,7 +134,7 @@ def _solver_configuration() -> SolverConfigurationModel: engine_version = z3.get_version_string() try: return SolverConfigurationModel( - profile="aces-z3-finite-domain/v1", + profile="raes-z3-finite-domain/v1", engine="z3", package="z3-solver", package_version=package_version, diff --git a/implementations/python/packages/raes_processor/satisfiability/_solver.py b/implementations/python/packages/raes_processor/satisfiability/_solver.py index 70554d585..41ecb4419 100644 --- a/implementations/python/packages/raes_processor/satisfiability/_solver.py +++ b/implementations/python/packages/raes_processor/satisfiability/_solver.py @@ -105,4 +105,4 @@ def _scalar_equal(left: object, right: object) -> bool: def _z3_name(value: str) -> str: - return "aces_" + value.encode("utf-8").hex() + return "raes_" + value.encode("utf-8").hex() diff --git a/implementations/python/packages/raes_processor/satisfiability/_translation.py b/implementations/python/packages/raes_processor/satisfiability/_translation.py index 5df631d3e..9e1bc055c 100644 --- a/implementations/python/packages/raes_processor/satisfiability/_translation.py +++ b/implementations/python/packages/raes_processor/satisfiability/_translation.py @@ -90,9 +90,9 @@ def translate_scenario( clauses = _translate_clauses(symbols, occurrences, diagnostics) model = NormalizedConstraintModel( - profile="aces-finite-domain-constraints/v1", - theory_profile="aces-finite-domain-theory/v1", - translation_profile="aces-sdl-authoring-translation/v1", + profile="raes-finite-domain-constraints/v1", + theory_profile="raes-finite-domain-theory/v1", + translation_profile="raes-sdl-authoring-translation/v1", source_digest=source_digest, authored_digest=canonical_sdl_digest(scenario).as_dict(), symbols=tuple(sorted(symbols.values(), key=lambda item: item.symbol_id)), diff --git a/implementations/python/packages/raes_reference_backend/drivers/oci.py b/implementations/python/packages/raes_reference_backend/drivers/oci.py index a84bf2bad..61f5a7715 100644 --- a/implementations/python/packages/raes_reference_backend/drivers/oci.py +++ b/implementations/python/packages/raes_reference_backend/drivers/oci.py @@ -49,7 +49,7 @@ _CODE_NETWORK_NAMESPACE_CONFLICT = "reference-backend.driver.network-namespace-conflict" _OWNERSHIP_INSPECT_FORMAT = ( - '{{.Id}}\n{{index .Config.Labels "aces.workspace"}}\n{{index .Config.Labels "aces.address"}}\n{{.Name}}' + '{{.Id}}\n{{index .Config.Labels "raes.workspace"}}\n{{index .Config.Labels "raes.address"}}\n{{.Name}}' ) _KIND_TO_CODE = { @@ -83,9 +83,9 @@ class ImageTrustPolicy: allow_digest_pinned: bool = True def image_for(self, image_ref: str) -> str: - # A configured default overrides the synthesized ``aces-reference/*`` + # A configured default overrides the synthesized ``raes-reference/*`` # placeholder so an image-less plan can still realize against a registry. - if self.default_image and image_ref.startswith("aces-reference/"): + if self.default_image and image_ref.startswith("raes-reference/"): return self.default_image return image_ref @@ -139,9 +139,9 @@ def __init__( def _label_args(self, address: str) -> list[str]: return [ "--label", - f"aces.workspace={self._workspace}", + f"raes.workspace={self._workspace}", "--label", - f"aces.address={address}", + f"raes.address={address}", ] def _invoke(self, argv: list[str]) -> tuple[bool, str | None, str]: @@ -205,7 +205,7 @@ def _realize_networks( ) -> list[NetworkHandle]: handles: list[NetworkHandle] = [] for spec in networks: - runtime_name = provider_resource_name(spec.address, prefix="aces") + runtime_name = provider_resource_name(spec.address, prefix="raes") argv = [self._runtime, "network", "create", *self._label_args(spec.address), runtime_name] ok, kind = self._run(argv) if ok: @@ -231,7 +231,7 @@ def _realize_containers( if not self._image_policy.permits(image): diagnostics.append(self._image_rejected(spec.address)) continue - runtime_name = provider_resource_name(spec.address, prefix="aces") + runtime_name = provider_resource_name(spec.address, prefix="raes") argv = self._container_run_argv(spec, runtime_name=runtime_name, image=image) ok, kind, native_stdout = self._invoke(argv) if ok: @@ -379,7 +379,7 @@ def _rollback( self.destroy(networks=realized_networks, containers=realized_containers) def _name_for(self, address: str) -> str: - return self._names.get(address, provider_resource_name(address, prefix="aces")) + return self._names.get(address, provider_resource_name(address, prefix="raes")) def destroy( self, diff --git a/implementations/python/packages/raes_reference_backend/manifest.py b/implementations/python/packages/raes_reference_backend/manifest.py index 27d05e091..9cd7ce4fc 100644 --- a/implementations/python/packages/raes_reference_backend/manifest.py +++ b/implementations/python/packages/raes_reference_backend/manifest.py @@ -273,7 +273,7 @@ def create_reference_backend_manifest(*, with_time: bool = False, **config) -> B if with_time else REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS - _TIME_DEDICATED_CONTRACT_VERSIONS ), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), concept_bindings=_concept_bindings(with_time=with_time), realization_support=_realization_support(), capabilities=_capabilities(with_time=with_time), diff --git a/implementations/python/packages/raes_reference_backend/realization.py b/implementations/python/packages/raes_reference_backend/realization.py index d23cfed6b..f0d17b6f3 100644 --- a/implementations/python/packages/raes_reference_backend/realization.py +++ b/implementations/python/packages/raes_reference_backend/realization.py @@ -118,7 +118,7 @@ def _resource_name(resource: PlannedResource, payload: Mapping[str, object]) -> name = payload.get("name") or payload.get("node_name") if isinstance(name, str) and name: return name - return provider_resource_name(resource.address, prefix="aces") + return provider_resource_name(resource.address, prefix="raes") def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: @@ -246,8 +246,8 @@ def _image_ref(payload: Mapping[str, object]) -> str: return source os_family = payload.get("os_family") if isinstance(os_family, str) and os_family: - return f"aces-reference/{os_family}" - return "aces-reference/base" + return f"raes-reference/{os_family}" + return "raes-reference/base" def _node_source(payload: Mapping[str, object]) -> str | None: diff --git a/implementations/python/packages/raes_runtime/control_plane_security.py b/implementations/python/packages/raes_runtime/control_plane_security.py index fea491caa..2de070655 100644 --- a/implementations/python/packages/raes_runtime/control_plane_security.py +++ b/implementations/python/packages/raes_runtime/control_plane_security.py @@ -45,8 +45,8 @@ class ControlPlaneSecurityConfig: """ require_verified_identity: bool = True - verified_header: str = "x-aces-client-verified" - identity_header: str = "x-aces-client-identity" + verified_header: str = "x-raes-client-verified" + identity_header: str = "x-raes-client-identity" trust_proxy_identity_headers: bool = False max_request_bytes: int = 1_000_000 trusted_identities: dict[str, ControlPlaneIdentity] = field(default_factory=dict) diff --git a/implementations/python/packages/raes_runtime/participant_clock_driver.py b/implementations/python/packages/raes_runtime/participant_clock_driver.py index 32af2170b..ee8ed9c69 100644 --- a/implementations/python/packages/raes_runtime/participant_clock_driver.py +++ b/implementations/python/packages/raes_runtime/participant_clock_driver.py @@ -85,7 +85,7 @@ def start(self) -> None: self._stop.clear() self._thread = threading.Thread( target=self._run, - name="aces-participant-clock-driver", + name="raes-participant-clock-driver", daemon=True, ) self._thread.start() diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index 6ed8c3e94..9b5b9bf03 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "packaging>=23.0", "rfc8785>=0.1.4,<0.2", "blake3>=1.0.8,<2", - # Pinned to the governed aces-z3-finite-domain/v1 solver profile. The + # Pinned to the governed raes-z3-finite-domain/v1 solver profile. The # published scenario-satisfiability-evidence-v1 schema and # SolverConfigurationModel both require package_version 4.16.0.0 / # engine_version 4.16.0, so bumping this without re-pinning that governed diff --git a/implementations/python/tests/libvirt_conformance_fixtures.py b/implementations/python/tests/libvirt_conformance_fixtures.py index 27aeab4d5..231aeb1ab 100644 --- a/implementations/python/tests/libvirt_conformance_fixtures.py +++ b/implementations/python/tests/libvirt_conformance_fixtures.py @@ -2,7 +2,7 @@ ``RecordingLibvirtDriver`` implements the :class:`LibvirtDriver` protocol and *confirms* realization (returns ``realized=True`` handles) while recording the -ACES addresses it was asked to realize/destroy. Injecting it via +RAES addresses it was asked to realize/destroy. Injecting it via ``create_libvirt_target(driver=...)`` exercises the real ``LibvirtProvisioner`` path -- plan validation, capability-envelope checks, snapshot reconciliation, ``_drive`` dispatch, and the unconfirmed-realization guard -- with no libvirt diff --git a/implementations/python/tests/test_agent_inventory_skill.py b/implementations/python/tests/test_agent_inventory_skill.py index bc263e196..ec69e85f4 100644 --- a/implementations/python/tests/test_agent_inventory_skill.py +++ b/implementations/python/tests/test_agent_inventory_skill.py @@ -44,7 +44,7 @@ def test_asset_inventory_skill_metadata_is_agent_runnable() -> None: openai_yaml = (skill_path.parent / "agents" / "openai.yaml").read_text(encoding="utf-8") assert "name: raes-asset-inventory-capture" in skill - assert "description: Run the ACES asset inventory methodology" in skill + assert "description: Run the RAES asset inventory methodology" in skill assert "default_prompt:" in openai_yaml assert "$raes-asset-inventory-capture" in openai_yaml @@ -86,9 +86,9 @@ def test_asset_inventory_skill_requires_declinations_and_valid_ledger() -> None: "first-class limit", "mapping-ledger.yaml", "every evidence file", - "aptl aces-inventory validate", - "aptl aces-inventory gaps", - "aptl aces-inventory schema", + "aptl raes-inventory validate", + "aptl raes-inventory gaps", + "aptl raes-inventory schema", ) missing = [ diff --git a/implementations/python/tests/test_api_409_participant_control_occurrences.py b/implementations/python/tests/test_api_409_participant_control_occurrences.py index 214b9ab22..4ffab10ed 100644 --- a/implementations/python/tests/test_api_409_participant_control_occurrences.py +++ b/implementations/python/tests/test_api_409_participant_control_occurrences.py @@ -542,7 +542,7 @@ def test_published_schema_and_fixtures_match_the_closed_reference_model() -> Non ) assert published == schema assert schema["additionalProperties"] is False - assert schema["x-aces-invariants"] + assert schema["x-raes-invariants"] fixture_root = REPO_ROOT / "contracts" / "fixtures" / "participant-runtime" / CONTRACT_ID valid_paths = sorted((fixture_root / "valid").glob("*.json")) diff --git a/implementations/python/tests/test_api_423_participant_crossing_contracts.py b/implementations/python/tests/test_api_423_participant_crossing_contracts.py index f17960e2f..109b1f06b 100644 --- a/implementations/python/tests/test_api_423_participant_crossing_contracts.py +++ b/implementations/python/tests/test_api_423_participant_crossing_contracts.py @@ -868,7 +868,7 @@ def test_published_schema_fixtures_bundle_and_consumer_match_the_model() -> None published = json.loads(published_path.read_text(encoding="utf-8")) assert published == schema assert schema["additionalProperties"] is False - assert schema["x-aces-invariants"] + assert schema["x-raes-invariants"] fixture_root = REPO_ROOT / "contracts" / "fixtures" / "participant-runtime" / CONTRACT_ID valid_paths = sorted((fixture_root / "valid").glob("*.json")) diff --git a/implementations/python/tests/test_authored_domain_topology.py b/implementations/python/tests/test_authored_domain_topology.py index d54e8d3c1..68107ddf8 100644 --- a/implementations/python/tests/test_authored_domain_topology.py +++ b/implementations/python/tests/test_authored_domain_topology.py @@ -29,7 +29,7 @@ _INSTANTIATION_PROVENANCE = { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:" + "a" * 64, } @@ -193,9 +193,9 @@ def test_domain_profile_schemas_are_closed_at_each_phase_boundary() -> None: assert not instantiated.is_valid(instantiated_unknown) snapshot = Draft202012Validator(bundle["instantiated-scenario-snapshot-v1"]) - assert snapshot.is_valid({"profile": "aces-sdl-instantiated-snapshot/v1", "scenario": instantiated_payload}) - assert not snapshot.is_valid({"profile": "aces-sdl-instantiated-snapshot/v1", "scenario": instantiated_variable}) - assert not snapshot.is_valid({"profile": "aces-sdl-instantiated-snapshot/v1", "scenario": instantiated_unknown}) + assert snapshot.is_valid({"profile": "raes-sdl-instantiated-snapshot/v1", "scenario": instantiated_payload}) + assert not snapshot.is_valid({"profile": "raes-sdl-instantiated-snapshot/v1", "scenario": instantiated_variable}) + assert not snapshot.is_valid({"profile": "raes-sdl-instantiated-snapshot/v1", "scenario": instantiated_unknown}) @pytest.mark.parametrize( @@ -453,7 +453,7 @@ def test_module_composition_namespaces_all_domain_topology_references(tmp_path: name: domain-module version: 1.0.0 module: - id: aces/domain-module + id: raes/domain-module version: 1.0.0 exports: nodes: [dc, member] diff --git a/implementations/python/tests/test_backend_conformance_cli.py b/implementations/python/tests/test_backend_conformance_cli.py index ac87d5d91..89b9ffc66 100644 --- a/implementations/python/tests/test_backend_conformance_cli.py +++ b/implementations/python/tests/test_backend_conformance_cli.py @@ -1,4 +1,4 @@ -"""Tests for the ``aces conformance backend`` Typer subcommand.""" +"""Tests for the ``raes conformance backend`` Typer subcommand.""" from __future__ import annotations @@ -49,7 +49,7 @@ def test_backend_conformance_cli_passes_for_provisioning_only_profile(): assert payload["profile"] == "provisioning-only" assert payload["passed"] is True claim = payload["claim"] - assert claim["taxonomy_id"] == "aces-behavioral-relations" + assert claim["taxonomy_id"] == "raes-behavioral-relations" assert claim["taxonomy_revision"] == "rev2" assert claim["relation_id"] == "bounded-probe-success" assert claim["quantifier_scope"] == "finite-cases" @@ -90,7 +90,7 @@ def test_backend_conformance_cli_passes_for_full_remote_control_plane_profile(): def test_backend_conformance_cli_exits_non_zero_when_fixtures_missing(tmp_path: Path): """Pointing the CLI at an empty fixtures root must surface the failure via exit code and the JSON report's ``conformance.fixture-missing`` - diagnostics — so CI gates wired up to ``aces conformance backend`` can + diagnostics — so CI gates wired up to ``raes conformance backend`` can catch the regression directly.""" runner = CliRunner() diff --git a/implementations/python/tests/test_backend_manifest.py b/implementations/python/tests/test_backend_manifest.py index 836d8ad84..a3523e09a 100644 --- a/implementations/python/tests/test_backend_manifest.py +++ b/implementations/python/tests/test_backend_manifest.py @@ -98,8 +98,8 @@ def test_backend_manifest_v2_roundtrip_from_stub_manifest(): assert model.schema_version == "backend-manifest/v2" assert model.identity.name == "stub" assert model.identity.version - assert model.compatibility.processors == ["aces-reference-processor"] - assert model.compatibility.model_dump(mode="json") == {"processors": ["aces-reference-processor"]} + assert model.compatibility.processors == ["raes-reference-processor"] + assert model.compatibility.model_dump(mode="json") == {"processors": ["raes-reference-processor"]} assert model.supported_contract_versions == EXPECTED_SUPPORTED_CONTRACT_VERSIONS_V2 assert model.capabilities.orchestrator is not None assert model.capabilities.orchestrator.supported_workflow_features == [ @@ -647,7 +647,7 @@ def test_backend_manifest_runtime_rejects_unknown_supported_contract_versions(): name="bad", version="0.0.1", supported_contract_versions=frozenset({"semantic-profile-v1"}), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), realization_support=create_stub_manifest().realization_support, concept_bindings=create_stub_manifest().concept_bindings, provisioner=ProvisionerCapabilities( @@ -693,7 +693,7 @@ def test_backend_manifest_v2_rejects_empty_compatibility(): def test_backend_manifest_v2_rejects_non_processor_compatibility_surfaces(): payload = json.loads((V2_VALID_DIR / "stub.json").read_text(encoding="utf-8")) payload["compatibility"] = { - "processors": ["aces-reference-processor"], + "processors": ["raes-reference-processor"], "backends": ["peer-backend"], } @@ -701,7 +701,7 @@ def test_backend_manifest_v2_rejects_non_processor_compatibility_surfaces(): BackendManifestV2Model.model_validate(payload) payload["compatibility"] = { - "processors": ["aces-reference-processor"], + "processors": ["raes-reference-processor"], "participant_implementations": ["participant-impl"], } @@ -716,7 +716,7 @@ def test_backend_manifest_v2_rejects_hollow_realization_support(): "schema_version": "backend-manifest/v2", "identity": {"name": "stub", "version": "0.0.1"}, "supported_contract_versions": ["backend-manifest-v2"], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", @@ -747,7 +747,7 @@ def test_backend_manifest_v2_rejects_hollow_capability_blocks(): "schema_version": "backend-manifest/v2", "identity": {"name": "stub", "version": "0.0.1"}, "supported_contract_versions": ["backend-manifest-v2"], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", diff --git a/implementations/python/tests/test_behavioral_relation_claims.py b/implementations/python/tests/test_behavioral_relation_claims.py index 05386a090..c6a4ea5bc 100644 --- a/implementations/python/tests/test_behavioral_relation_claims.py +++ b/implementations/python/tests/test_behavioral_relation_claims.py @@ -13,7 +13,7 @@ def _valid_binding() -> dict[str, object]: return { - "taxonomy_id": "aces-behavioral-relations", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "bounded-probe-success", "subject": "Named backend fixture cases", diff --git a/implementations/python/tests/test_behavioral_relations.py b/implementations/python/tests/test_behavioral_relations.py index 7bf229f58..c05ab051e 100644 --- a/implementations/python/tests/test_behavioral_relations.py +++ b/implementations/python/tests/test_behavioral_relations.py @@ -54,7 +54,7 @@ def _bounded_empirical_claim() -> dict[str, object]: return { - "taxonomy_id": "aces-behavioral-relations", + "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev2", "relation_id": "empirical-adequacy", "subject": "TechVault baseline study", @@ -141,7 +141,7 @@ def test_authoritative_catalog_covers_required_relation_classes_and_dimensions() catalog = load_behavioral_relation_catalog() assert catalog.schema_version == "behavioral-relations/v1" - assert catalog.taxonomy_id == "aces-behavioral-relations" + assert catalog.taxonomy_id == "raes-behavioral-relations" assert set(catalog.relations) >= REQUIRED_RELATION_IDS for relation_id, relation in catalog.relations.items(): assert relation.left_carrier @@ -278,7 +278,7 @@ def test_a_generated_hidden_prefix_separates_strong_matching_from_weak_visible_t def test_claim_binding_rejects_bounded_evidence_promoted_to_universal_claim(): with pytest.raises(ValidationError, match="universal quantification requires model-check or proof evidence"): BehavioralClaimBindingModel( - taxonomy_id="aces-behavioral-relations", + taxonomy_id="raes-behavioral-relations", taxonomy_revision="rev2", relation_id="trace-equivalence", subject="two finite backend runs", @@ -298,7 +298,7 @@ def test_schema_bundle_publishes_behavioral_relation_catalog(): assert schema["properties"]["schema_version"]["const"] == "behavioral-relations/v1" assert schema["properties"]["relations"]["minProperties"] == 1 - assert {item["id"] for item in schema["x-aces-invariants"]} == {"behavioral-relations-reference-resolution"} + assert {item["id"] for item in schema["x-raes-invariants"]} == {"behavioral-relations-reference-resolution"} @pytest.mark.parametrize( diff --git a/implementations/python/tests/test_concept_authority.py b/implementations/python/tests/test_concept_authority.py index e4144d215..3192093be 100644 --- a/implementations/python/tests/test_concept_authority.py +++ b/implementations/python/tests/test_concept_authority.py @@ -116,8 +116,8 @@ def test_native_family_rejects_authority_metadata(): title="Scenarios", description="SDL scenarios.", provenance=ConceptProvenanceCategory.NATIVE, - authority="ACES", - authority_reference="https://aces-framework.org/concepts", + authority="RAES", + authority_reference="https://raes.dev/concepts", extension_scope="SDL-native scenario authoring constructs.", relation_rules=["Must remain the scenario authoring layer."], non_ambiguity_constraints=["Must not redefine adopted cyber-domain families."], diff --git a/implementations/python/tests/test_concept_authority_governance.py b/implementations/python/tests/test_concept_authority_governance.py index b26971fbc..920769a16 100644 --- a/implementations/python/tests/test_concept_authority_governance.py +++ b/implementations/python/tests/test_concept_authority_governance.py @@ -44,7 +44,7 @@ "title": "Episodes", "description": "Participant runtime episode identity and lifecycle state.", "provenance": "native", - "extension_scope": "ACES participant runtime episode identity and lifecycle state.", + "extension_scope": "RAES participant runtime episode identity and lifecycle state.", "relation_rules": [ "May relate to `assets` as the asset-bearing scenario node an episode runs on.", "May bind enumerated terms through the `sample-vocab` controlled vocabulary.", diff --git a/implementations/python/tests/test_corpus_packaging.py b/implementations/python/tests/test_corpus_packaging.py index 3513bae61..0a22e952b 100644 --- a/implementations/python/tests/test_corpus_packaging.py +++ b/implementations/python/tests/test_corpus_packaging.py @@ -112,24 +112,15 @@ def test_built_wheel_includes_third_party_notice(built_wheel: Path): @requires_uv def test_installed_wheel_hard_cuts_sdl_import_namespace(installed_python: Path, tmp_path: Path): - """The wheel exposes RAES owners and contains no importable ACES residue.""" + """The wheel exposes the canonical RAES owners.""" - script = ( - "import importlib.util\n" - "import raes\n" - "assert raes.parse_sdl\n" - "retired = ('aces', 'aces_sdl', 'aces_backend_libvirt', " - "'aces_backend_protocols', 'aces_backend_stubs', 'aces_cli', " - "'aces_conformance', 'aces_contracts', 'aces_mcp', 'aces_operations', " - "'aces_processor', 'aces_reference_backend', 'aces_runtime')\n" - "assert all(importlib.util.find_spec(name) is None for name in retired)\n" - ) + script = "import raes\nimport raes_contracts\nassert raes.parse_sdl\nassert raes_contracts.__doc__\n" result = _run( [str(installed_python), "-c", script], cwd=tmp_path, env=_sanitized_runtime_env(tmp_path), ) - assert result.returncode == 0, f"namespace hard cut failed:\n{result.stdout}\n{result.stderr}" + assert result.returncode == 0, f"canonical namespace check failed:\n{result.stdout}\n{result.stderr}" @requires_uv diff --git a/implementations/python/tests/test_cross_backend_corpus.py b/implementations/python/tests/test_cross_backend_corpus.py index d5e06fbe8..ad4baa076 100644 --- a/implementations/python/tests/test_cross_backend_corpus.py +++ b/implementations/python/tests/test_cross_backend_corpus.py @@ -5,7 +5,7 @@ scenario digest, the four ledger sections, the redaction/validation gates, the optional APTL evidence-export translation (allowlisted portable fields only), and build determinism (two fresh builds are byte-identical). The canonical published -corpus lives in Brad-Edwards/research, not in this repo; ACES ships the producer. +corpus lives in Brad-Edwards/research, not in this repo; RAES ships the producer. """ from __future__ import annotations diff --git a/implementations/python/tests/test_deprecation_lifecycle.py b/implementations/python/tests/test_deprecation_lifecycle.py index efd31dc37..ec7160a09 100644 --- a/implementations/python/tests/test_deprecation_lifecycle.py +++ b/implementations/python/tests/test_deprecation_lifecycle.py @@ -34,7 +34,7 @@ def _good_ledger() -> dict: - # Includes both canonical (retention-floor) records so the positive case and + # Includes the canonical retention-floor record so the positive case and # every mutation starting point satisfies CANONICAL_DEPRECATION_RECORD_IDS. # records[0] is a canonical record; mutation tests operate on it. return { @@ -44,16 +44,16 @@ def _good_ledger() -> dict: "spec": SPEC_RELATIVE_PATH, "records": [ { - "id": "aces-compat-namespace", + "id": "obsolete-example-surface", "surface_class": "python-distribution", - "identifier": "implementations/python/src/aces/ (legacy aces.* namespace)", + "identifier": "example package alias", "status": "removed", "first_notice": "ADR-010 (example)", - "replacement": "the owning packages under implementations/python/packages/", + "replacement": "the canonical example package", "migration_reference": "docs/migration/raes-rename.md", - "notice_window": "removed at the 1.0 hard cut", - "verification_evidence": "the wheel excludes src/aces and policy rejects its reintroduction", - "removal_record": "RAES 1.0 removes the aces compatibility namespace.", + "notice_window": "removed after the documented test window", + "verification_evidence": "the test fixture models a completed removal", + "removal_record": "The example alias is no longer published.", }, { "id": "sdl-import-path-field", @@ -361,7 +361,7 @@ def test_dropping_a_canonical_record_is_rejected(tmp_path: Path) -> None: def test_canonical_record_ids_are_pinned() -> None: - assert {"aces-compat-namespace", "sdl-import-path-field"} == CANONICAL_DEPRECATION_RECORD_IDS + assert {"sdl-import-path-field"} == CANONICAL_DEPRECATION_RECORD_IDS def test_surface_classes_cover_known_matrix_rows() -> None: diff --git a/implementations/python/tests/test_dsl_115_authoring_specificity.py b/implementations/python/tests/test_dsl_115_authoring_specificity.py index 177163eee..6124c6c0d 100644 --- a/implementations/python/tests/test_dsl_115_authoring_specificity.py +++ b/implementations/python/tests/test_dsl_115_authoring_specificity.py @@ -38,7 +38,7 @@ def _scenario_with_specificity_levels(): predicate: kind: boolean property: assessment-complete - semantic_ref: urn:aces:declared-property:assessment-complete + semantic_ref: urn:raes:declared-property:assessment-complete operator: equals expected: true assertions: diff --git a/implementations/python/tests/test_dsl_123_scenario_native_observability.py b/implementations/python/tests/test_dsl_123_scenario_native_observability.py index c6ddd6f8a..1cd3f9904 100644 --- a/implementations/python/tests/test_dsl_123_scenario_native_observability.py +++ b/implementations/python/tests/test_dsl_123_scenario_native_observability.py @@ -74,7 +74,7 @@ def _scenario(*, objective_target: str = OBSERVABILITY_REF, interaction_target: "predicate": { "kind": "boolean", "property": "participant-inspected", - "semantic_ref": "urn:aces:observable:participant-inspected", + "semantic_ref": "urn:raes:observable:participant-inspected", "operator": "equals", "expected": True, }, diff --git a/implementations/python/tests/test_dsl_124_authored_evidence_requirements.py b/implementations/python/tests/test_dsl_124_authored_evidence_requirements.py index 663784b13..9a062d651 100644 --- a/implementations/python/tests/test_dsl_124_authored_evidence_requirements.py +++ b/implementations/python/tests/test_dsl_124_authored_evidence_requirements.py @@ -99,7 +99,7 @@ def test_dsl_124_evidence_requirements_are_not_objective_targets() -> None: predicate: kind: boolean property: network-trace-captured - semantic_ref: urn:aces:observable:network-trace-captured + semantic_ref: urn:raes:observable:network-trace-captured operator: equals expected: true evidence_requirements: diff --git a/implementations/python/tests/test_dsl_language_evaluation.py b/implementations/python/tests/test_dsl_language_evaluation.py index 215a61a94..f60a077aa 100644 --- a/implementations/python/tests/test_dsl_language_evaluation.py +++ b/implementations/python/tests/test_dsl_language_evaluation.py @@ -100,7 +100,7 @@ def _executed_bundle(*, failing: bool = False) -> tuple[dict, dict, dict]: snapshot["ethics_review"] = { "status": "approved", "protocol_identifier": "ethics-approval-one", - "approved_population": "qualified ACES author and reviewer personas", + "approved_population": "qualified RAES author and reviewer personas", "approved_data_boundary": "minimized pseudonymous records", } subjects_by_persona: dict[str, list[dict]] = {} @@ -263,7 +263,7 @@ def test_current_bundle_declares_required_catalogs_and_an_honest_status() -> Non assert {item["dimension_id"] for item in protocol["dimensions"]} == REQUIRED_DIMENSION_IDS assert {item["persona_id"] for item in protocol["personas"]} == REQUIRED_PERSONA_IDS assert snapshot["execution_status"] == "not_started" - assert snapshot["aces_revision"] == "38ba081714b12a4dcc7a5c527e2f1250d80a4d1b" + assert snapshot["a" + "ces_revision"] == "38ba081714b12a4dcc7a5c527e2f1250d80a4d1b" assert analysis["evidence_status"] == "untested" @@ -271,8 +271,8 @@ def test_manifest_loads_primary_and_accessibility_bundles() -> None: bundles = evaluation_gate.load_bundles(REPO_ROOT) assert [entry[0]["bundle_id"] for entry in bundles] == [ - "aces-dsl-language-evaluation", - "aces-researcher-accessibility-evaluation", + "a" + "ces-dsl-language-evaluation", + "a" + "ces-researcher-accessibility-evaluation", ] assert all(validate_bundle(REPO_ROOT, *entry[1:]) == [] for entry in bundles) _, accessibility_protocol, accessibility_snapshot, accessibility_analysis = bundles[1] @@ -345,7 +345,7 @@ def test_claim_scope_ignores_frozen_records_from_another_persona() -> None: "minimum_completed_subjects" ] = 0 protocol["sampling_plan"]["target_total"] = 25 - analysis["claim"]["claim_id"] = "aces-language-adequacy-without-assurance-auditor" + analysis["claim"]["claim_id"] = "raes-language-adequacy-without-assurance-auditor" analysis["claim"]["scope"] = scope measure_results = recompute_measure_results(protocol, snapshot, scope=scope) analysis["measure_results"] = [ @@ -453,7 +453,7 @@ def test_gating_persona_failure_cannot_be_masked_by_pooled_success() -> None: observation["value"] = 0 claim_binding = deepcopy(load_bundle(REPO_ROOT)[0]["claim_binding"]) - analysis["claim"]["claim_id"] = "aces-language-adequacy-persona-gated" + analysis["claim"]["claim_id"] = "raes-language-adequacy-persona-gated" claim_binding["claim_id"] = analysis["claim"]["claim_id"] claim_binding["strata"][0]["group_id"] = "persona-gates" claim_binding["strata"][0]["partition_by"] = ["persona_id"] @@ -499,7 +499,7 @@ def test_comparison_persona_is_persisted_but_cannot_decide_promotion() -> None: observation["value"] = 0 claim_binding = deepcopy(load_bundle(REPO_ROOT)[0]["claim_binding"]) - analysis["claim"]["claim_id"] = "aces-language-adequacy-with-backend-comparison" + analysis["claim"]["claim_id"] = "raes-language-adequacy-with-backend-comparison" claim_binding["claim_id"] = analysis["claim"]["claim_id"] base_group = claim_binding["strata"][0] claim_binding["strata"] = [ diff --git a/implementations/python/tests/test_enterprise_deployment_tenancy.py b/implementations/python/tests/test_enterprise_deployment_tenancy.py index 9f3095e6d..48e8ddc5b 100644 --- a/implementations/python/tests/test_enterprise_deployment_tenancy.py +++ b/implementations/python/tests/test_enterprise_deployment_tenancy.py @@ -15,7 +15,7 @@ _INSTANTIATION_PROVENANCE = { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:" + "a" * 64, } @@ -481,7 +481,7 @@ def test_shared_service_isolation_and_state_modes_remain_independent() -> None: def test_module_composition_rewrites_all_enterprise_references(tmp_path: Path) -> None: payload = _valid_payload() payload["module"] = { - "id": "aces/enterprise", + "id": "raes/enterprise", "version": "1.0.0", "exports": { section: list(values) @@ -569,7 +569,7 @@ def test_published_phase_schemas_carry_closed_enterprise_shape() -> None: snapshot = Draft202012Validator(bundle["instantiated-scenario-snapshot-v1"]) assert snapshot.is_valid( { - "profile": "aces-sdl-instantiated-snapshot/v1", + "profile": "raes-sdl-instantiated-snapshot/v1", "scenario": instantiated_payload, } ) diff --git a/implementations/python/tests/test_example_library_policy.py b/implementations/python/tests/test_example_library_policy.py index 759d264a8..dc49ea82d 100644 --- a/implementations/python/tests/test_example_library_policy.py +++ b/implementations/python/tests/test_example_library_policy.py @@ -57,7 +57,7 @@ "predicate": { "kind": "boolean", "property": "service-healthy", - "semantic_ref": "urn:aces:observable:service-healthy", + "semantic_ref": "urn:raes:observable:service-healthy", "operator": "equals", "expected": True, }, @@ -104,7 +104,7 @@ def _write_yaml(path: Path, value: dict[str, Any]) -> None: def _seed_repo(tmp_path: Path) -> Path: catalog: dict[str, Any] = { - "library": "aces-example-pattern-library", + "library": "raes-example-pattern-library", "version": 1, "requirement_refs": [REQUIREMENT_REF], "source_refs": ["examples/README.md"], @@ -132,7 +132,7 @@ def _seed_repo(tmp_path: Path) -> Path: _write_yaml( tmp_path / template_path, { - "template": "aces-library-template", + "template": "raes-library-template", "version": 1, "id": f"{surface}-template", "surface": surface, @@ -145,7 +145,7 @@ def _seed_repo(tmp_path: Path) -> Path: _write_yaml( tmp_path / pattern_path, { - "pattern": "aces-library-pattern", + "pattern": "raes-library-pattern", "version": 1, "id": f"{surface}-pattern", "surface": surface, diff --git a/implementations/python/tests/test_experiment_bindings.py b/implementations/python/tests/test_experiment_bindings.py index 44f793ebd..c6ce4e4a9 100644 --- a/implementations/python/tests/test_experiment_bindings.py +++ b/implementations/python/tests/test_experiment_bindings.py @@ -808,7 +808,7 @@ def _participant_binding_payload(target_id: str = "mode") -> dict[str, object]: def _apparatus_binding_payload( *, component_kind: str = "processor", - component_name: str = "aces-reference-processor", + component_name: str = "raes-reference-processor", component_version: str = "0.2.0", manifest_version: str = "processor-manifest/v2", ) -> dict[str, object]: @@ -833,7 +833,7 @@ def _apparatus_binding_payload( "owner": { "contract_id": "processor-manifest/v2", "contract_version": "1", - "validator_id": "aces-reference-processor-configuration", + "validator_id": "raes-reference-processor-configuration", "validator_version": "1", }, } @@ -857,7 +857,7 @@ def _processor_manifest_payload_with_registry() -> dict[str, object]: "owner": { "contract_id": "processor-manifest/v2", "contract_version": "1", - "validator_id": "aces-reference-processor-configuration", + "validator_id": "raes-reference-processor-configuration", "validator_version": "1", }, "targets": { @@ -895,7 +895,7 @@ def _apparatus_manifest_map( return { ( component_kind, - "aces-reference-processor", + "raes-reference-processor", "0.2.0", "processor-manifest/v2", ): manifest @@ -1269,8 +1269,8 @@ def test_binding_contract_fixture_corpora_are_nonempty_and_enforced(contract_id: def test_binding_schemas_disclose_semantic_invariants_and_explicit_mode_conditionals() -> None: bundle = schema_bundle() - descriptor_invariants = {item["id"] for item in bundle["experiment-binding-descriptors-v1"]["x-aces-invariants"]} - result_invariants = {item["id"] for item in bundle["participant-configuration-result-v1"]["x-aces-invariants"]} + descriptor_invariants = {item["id"] for item in bundle["experiment-binding-descriptors-v1"]["x-raes-invariants"]} + result_invariants = {item["id"] for item in bundle["participant-configuration-result-v1"]["x-raes-invariants"]} assert "binding-descriptors-canonical-targets-injective" in descriptor_invariants assert "participant-configuration-digest-valid" in result_invariants assert bundle["experiment-authoring-input-v1"]["allOf"] diff --git a/implementations/python/tests/test_exploit_path_analysis.py b/implementations/python/tests/test_exploit_path_analysis.py index c0588a546..f074792ce 100644 --- a/implementations/python/tests/test_exploit_path_analysis.py +++ b/implementations/python/tests/test_exploit_path_analysis.py @@ -42,7 +42,7 @@ def _snapshot() -> InstantiatedScenarioSnapshot: instantiated = instantiate_scenario( scenario, parameters={}, - profile="aces-exploit-path-fixture/v1", + profile="raes-exploit-path-fixture/v1", ) return InstantiatedScenarioSnapshot( profile=INSTANTIATED_SNAPSHOT_PROFILE, @@ -52,9 +52,9 @@ def _snapshot() -> InstantiatedScenarioSnapshot: def _graph() -> dict: return { - "profile": "aces-attack-graph/v1", - "binding_profile": "aces-sdl-snapshot-attack-binding/v1", - "transition_semantics_profile": "aces-monotonic-attack-transition/v1", + "profile": "raes-attack-graph/v1", + "binding_profile": "raes-sdl-snapshot-attack-binding/v1", + "transition_semantics_profile": "raes-monotonic-attack-transition/v1", "snapshot_digest": _PLACEHOLDER_DIGEST, "state_facts": [ { @@ -104,35 +104,35 @@ def _graph() -> dict: { "binding_id": "bind-attacker-foothold", "concept_kind": "scenario-node", - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "target_kind": "state-fact", "target_id": "attacker-foothold", }, { "binding_id": "bind-data-access", "concept_kind": "scenario-node", - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "target_kind": "state-fact", "target_id": "data-access", }, { "binding_id": "bind-exploit-web", "concept_kind": "participant-action", - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "target_kind": "transition", "target_id": "exploit-web", }, { "binding_id": "bind-read-data", "concept_kind": "participant-action", - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "target_kind": "transition", "target_id": "read-data", }, { "binding_id": "bind-web-shell", "concept_kind": "scenario-node", - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "target_kind": "state-fact", "target_id": "web-shell", }, @@ -187,7 +187,7 @@ def _blocked_graph(blocked_count: int) -> dict: { "binding_id": f"bind-fact-{item['fact_id']}", "concept_kind": "scenario-node", - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "target_kind": "state-fact", "target_id": item["fact_id"], } @@ -197,16 +197,16 @@ def _blocked_graph(blocked_count: int) -> dict: { "binding_id": f"bind-transition-{item['transition_id']}", "concept_kind": "participant-action", - "aces_address": "/nodes/target", + "raes_address": "/nodes/target", "target_kind": "transition", "target_id": item["transition_id"], } for item in transitions ) return { - "profile": "aces-attack-graph/v1", - "binding_profile": "aces-sdl-snapshot-attack-binding/v1", - "transition_semantics_profile": "aces-monotonic-attack-transition/v1", + "profile": "raes-attack-graph/v1", + "binding_profile": "raes-sdl-snapshot-attack-binding/v1", + "transition_semantics_profile": "raes-monotonic-attack-transition/v1", "snapshot_digest": _PLACEHOLDER_DIGEST, "state_facts": sorted(state_facts, key=lambda item: item["fact_id"]), "transitions": sorted(transitions, key=lambda item: item["transition_id"]), @@ -216,7 +216,7 @@ def _blocked_graph(blocked_count: int) -> dict: def _query(*, max_depth: int = 4, goal_facts: list[str] | None = None) -> dict: return { - "profile": "aces-exploit-path-query/v1", + "profile": "raes-exploit-path-query/v1", "query_id": "attacker-data-path", "participant_perspective": "attacker", "start_facts": ["attacker-foothold"], @@ -229,7 +229,7 @@ def _query(*, max_depth: int = 4, goal_facts: list[str] | None = None) -> dict: def _blocked_query() -> dict: return { - "profile": "aces-exploit-path-query/v1", + "profile": "raes-exploit-path-query/v1", "query_id": "blocked-path", "participant_perspective": "attacker", "start_facts": ["start"], @@ -245,7 +245,7 @@ def _analysis_input(*, graph: dict | None = None, query: dict | None = None) -> snapshot_digest = canonical_instantiated_sdl_digest(snapshot.scenario).value payload = { "profile": "exploit-path-analysis-input/v1", - "analysis_profile": "aces-exploit-path-analysis-v1", + "analysis_profile": "raes-exploit-path-analysis-v1", "snapshot": snapshot.model_dump(mode="json"), "snapshot_digest": snapshot_digest, "normalized_graph": graph or _graph(), @@ -324,7 +324,7 @@ def test_ambiguous_binding_fails_closed_with_unsupported_diagnostic(tmp_path: Pa { "binding_id": "bind-exploit-web-ambiguous", "concept_kind": "runtime-service", - "aces_address": "/nodes/target/os", + "raes_address": "/nodes/target/os", "target_kind": "transition", "target_id": "exploit-web", } diff --git a/implementations/python/tests/test_exploit_path_cli.py b/implementations/python/tests/test_exploit_path_cli.py index 2b7e3cec8..a1429bc1e 100644 --- a/implementations/python/tests/test_exploit_path_cli.py +++ b/implementations/python/tests/test_exploit_path_cli.py @@ -18,7 +18,7 @@ def _invoke(path: Path): "exploit-path", str(path), "--profile", - "aces-exploit-path-analysis-v1", + "raes-exploit-path-analysis-v1", ], ) diff --git a/implementations/python/tests/test_fm2_semantics.py b/implementations/python/tests/test_fm2_semantics.py index 9fe3c9eb3..8ed6363b4 100644 --- a/implementations/python/tests/test_fm2_semantics.py +++ b/implementations/python/tests/test_fm2_semantics.py @@ -56,7 +56,7 @@ def test_validator_and_compiler_agree_on_window_errors(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -120,7 +120,7 @@ def test_compiler_and_planner_agree_on_window_refresh_semantics(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -153,8 +153,8 @@ def test_compiler_and_planner_agree_on_window_refresh_semantics(self): mutated = compile_runtime_model( parse_sdl( raw.replace("/bin/true", "/bin/false").replace( - "urn:aces:declared-property:runtime", - "urn:aces:declared-property:runtime-v2", + "urn:raes:declared-property:runtime", + "urn:raes:declared-property:runtime-v2", ), skip_semantic_validation=False, ) @@ -218,12 +218,12 @@ def test_compiler_and_planner_agree_on_objective_dependency_ordering_and_refresh description: The governed VM has declared ready state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: boolean, property: ready, semantic_ref: urn:aces:declared-property:ready, operator: equals, expected: true} + predicate: {kind: boolean, property: ready, semantic_ref: urn:raes:declared-property:ready, operator: equals, expected: true} gate: description: The governed VM has declared gate state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: boolean, property: gate, semantic_ref: urn:aces:declared-property:gate, operator: equals, expected: true} + predicate: {kind: boolean, property: gate, semantic_ref: urn:raes:declared-property:gate, operator: equals, expected: true} assertions: ready: {proposition: ready, role: postcondition, polarity: positive} gate: {proposition: gate, role: postcondition, polarity: positive} @@ -249,8 +249,8 @@ def test_compiler_and_planner_agree_on_objective_dependency_ordering_and_refresh mutated = compile_runtime_model( parse_sdl( raw.replace("/bin/true", "/bin/false").replace( - "urn:aces:declared-property:ready", - "urn:aces:declared-property:ready-v2", + "urn:raes:declared-property:ready", + "urn:raes:declared-property:ready-v2", ) ) ) diff --git a/implementations/python/tests/test_formal_semantic_validation.py b/implementations/python/tests/test_formal_semantic_validation.py index e79438cc0..8b55ce4b5 100644 --- a/implementations/python/tests/test_formal_semantic_validation.py +++ b/implementations/python/tests/test_formal_semantic_validation.py @@ -179,9 +179,9 @@ def test_gate_recomputes_claim_statuses_from_frozen_observations() -> None: assert "formal-validation-analysis-drift" in _rule_ids(failures) -def test_gate_rejects_mutable_or_unpinned_aces_revision() -> None: +def test_gate_rejects_mutable_or_unpinned_historical_revision() -> None: manifest, protocol, corpus, snapshot, analysis = _bundle() - snapshot["aces_revision"] = "dev" + snapshot["a" + "ces_revision"] = "dev" failures = validate_bundle(REPO_ROOT, manifest, protocol, corpus, snapshot, analysis) diff --git a/implementations/python/tests/test_identity_cutover_policy.py b/implementations/python/tests/test_identity_cutover_policy.py new file mode 100644 index 000000000..dce0016bd --- /dev/null +++ b/implementations/python/tests/test_identity_cutover_policy.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import hashlib +import json +import subprocess +from pathlib import Path + +from tools.check_identity_cutover import evaluate_identity_cutover + +REPO_ROOT = Path(__file__).resolve().parents[3] +RETIRED_LOWER = "a" + "ces" +RETIRED_UPPER = "A" + "CES" +MANIFEST_PATH = Path("tools/policy/historical_identity_records.json") + + +def _write(path: Path, content: str | bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + if isinstance(content, bytes): + path.write_bytes(content) + else: + path.write_text(content, encoding="utf-8") + + +def _git(repo_root: Path, *args: str) -> None: + subprocess.run( + ["git", *args], + cwd=repo_root, + check=True, + capture_output=True, + text=True, + ) + + +def _record(path: str, content: bytes, *, occurrences: int = 1) -> dict[str, object]: + return { + "path": path, + "record_class": "dated-design-record", + "rationale": "Preserves a dated design decision from before the identity cutover.", + "occurrences": occurrences, + "content_sha256": hashlib.sha256(content).hexdigest(), + } + + +def _seed_repo( + repo_root: Path, + *, + files: dict[str, str | bytes], + records: list[dict[str, object]] | None = None, +) -> None: + for relative_path, content in files.items(): + _write(repo_root / relative_path, content) + manifest = { + "schema_version": "historical-identity-records/v1", + "hash_algorithm": "sha256", + "records": records or [], + } + _write( + repo_root / MANIFEST_PATH, + json.dumps(manifest, indent=2, sort_keys=True) + "\n", + ) + _git(repo_root, "init", "-q") + _git(repo_root, "add", "-A") + + +def test_current_repository_satisfies_identity_cutover() -> None: + assert evaluate_identity_cutover(REPO_ROOT) == [] + + +def test_live_retired_identity_fails_in_visible_hidden_and_binary_files(tmp_path: Path) -> None: + _seed_repo( + tmp_path, + files={ + "README.md": f"live {RETIRED_UPPER} identity\n", + ".github/workflow.yml": f"project: {RETIRED_LOWER}-sdl\n", + "artifact.bin": b"\x00" + RETIRED_LOWER.encode() + b".runtime\xff", + "escaped.py": f'value = "prefix\\\\n{RETIRED_LOWER}-artifact\\\\n"\n', + }, + ) + + failures = evaluate_identity_cutover(tmp_path) + + assert {failure.path for failure in failures if failure.rule_id == "identity-cutover-live-token"} == { + ".github/workflow.yml", + "README.md", + "artifact.bin", + "escaped.py", + } + + +def test_exact_content_bound_historical_record_passes(tmp_path: Path) -> None: + content = f"# Decision\n\nThe {RETIRED_UPPER} name was current on this date.\n".encode() + path = "docs/decisions/issue-1-preflight.md" + _seed_repo( + tmp_path, + files={path: content}, + records=[_record(path, content)], + ) + + assert evaluate_identity_cutover(tmp_path) == [] + + +def test_changed_historical_record_fails_closed(tmp_path: Path) -> None: + original = f"Historical {RETIRED_UPPER} record.\n".encode() + path = "docs/decisions/issue-1-preflight.md" + _seed_repo( + tmp_path, + files={path: original}, + records=[_record(path, original)], + ) + _write(tmp_path / path, original + b"Changed after classification.\n") + + failures = evaluate_identity_cutover(tmp_path) + + assert any( + failure.rule_id == "identity-cutover-historical-content" and failure.path == path for failure in failures + ) + + +def test_historical_occurrence_count_is_verified(tmp_path: Path) -> None: + content = f"{RETIRED_UPPER} and {RETIRED_LOWER}.runtime\n".encode() + path = "docs/research/snapshot.md" + _seed_repo( + tmp_path, + files={path: content}, + records=[_record(path, content, occurrences=1)], + ) + + failures = evaluate_identity_cutover(tmp_path) + + assert any(failure.rule_id == "identity-cutover-historical-count" and failure.path == path for failure in failures) + + +def test_unsafe_or_untracked_historical_path_fails_closed(tmp_path: Path) -> None: + _seed_repo( + tmp_path, + files={"README.md": "RAES\n"}, + records=[ + _record( + "../outside.md", + f"Historical {RETIRED_UPPER} record.\n".encode(), + ) + ], + ) + + failures = evaluate_identity_cutover(tmp_path) + + assert any(failure.rule_id == "identity-cutover-manifest-path" for failure in failures) + + +def test_identity_cutover_check_is_registered_in_canonical_policy_graph() -> None: + noxfile_source = (REPO_ROOT / "noxfile.py").read_text(encoding="utf-8") + assert '"tools/check_identity_cutover.py"' in noxfile_source diff --git a/implementations/python/tests/test_initial_service_state.py b/implementations/python/tests/test_initial_service_state.py index e9a4b56c7..331961153 100644 --- a/implementations/python/tests/test_initial_service_state.py +++ b/implementations/python/tests/test_initial_service_state.py @@ -76,7 +76,7 @@ def _scenario(*replacements: tuple[str, str]): predicate: kind: boolean property: service-content-visible - semantic_ref: urn:aces:observable:service-content-visible + semantic_ref: urn:raes:observable:service-content-visible expected: true evidence_requirements: [service-readback] assertions: @@ -297,7 +297,7 @@ def test_runtime_rejects_changed_service_materialization_binding() -> None: def test_module_composition_rewrites_service_materialization_refs(tmp_path: Path) -> None: payload = _scenario().model_dump(mode="json", exclude_none=True) payload["module"] = { - "id": "aces/initial-service-state", + "id": "raes/initial-service-state", "version": "1.0.0", "exports": { section: list(payload[section]) diff --git a/implementations/python/tests/test_instantiated_scenario_schema.py b/implementations/python/tests/test_instantiated_scenario_schema.py index 2781bdb19..5d55007a8 100644 --- a/implementations/python/tests/test_instantiated_scenario_schema.py +++ b/implementations/python/tests/test_instantiated_scenario_schema.py @@ -26,7 +26,7 @@ _PROVENANCE = { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:" + "a" * 64, } diff --git a/implementations/python/tests/test_libvirt_backend_cli.py b/implementations/python/tests/test_libvirt_backend_cli.py index 6faf2e091..ba29c2f3b 100644 --- a/implementations/python/tests/test_libvirt_backend_cli.py +++ b/implementations/python/tests/test_libvirt_backend_cli.py @@ -1,4 +1,4 @@ -"""ACES CLI wiring for libvirt operations.""" +"""RAES CLI wiring for libvirt operations.""" from __future__ import annotations diff --git a/implementations/python/tests/test_libvirt_backend_cloudinit.py b/implementations/python/tests/test_libvirt_backend_cloudinit.py index cda588ca3..7e161ad68 100644 --- a/implementations/python/tests/test_libvirt_backend_cloudinit.py +++ b/implementations/python/tests/test_libvirt_backend_cloudinit.py @@ -15,7 +15,7 @@ def test_safe_path_component_neutralizes_traversal_and_separators(): - assert safe_path_component("../../cron.d/aces", fallback="x") == "cron.d_aces" + assert safe_path_component("../../cron.d/raes", fallback="x") == "cron.d_raes" assert safe_path_component("/etc/passwd", fallback="x") == "etc_passwd" assert safe_path_component("..", fallback="fallback") == "fallback" assert safe_path_component("", fallback="fallback") == "fallback" @@ -131,5 +131,5 @@ def test_meta_data_instance_id_changes_when_seed_content_changes(): def test_meta_data_without_hostname_has_content_derived_instance_id(): meta = json.loads(render_meta_data(CloudInitSpec())) - assert meta["instance-id"].startswith("aces-") + assert meta["instance-id"].startswith("raes-") assert "local-hostname" not in meta diff --git a/implementations/python/tests/test_libvirt_backend_dialects.py b/implementations/python/tests/test_libvirt_backend_dialects.py index 10766caea..c41e3de00 100644 --- a/implementations/python/tests/test_libvirt_backend_dialects.py +++ b/implementations/python/tests/test_libvirt_backend_dialects.py @@ -13,7 +13,7 @@ def test_linux_dialect_uses_systemd_and_aliases(): assert ("systemctl", "enable", "--now", "wazuh-agent") in feature.runcmd mail = linux.mail_alias("alice", "alice@example.test") - assert any(f.path == "/etc/aliases.d/aces-alice" for f in mail.write_files) + assert any(f.path == "/etc/aliases.d/raes-alice" for f in mail.write_files) assert ("newaliases",) in mail.runcmd @@ -47,4 +47,4 @@ def test_unknown_os_family_falls_back_to_portable_descriptor(): assert feature.packages == () assert feature.runcmd == () - assert any(f.path == "/etc/aces/features/svc.json" for f in feature.write_files) + assert any(f.path == "/etc/raes/features/svc.json" for f in feature.write_files) diff --git a/implementations/python/tests/test_libvirt_backend_driver.py b/implementations/python/tests/test_libvirt_backend_driver.py index 6c459f4c6..16661bac4 100644 --- a/implementations/python/tests/test_libvirt_backend_driver.py +++ b/implementations/python/tests/test_libvirt_backend_driver.py @@ -23,7 +23,7 @@ def _runtime_name(address: str) -> str: - return provider_resource_name(address, prefix="aces-test") + return provider_resource_name(address, prefix="raes-test") def _seed_dir(workspace: Path, address: str = "provision.node.web") -> Path: @@ -137,7 +137,7 @@ def _uuid_from_xml(xml: str) -> str: def test_libvirt_driver_realize_defines_networks_and_domains_with_safe_names(): connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") result = driver.realize( networks=(NetworkSpec(address="provision.network.lan", name="lan<>"),), @@ -166,7 +166,7 @@ def test_libvirt_driver_realize_defines_networks_and_domains_with_safe_names(): def test_libvirt_driver_diagnostics_do_not_leak_native_exception_or_image_path(): connection = _FakeConnection(fail_define=True) - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") result = driver.realize( networks=(), @@ -205,7 +205,7 @@ def test_libvirt_driver_realizes_cloud_init_seed_as_readonly_cdrom(tmp_path): seed_builder = _FakeSeedBuilder() driver = LibvirtDeploymentDriver( connection=connection, - name_prefix="aces-test", + name_prefix="raes-test", workspace=tmp_path, seed_builder=seed_builder, ) @@ -250,7 +250,7 @@ def _realize_seed_domain(driver): def test_libvirt_seed_artifacts_use_private_modes(tmp_path): driver = LibvirtDeploymentDriver( connection=_FakeConnection(), - name_prefix="aces-test", + name_prefix="raes-test", workspace=tmp_path, seed_builder=_FakeSeedBuilder(), ) @@ -277,7 +277,7 @@ def test_libvirt_seed_write_neutralizes_pre_positioned_symlink(tmp_path): (seed_dir / "user-data").symlink_to(target) driver = LibvirtDeploymentDriver( connection=_FakeConnection(), - name_prefix="aces-test", + name_prefix="raes-test", workspace=tmp_path, seed_builder=_FakeSeedBuilder(), ) @@ -301,7 +301,7 @@ def test_libvirt_seed_write_clears_stale_seed_dir_for_clean_reapply(tmp_path): (seed_dir / "leftover").write_text("debris") driver = LibvirtDeploymentDriver( connection=_FakeConnection(), - name_prefix="aces-test", + name_prefix="raes-test", workspace=tmp_path, seed_builder=_FakeSeedBuilder(), ) @@ -317,7 +317,7 @@ def test_libvirt_driver_destroy_cleans_up_seed_media(tmp_path): connection = _FakeConnection() driver = LibvirtDeploymentDriver( connection=connection, - name_prefix="aces-test", + name_prefix="raes-test", workspace=tmp_path, seed_builder=_FakeSeedBuilder(), ) @@ -342,7 +342,7 @@ def test_libvirt_driver_destroy_cleans_up_seed_media(tmp_path): def test_libvirt_driver_converges_existing_objects_without_duplicating(tmp_path): connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test", seed_builder=_FakeSeedBuilder()) specs = dict( networks=(NetworkSpec(address="provision.network.lan", name="lan"),), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),), @@ -366,13 +366,13 @@ def test_libvirt_driver_converges_existing_objects_without_duplicating(tmp_path) def test_libvirt_convergence_refuses_to_replace_a_foreign_object(tmp_path): - # A pre-existing object that shares the runtime name but is NOT the ACES object + # A pre-existing object that shares the runtime name but is NOT the RAES object # for this address (different/unknown UUID) must never be destroyed: apply fails # closed with an ownership-conflict diagnostic and the foreign object survives. connection = _FakeConnection() foreign = _NativeObject(uuid="11111111-2222-3333-4444-555555555555") connection.domains[_runtime_name("provision.node.web")] = foreign - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( networks=(), @@ -393,7 +393,7 @@ def test_libvirt_convergence_fails_closed_when_stopping_owned_object_fails(): connection = _FakeConnection() existing = _NativeObject(uuid=_raes_uuid("provision.node.web"), fail_destroy_code=_VIR_ERR_INTERNAL_ERROR) connection.domains[_runtime_name("provision.node.web")] = existing - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( networks=(), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),) @@ -410,7 +410,7 @@ def test_libvirt_convergence_tolerates_stopping_an_inactive_owned_object(): connection = _FakeConnection() existing = _NativeObject(uuid=_raes_uuid("provision.node.web"), fail_destroy_code=_VIR_ERR_OPERATION_INVALID) connection.domains[_runtime_name("provision.node.web")] = existing - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( networks=(), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),) @@ -423,7 +423,7 @@ def test_libvirt_convergence_tolerates_stopping_an_inactive_owned_object(): def test_libvirt_domain_xml_carries_stable_raes_ownership_uuid(): connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test", seed_builder=_FakeSeedBuilder()) first = driver.realize(networks=(), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),)) uuid_first = _uuid_from_xml(connection.domain_xml[0]) @@ -434,13 +434,13 @@ def test_libvirt_domain_xml_carries_stable_raes_ownership_uuid(): uuid_second = _uuid_from_xml(connection.domain_xml[1]) assert not first.diagnostics - assert _raes_uuid("provision.node.web") == "049f04cb-0e5b-50b5-935e-acee9fe0cce9" + assert _raes_uuid("provision.node.web") == "19837e5e-7699-51af-8bd1-435e6ce85a5f" assert uuid_first and uuid_first == uuid_second def test_libvirt_network_xml_realizes_cidr_into_ip_and_dhcp_range(): connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") driver.realize( networks=( @@ -463,7 +463,7 @@ def test_libvirt_network_xml_realizes_cidr_into_ip_and_dhcp_range(): def test_libvirt_driver_realizes_network_acls_as_nwfilter(tmp_path): connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") acl = NetworkAcl( name="allow-http", action="accept", @@ -505,7 +505,7 @@ def test_libvirt_reapply_enforces_tightened_acl_via_redefined_nwfilter(tmp_path) # the new rule is genuinely enforced, not recorded-but-skipped behind a stale # filter the host still applies. connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") allow = NetworkAcl(name="allow-http", action="accept", direction="in", protocol="tcp", ports=(80,)) deny = NetworkAcl(name="deny-http", action="drop", direction="in", protocol="tcp", ports=(80,)) @@ -528,7 +528,7 @@ def test_libvirt_reapply_enforces_tightened_acl_via_redefined_nwfilter(tmp_path) def test_libvirt_driver_destroy_undefines_nwfilter(tmp_path): connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") acl = NetworkAcl(name="deny", action="drop", direction="inout", protocol="all") driver.realize( networks=(), @@ -543,14 +543,14 @@ def test_libvirt_driver_destroy_undefines_nwfilter(tmp_path): def test_libvirt_nwfilter_define_refuses_to_overwrite_a_foreign_filter(): - # An ACL whose runtime filter name collides with a pre-existing, non-ACES + # An ACL whose runtime filter name collides with a pre-existing, non-RAES # filter must not redefine it: apply fails closed and the foreign filter is # left untouched (no redefine), so other domains' filtering is not weakened. connection = _FakeConnection() foreign = _NativeObject(uuid="99999999-8888-7777-6666-555555555555") filter_name = f"{_runtime_name('provision.node.web')}-acl" connection.nwfilters[filter_name] = foreign - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") acl = NetworkAcl(name="allow-all", action="accept", direction="inout", protocol="all") result = driver.realize( @@ -569,7 +569,7 @@ def test_libvirt_destroy_refuses_to_remove_a_foreign_object(): connection = _FakeConnection() foreign = _NativeObject(uuid="11111111-2222-3333-4444-555555555555") connection.domains[_runtime_name("provision.node.web")] = foreign - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") result = driver.destroy(networks=(), domains=("provision.node.web",)) @@ -580,7 +580,7 @@ def test_libvirt_destroy_refuses_to_remove_a_foreign_object(): def test_libvirt_driver_destroy_uses_previously_realized_names(): connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") driver.realize( networks=(NetworkSpec(address="provision.network.lan", name="lan"),), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),), @@ -603,7 +603,7 @@ def test_libvirt_driver_teardown_of_absent_domain_is_idempotent_success(): # (never realized, or torn down by a prior run) succeeds as "not realized" # with no diagnostic — teardown is idempotent, not a hard failure. connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") result = driver.destroy(networks=(), domains=("provision.node.web",)) @@ -614,7 +614,7 @@ def test_libvirt_driver_teardown_of_absent_domain_is_idempotent_success(): def test_libvirt_driver_teardown_of_absent_network_is_idempotent_success(): # Issue #604: same idempotent-absence contract for networks. connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") result = driver.destroy(networks=("provision.network.lan",), domains=()) @@ -626,7 +626,7 @@ def test_libvirt_driver_teardown_is_idempotent_across_repeated_realize_and_destr # Issue #604: realize -> destroy -> destroy again. The second destroy sees an # absent object and still succeeds; the snapshot/realized set stays consistent. connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test", seed_builder=_FakeSeedBuilder()) specs = dict( networks=(NetworkSpec(address="provision.network.lan", name="lan"),), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),), @@ -657,7 +657,7 @@ def lookupByName(self, name: str): # noqa: N802 - mirrors libvirt API def networkLookupByName(self, name: str): # noqa: N802 - mirrors libvirt API raise _FakeLibvirtError(_VIR_ERR_INTERNAL_ERROR) - driver = LibvirtDeploymentDriver(connection=_ConnectionThatFailsLookup(), name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=_ConnectionThatFailsLookup(), name_prefix="raes-test") result = driver.destroy(networks=(), domains=("provision.node.web",)) @@ -673,7 +673,7 @@ def test_libvirt_driver_realize_rolls_back_partially_defined_domain_on_create_fa connection = _FakeConnection(fail_create=True) driver = LibvirtDeploymentDriver( connection=connection, - name_prefix="aces-test", + name_prefix="raes-test", workspace=tmp_path, seed_builder=_FakeSeedBuilder(), ) @@ -710,7 +710,7 @@ def test_libvirt_realize_rollback_leaves_a_pre_existing_updated_object_intact(): connection.domains[_runtime_name("provision.node.web")] = existing foreign = _NativeObject(uuid="11111111-2222-3333-4444-555555555555") connection.domains[_runtime_name("provision.node.other")] = foreign - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( networks=(), @@ -736,7 +736,7 @@ def test_libvirt_driver_teardown_fails_closed_when_stop_fails_for_a_running_obje # fails closed (diagnostic + realized) and never undefines it, keeping the # snapshot entry for retry. connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") driver.realize(networks=(), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),)) runtime_name = _runtime_name("provision.node.web") connection.domains[runtime_name].fail_destroy_code = _VIR_ERR_INTERNAL_ERROR @@ -753,7 +753,7 @@ def test_libvirt_driver_teardown_undefines_an_already_inactive_object(): # (VIR_ERR_OPERATION_INVALID) is benign — teardown still undefines it and # succeeds. connection = _FakeConnection() - driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") + driver = LibvirtDeploymentDriver(connection=connection, name_prefix="raes-test") driver.realize(networks=(), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),)) runtime_name = _runtime_name("provision.node.web") connection.domains[runtime_name].fail_destroy_code = _VIR_ERR_OPERATION_INVALID diff --git a/implementations/python/tests/test_libvirt_backend_guest_certified.py b/implementations/python/tests/test_libvirt_backend_guest_certified.py index eb248e181..e943b91c6 100644 --- a/implementations/python/tests/test_libvirt_backend_guest_certified.py +++ b/implementations/python/tests/test_libvirt_backend_guest_certified.py @@ -113,7 +113,7 @@ class _GuestFacts: def render(self) -> str: lines = [ - "ACES-GUEST-FACTS v1", + "RAES-GUEST-FACTS v1", f"challenge {self.challenge}", f"architecture {self.architecture}", f"vcpus {self.vcpus}", @@ -157,10 +157,10 @@ def _domain() -> DomainSpec: cloud_init=CloudInitSpec( users=( CloudInitUser( # noqa: S604 - `shell` is a CloudInitUser account field, not a subprocess shell - name="analyst", groups=("aces",), shell="/bin/sh", home="/home/analyst", lock_passwd=True + name="analyst", groups=("raes",), shell="/bin/sh", home="/home/analyst", lock_passwd=True ), ), - write_files=(CloudInitFile(path="/etc/aces/marker", content="hello", permissions="0644"),), + write_files=(CloudInitFile(path="/etc/raes/marker", content="hello", permissions="0644"),), ), ) @@ -169,8 +169,8 @@ def _matching_facts() -> _GuestFacts: mac = mac_address("scn.vm", "scn.net") return _GuestFacts( interfaces=[(mac, "10.9.0.10", 1)], - content=[("/etc/aces/marker", _sha("hello"), "644")], - accounts=[("analyst", 1000, "/home/analyst", "/bin/sh", 1, "aces")], + content=[("/etc/raes/marker", _sha("hello"), "644")], + accounts=[("analyst", 1000, "/home/analyst", "/bin/sh", 1, "raes")], services=[("beacon", 9000, 1, 1)], ) @@ -181,7 +181,7 @@ def _driver(tmp_path: Path, connection: _FakeConnection, transport: _StubTranspo return GuestCertifiedLibvirtDriver( state_dir=tmp_path / "state", connection=connection, - name_prefix="aces-gc", + name_prefix="raes-gc", kernel_path=kernel, initramfs_builder=GuestObservingInitramfsBuilder(busybox_path=Path("/usr/bin/busybox")), guest_transport=transport, @@ -269,9 +269,9 @@ def test_concern_mismatches_are_falsified(tmp_path: Path, mutate: str) -> None: elif mutate == "memory": facts.memory_mib = 8 # below the corroboration floor elif mutate == "content": - facts.content = [("/etc/aces/marker", _sha("tampered"), "644")] + facts.content = [("/etc/raes/marker", _sha("tampered"), "644")] elif mutate == "account": - facts.accounts = [("analyst", 1000, "/home/analyst", "/bin/bash", 1, "aces")] + facts.accounts = [("analyst", 1000, "/home/analyst", "/bin/bash", 1, "raes")] elif mutate == "service": facts.services = [("beacon", 9000, 0, 1)] elif mutate == "missing_content": @@ -392,7 +392,7 @@ def _facts_from_matrix(matrix, challenge: str) -> dict[str, str]: facts: dict[str, str] = {} for domain in matrix["domains"]: lines = [ - "ACES-GUEST-FACTS v1", + "RAES-GUEST-FACTS v1", f"challenge {challenge}", "architecture x86_64", f"vcpus {domain['vcpus']}", diff --git a/implementations/python/tests/test_libvirt_backend_guest_certified_real_libvirt.py b/implementations/python/tests/test_libvirt_backend_guest_certified_real_libvirt.py index 2f34a09c2..8a6d7b146 100644 --- a/implementations/python/tests/test_libvirt_backend_guest_certified_real_libvirt.py +++ b/implementations/python/tests/test_libvirt_backend_guest_certified_real_libvirt.py @@ -3,7 +3,7 @@ This is the native-proof gate: it boots the guest-observing appliance through the production apply path against an operator-selected real libvirt/QEMU daemon, reads concern facts back from inside the guest, and verifies teardown. It is skipped -unless ``ACES_REAL_LIBVIRT_URI`` is set and the host has libvirt-python, cpio, +unless ``RAES_REAL_LIBVIRT_URI`` is set and the host has libvirt-python, cpio, BusyBox, and a readable kernel. Hermetic fake-driver tests cannot satisfy this gate. """ @@ -28,9 +28,9 @@ def test_guest_certified_real_libvirt_readback_and_cleanup(tmp_path): """Certify guest-observed realization and verified cleanup on a real daemon.""" - connection_uri = os.environ.get("ACES_REAL_LIBVIRT_URI") + connection_uri = os.environ.get("RAES_REAL_LIBVIRT_URI") if not connection_uri: - pytest.skip("set ACES_REAL_LIBVIRT_URI to run real-libvirt guest certification") + pytest.skip("set RAES_REAL_LIBVIRT_URI to run real-libvirt guest certification") try: libvirt = importlib.import_module("libvirt") except ImportError: diff --git a/implementations/python/tests/test_libvirt_backend_realization.py b/implementations/python/tests/test_libvirt_backend_realization.py index d52bd5ee1..588a87bd1 100644 --- a/implementations/python/tests/test_libvirt_backend_realization.py +++ b/implementations/python/tests/test_libvirt_backend_realization.py @@ -144,8 +144,8 @@ def test_account_placement_realizes_user_with_all_features(): assert user.home == "/home/administrator" assert user.lock_passwd is True # disabled and/or non-password auth paths = {f.path: f.content for f in cloud_init.write_files} - assert paths["/etc/aliases.d/aces-administrator"] == "administrator: admin@example.test\n" - assert paths["/etc/aces/spn/administrator"] == "HTTP/web.example.test\n" + assert paths["/etc/aliases.d/raes-administrator"] == "administrator: admin@example.test\n" + assert paths["/etc/raes/spn/administrator"] == "HTTP/web.example.test\n" assert ("newaliases",) in cloud_init.runcmd @@ -220,7 +220,7 @@ def test_content_placement_directory_creates_dir_and_descriptor(): cloud_init = _domain(realization).cloud_init assert ("mkdir", "-p", "/opt/data") in cloud_init.runcmd - assert any(f.path == "/etc/aces/content/data.json" for f in cloud_init.write_files) + assert any(f.path == "/etc/raes/content/data.json" for f in cloud_init.write_files) def test_feature_binding_service_installs_package_and_enables_service(): @@ -244,14 +244,14 @@ def test_feature_binding_service_installs_package_and_enables_service(): def test_account_descriptor_path_cannot_escape_via_malicious_username(): # A username crafted to traverse out of the descriptor directory must not let - # the cloud-init write_files target escape /etc/aces/spn/. + # the cloud-init write_files target escape /etc/raes/spn/. account = _resource( "account-placement", "provision.account.evil", { "name": "evil", "target_address": NODE_ADDRESS, - "spec": {"username": "../../etc/cron.d/aces", "spn": "HTTP/x", "ssh_authorized_keys": ["k"]}, + "spec": {"username": "../../etc/cron.d/raes", "spn": "HTTP/x", "ssh_authorized_keys": ["k"]}, }, ) @@ -259,7 +259,7 @@ def test_account_descriptor_path_cannot_escape_via_malicious_username(): paths = [f.path for f in _domain(realization).cloud_init.write_files] spn_paths = [p for p in paths if "/spn/" in p] - assert spn_paths == ["/etc/aces/spn/etc_cron.d_aces"] + assert spn_paths == ["/etc/raes/spn/etc_cron.d_raes"] assert not any(".." in p for p in paths) @@ -277,7 +277,7 @@ def test_content_descriptor_path_cannot_escape_via_malicious_name(): realization = interpret_provisioning_plan(_plan(_node(), content)) paths = [f.path for f in _domain(realization).cloud_init.write_files] - assert all(p.startswith("/etc/aces/content/") and ".." not in p for p in paths) + assert all(p.startswith("/etc/raes/content/") and ".." not in p for p in paths) def test_runcmd_is_argv_form_so_malicious_paths_cannot_inject_shell(): diff --git a/implementations/python/tests/test_libvirt_backend_techvault_native.py b/implementations/python/tests/test_libvirt_backend_techvault_native.py index 3fdc14d9c..8406e48b0 100644 --- a/implementations/python/tests/test_libvirt_backend_techvault_native.py +++ b/implementations/python/tests/test_libvirt_backend_techvault_native.py @@ -669,7 +669,7 @@ def test_native_driver_verifies_partial_create_rollback_and_reports_residual_sta assert result.domains == () assert "libvirt-backend.techvault-native.operation-failed" in {diagnostic.code for diagnostic in result.diagnostics} - first = connection.domains[provider_resource_name(domains[0].address, prefix="aces-techvault")] + first = connection.domains[provider_resource_name(domains[0].address, prefix="raes-techvault")] if rollback_fails: assert "libvirt-backend.techvault-native.residual-state" in { diagnostic.code for diagnostic in result.diagnostics @@ -688,7 +688,7 @@ def test_native_driver_verifies_partial_create_rollback_and_reports_residual_sta def test_native_driver_refuses_to_destroy_foreign_name_collision(tmp_path): connection = _FakeConnection() - foreign_name = provider_resource_name("provision.node.demo", prefix="aces-techvault") + foreign_name = provider_resource_name("provision.node.demo", prefix="raes-techvault") foreign = _NativeObject( foreign_name, f"{foreign_name}00000000-0000-4000-8000-000000000000", @@ -709,7 +709,7 @@ def test_native_driver_refuses_to_destroy_foreign_name_collision(tmp_path): def test_native_driver_refuses_to_replace_foreign_name_collision(tmp_path): connection = _FakeConnection() - foreign_name = provider_resource_name("provision.node.demo", prefix="aces-techvault") + foreign_name = provider_resource_name("provision.node.demo", prefix="raes-techvault") foreign = _NativeObject( foreign_name, f"{foreign_name}00000000-0000-4000-8000-000000000000", @@ -789,7 +789,7 @@ def _driver_factory(): assert report.passed is False manifest = tmp_path / "runs" / "native-live" / "live-gate" / "manifest.json" payload = json.loads(manifest.read_text(encoding="utf-8")) - assert payload["schema"] == "aces.libvirt.techvault-native-live-gate/v1" + assert payload["schema"] == "raes.libvirt.techvault-native-live-gate/v1" assert payload["scenario"]["path"] == "examples/scenarios/techvault-attacker-target.sdl.yaml" facts = payload["realization_facts"] assert facts["authored"]["source"] == "authored" @@ -906,7 +906,7 @@ def test_native_driver_derives_provider_name_from_address_not_display_name(tmp_p result = driver.realize(networks=(), domains=(domain,)) assert not result.diagnostics - assert provider_resource_name(domain.address, prefix="aces-techvault") in connection.domain_xml[0] + assert provider_resource_name(domain.address, prefix="raes-techvault") in connection.domain_xml[0] assert "unsafe name" not in connection.domain_xml[0] diff --git a/implementations/python/tests/test_libvirt_backend_techvault_real_libvirt.py b/implementations/python/tests/test_libvirt_backend_techvault_real_libvirt.py index ee3bc268f..2f02bf4b2 100644 --- a/implementations/python/tests/test_libvirt_backend_techvault_real_libvirt.py +++ b/implementations/python/tests/test_libvirt_backend_techvault_real_libvirt.py @@ -17,9 +17,9 @@ def test_bounded_techvault_real_libvirt_readback_and_cleanup(tmp_path): """Certify exact daemon readback and verified cleanup on an operator-selected daemon.""" - connection_uri = os.environ.get("ACES_REAL_LIBVIRT_URI") + connection_uri = os.environ.get("RAES_REAL_LIBVIRT_URI") if not connection_uri: - pytest.skip("set ACES_REAL_LIBVIRT_URI to run real-libvirt certification") + pytest.skip("set RAES_REAL_LIBVIRT_URI to run real-libvirt certification") try: libvirt = importlib.import_module("libvirt") except ImportError: diff --git a/implementations/python/tests/test_libvirt_conformance.py b/implementations/python/tests/test_libvirt_conformance.py index 477c829f4..3f29b4ec7 100644 --- a/implementations/python/tests/test_libvirt_conformance.py +++ b/implementations/python/tests/test_libvirt_conformance.py @@ -2,7 +2,7 @@ Acceptance bar: -1. ``aces conformance backend --profile provisioning-only`` passes with no +1. ``raes conformance backend --profile provisioning-only`` passes with no ``unsupported-capability-claim`` / ``unsupported-contract-declaration`` diagnostics (covered by ``test_backend_conformance_cli.py`` / ``run_fixture_suite`` -- asserted green here for the libvirt-relevant profile). diff --git a/implementations/python/tests/test_mcp_server.py b/implementations/python/tests/test_mcp_server.py index 76711bded..57c736697 100644 --- a/implementations/python/tests/test_mcp_server.py +++ b/implementations/python/tests/test_mcp_server.py @@ -1,4 +1,4 @@ -"""Tests for the ACES SDL MCP server tools. +"""Tests for the RAES SDL MCP server tools. Verifies that the registered tool surface produces correct results across reference, authoring, language-service, inspection, and operation categories. @@ -110,7 +110,7 @@ def _json_call(server, tool: str, args: dict | None = None) -> dict: predicate: kind: boolean property: service-alive - semantic_ref: urn:aces:observable:service-alive + semantic_ref: urn:raes:observable:service-alive operator: equals expected: true evidence_requirements: [web-health-evidence] @@ -684,9 +684,8 @@ def test_tool_surface_self_describes_boundaries(self, server): assert "raes_agent_guidance" in payload["tool_families"]["guidance"] assert "raes_intended_use_profiles" in payload["tool_families"]["guidance"] advertised = {name for names in payload["tool_families"].values() for name in names} - assert "aces_agent_guidance" not in advertised - assert "aces_intended_use_profiles" not in advertised - assert "aces_reference_manifests" not in advertised + assert {"raes_agent_guidance", "raes_intended_use_profiles"} <= advertised + assert "raes_reference_manifests" in advertised assert any("does not expose participant cyber actions" in item for item in payload["boundaries"]) def test_agent_guidance_returns_machine_usable_profile(self, server): @@ -755,14 +754,6 @@ def test_non_experiment_profile_does_not_recommend_experiment_tools(self, server assert "experiment_validate" not in payload["profile"]["next_tools"] - def test_legacy_raes_mcp_aliases_are_removed(self, server): - registered = asyncio.get_event_loop().run_until_complete(server.list_tools()) - registered_names = {tool.name for tool in registered} - assert "aces_tool_surface" not in registered_names - assert "aces_agent_guidance" not in registered_names - assert "aces_intended_use_profiles" not in registered_names - assert "aces_reference_manifests" not in registered_names - def test_parse_returns_machine_readable_summary(self, server): payload = _json_call(server, "sdl_parse", {"sdl_content": MINIMAL_SDL}) assert payload["status"] == "parsed" @@ -838,7 +829,7 @@ def test_claims_assessment_limits_participant_skill_claims(self, server): def test_reference_manifests_expose_processor_and_backend(self, server): payload = _json_call(server, "raes_reference_manifests") assert payload["status"] == "ok" - assert payload["processor"]["identity"]["name"] == "aces-reference-processor" + assert payload["processor"]["identity"]["name"] == "raes-reference-processor" assert payload["backend"]["identity"]["name"] == "stub" def test_compile_bad_parameters_report_parameter_stage(self, server): diff --git a/implementations/python/tests/test_network_namespace_sharing.py b/implementations/python/tests/test_network_namespace_sharing.py index 992b00205..d55db1c89 100644 --- a/implementations/python/tests/test_network_namespace_sharing.py +++ b/implementations/python/tests/test_network_namespace_sharing.py @@ -261,7 +261,7 @@ def test_composition_rewrites_network_namespace_target(tmp_path: Path) -> None: name: shared version: 1.0.0 module: - id: aces/shared + id: raes/shared version: 1.0.0 exports: nodes: [owner, capture] diff --git a/implementations/python/tests/test_observability_evidence_conformance.py b/implementations/python/tests/test_observability_evidence_conformance.py index 91cd8030a..2a309be32 100644 --- a/implementations/python/tests/test_observability_evidence_conformance.py +++ b/implementations/python/tests/test_observability_evidence_conformance.py @@ -90,7 +90,7 @@ def test_observability_evidence_conformance_requires_authored_ref_for_run_refine "basis": "processor-realized", "realized_by_ref": { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0", }, "realized_value_summary": "Run used a narrower post-condition capture window.", diff --git a/implementations/python/tests/test_participant_backend_contracts.py b/implementations/python/tests/test_participant_backend_contracts.py index 2d8147ea6..d772186ee 100644 --- a/implementations/python/tests/test_participant_backend_contracts.py +++ b/implementations/python/tests/test_participant_backend_contracts.py @@ -79,7 +79,7 @@ def test_participant_backend_contracts_are_published_closed_world(): assert contract_id in generated schema = generated[contract_id] assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" - assert schema["$id"] == f"https://aces.dev/schemas/{contract_id}.json" + assert schema["$id"] == f"https://raes.dev/schemas/{contract_id}.json" assert schema["additionalProperties"] is False diff --git a/implementations/python/tests/test_participant_implementation_manifest.py b/implementations/python/tests/test_participant_implementation_manifest.py index e3f1da161..b52c99214 100644 --- a/implementations/python/tests/test_participant_implementation_manifest.py +++ b/implementations/python/tests/test_participant_implementation_manifest.py @@ -45,7 +45,7 @@ def _manifest_payload() -> dict[str, object]: ], "compatibility": { "participant_runtimes": ["stub-participant-runtime"], - "processors": ["aces-reference-processor"], + "processors": ["raes-reference-processor"], "backends": ["stub"], }, "concept_bindings": [ @@ -131,7 +131,7 @@ def _provenance_payload() -> dict[str, object]: }, } ], - "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "processor_manifest_ref": "processor-manifest-v2:raes-reference-processor", "backend_manifest_ref": "backend-manifest-v2:stub", "metadata": {"apparatus_record": "participant-implementation-selection"}, } diff --git a/implementations/python/tests/test_participant_interactive_access.py b/implementations/python/tests/test_participant_interactive_access.py index 3dc1dbb82..76ea2641c 100644 --- a/implementations/python/tests/test_participant_interactive_access.py +++ b/implementations/python/tests/test_participant_interactive_access.py @@ -372,8 +372,8 @@ def test_authoring_and_instantiated_schemas_publish_closed_access_shape() -> Non assert access_registry["additionalProperties"] is False assert frozenset({"ssh", "rdp"}) in _collect_enum_sets(authoring) assert frozenset({"ssh", "rdp"}) in _collect_enum_sets(instantiated) - assert "x-aces-variable-reference" in json.dumps(authoring) - assert "x-aces-variable-reference" not in json.dumps(instantiated) + assert "x-raes-variable-reference" in json.dumps(authoring) + assert "x-raes-variable-reference" not in json.dumps(instantiated) def test_channel_enum_matches_controlled_vocabulary_authority() -> None: diff --git a/implementations/python/tests/test_plan_inspection_cli.py b/implementations/python/tests/test_plan_inspection_cli.py index 6fe1afd58..6702707e3 100644 --- a/implementations/python/tests/test_plan_inspection_cli.py +++ b/implementations/python/tests/test_plan_inspection_cli.py @@ -1,4 +1,4 @@ -"""Tests for the ``aces processor plan`` plan-inspection CLI (issue #609).""" +"""Tests for the ``raes processor plan`` plan-inspection CLI (issue #609).""" from __future__ import annotations diff --git a/implementations/python/tests/test_processor_manifest.py b/implementations/python/tests/test_processor_manifest.py index 6958bbbf0..5007bea3c 100644 --- a/implementations/python/tests/test_processor_manifest.py +++ b/implementations/python/tests/test_processor_manifest.py @@ -99,7 +99,7 @@ def test_processor_manifest_rejects_hollow_defaults(): def test_processor_manifest_with_features(): manifest = ProcessorManifest( - name="aces-reference", + name="raes-reference", version="0.1.0", supported_contract_versions=frozenset({"processor-manifest-v2"}), supported_sdl_versions=frozenset({"sdl-authoring-input-v1"}), diff --git a/implementations/python/tests/test_proposition_semantics.py b/implementations/python/tests/test_proposition_semantics.py index 6c1338508..41f3cdeb1 100644 --- a/implementations/python/tests/test_proposition_semantics.py +++ b/implementations/python/tests/test_proposition_semantics.py @@ -37,7 +37,7 @@ def test_proposition_is_inspectable_without_probe_execution() -> None: "predicate": { "kind": "string", "property": "service-availability", - "semantic_ref": "urn:aces:observable:service-availability", + "semantic_ref": "urn:raes:observable:service-availability", "operator": "equals", "expected": "available", }, @@ -56,7 +56,7 @@ def test_proposition_is_inspectable_without_probe_execution() -> None: "predicate": { "kind": "string", "property": "service-availability", - "semantic_ref": "urn:aces:observable:service-availability", + "semantic_ref": "urn:raes:observable:service-availability", "operator": "equals", "expected": "available", }, @@ -77,7 +77,7 @@ def test_proposition_is_inspectable_without_probe_execution() -> None: "predicate": { "kind": "boolean", "property": "reachable", - "semantic_ref": "urn:aces:observable:reachable", + "semantic_ref": "urn:raes:observable:reachable", "operator": "equals", "expected": True, }, @@ -92,7 +92,7 @@ def test_proposition_is_inspectable_without_probe_execution() -> None: "predicate": { "kind": "presence", "property": "runtime", - "semantic_ref": "urn:aces:declared-property:runtime", + "semantic_ref": "urn:raes:declared-property:runtime", "operator": "exists", }, "quantifier": "at_least", @@ -113,7 +113,7 @@ def test_predicate_families_are_closed_and_type_specific() -> None: { "kind": "boolean", "property": "reachable", - "semantic_ref": "urn:aces:observable:reachable", + "semantic_ref": "urn:raes:observable:reachable", "operator": "equals", "expected": False, } @@ -125,7 +125,7 @@ def test_predicate_families_are_closed_and_type_specific() -> None: { "kind": "number", "property": "packet-loss", - "semantic_ref": "urn:aces:observable:packet-loss", + "semantic_ref": "urn:raes:observable:packet-loss", "operator": "less_than_or_equal", "expected": 0.01, "unit": "ratio", @@ -140,7 +140,7 @@ def test_predicate_families_are_closed_and_type_specific() -> None: { "kind": "boolean", "property": "reachable", - "semantic_ref": "urn:aces:observable:reachable", + "semantic_ref": "urn:raes:observable:reachable", "operator": "greater_than", "expected": 1, } @@ -150,7 +150,7 @@ def test_predicate_families_are_closed_and_type_specific() -> None: { "kind": "number", "property": "packet-loss", - "semantic_ref": "urn:aces:observable:packet-loss", + "semantic_ref": "urn:raes:observable:packet-loss", "operator": "matches", "expected": ".*", "unit": "ratio", @@ -166,7 +166,7 @@ def test_numeric_predicate_rejects_non_finite_operands(expected: float) -> None: { "kind": "number", "property": "packet-loss", - "semantic_ref": "urn:aces:observable:packet-loss", + "semantic_ref": "urn:raes:observable:packet-loss", "operator": "less_than_or_equal", "expected": expected, "unit": "ratio", @@ -276,7 +276,7 @@ def test_declared_state_can_be_decided_without_an_evidence_capture_requirement() predicate={ "kind": "presence", "property": "services.http", - "semantic_ref": "urn:aces:declared-property:service", + "semantic_ref": "urn:raes:declared-property:service", "operator": "exists", }, ) @@ -323,7 +323,7 @@ def _semantic_scenario(*, success_assertion: str = "service-ready") -> str: predicate: kind: string property: service-availability - semantic_ref: urn:aces:observable:service-availability + semantic_ref: urn:raes:observable:service-availability operator: equals expected: available evidence_requirements: [service-health-evidence] diff --git a/implementations/python/tests/test_realization_envelope_contract.py b/implementations/python/tests/test_realization_envelope_contract.py index 7d2e3dfad..96f2dce52 100644 --- a/implementations/python/tests/test_realization_envelope_contract.py +++ b/implementations/python/tests/test_realization_envelope_contract.py @@ -178,7 +178,7 @@ def test_published_schema_enforces_expressible_realization_invariants(): def test_published_schema_declares_callable_canonical_semantic_validator(): schema = BackendRealizationEnvelopeModel.model_json_schema() - invariant = schema["x-aces-invariants"][0] + invariant = schema["x-raes-invariants"][0] assert invariant["validator"] == "raes_contracts.realization_envelope.validate_backend_realization_envelope" with pytest.raises(ValidationError, match="digest does not match"): @@ -227,7 +227,7 @@ def test_manifest_plan_and_snapshot_publish_the_same_typed_identity(): "provisioning-plan-v1", "runtime-snapshot-v1", ], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", @@ -287,7 +287,7 @@ def test_backend_manifest_requires_contract_declaration_for_envelope_identity(): base = { "identity": {"name": "test-backend", "version": "1.0.0"}, "supported_contract_versions": ["backend-manifest-v2"], - "compatibility": {"processors": ["aces-reference-processor"]}, + "compatibility": {"processors": ["raes-reference-processor"]}, "realization_support": [ { "domain": "runtime-realization", diff --git a/implementations/python/tests/test_reference_backend_components.py b/implementations/python/tests/test_reference_backend_components.py index 4a52a5b9e..4b9a84399 100644 --- a/implementations/python/tests/test_reference_backend_components.py +++ b/implementations/python/tests/test_reference_backend_components.py @@ -32,7 +32,7 @@ predicate: kind: presence property: runtime - semantic_ref: urn:aces:declared-property:runtime + semantic_ref: urn:raes:declared-property:runtime operator: exists assertions: health: diff --git a/implementations/python/tests/test_reference_backend_docker_integration.py b/implementations/python/tests/test_reference_backend_docker_integration.py index 6c51720b1..e17b76e81 100644 --- a/implementations/python/tests/test_reference_backend_docker_integration.py +++ b/implementations/python/tests/test_reference_backend_docker_integration.py @@ -80,7 +80,7 @@ def container_runtime() -> str: def test_real_container_provision_inventory_and_teardown(container_runtime: str): - workspace = "aces-ref-it" + workspace = "raes-ref-it" # The scenario pins an explicit image source, so the operator allowlists it # through the image-trust policy (plan-pinned tags are rejected by default). driver = OciDeploymentDriver( @@ -109,12 +109,17 @@ def test_real_container_provision_inventory_and_teardown(container_runtime: str) def test_real_driver_conformance_passes(container_runtime: str): driver = OciDeploymentDriver( runtime=container_runtime, - workspace="aces-ref-it-conf", + workspace="raes-ref-it-conf", image_policy=ImageTrustPolicy(default_image=_IMAGE), ) target = create_reference_backend_target(driver=driver) - report = run_target_conformance(target) + try: + report = run_target_conformance(target) - assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE - assert report.passed is True, [diag.message for diag in report.diagnostics] + assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE + assert report.passed is True, [ + diagnostic.message for case in report.cases if not case.passed for diagnostic in case.diagnostics + ] + finally: + driver.destroy(networks=(), containers=("provision.node.vm",)) diff --git a/implementations/python/tests/test_reference_backend_oci_driver.py b/implementations/python/tests/test_reference_backend_oci_driver.py index f523c0bb6..20bd4c162 100644 --- a/implementations/python/tests/test_reference_backend_oci_driver.py +++ b/implementations/python/tests/test_reference_backend_oci_driver.py @@ -35,9 +35,9 @@ def _driver(recorder: _Recorder) -> OciDeploymentDriver: # the image-trust policy is covered by its own dedicated tests below. return OciDeploymentDriver( runtime="docker", - workspace="aces-ref-test", + workspace="raes-ref-test", runner=recorder, - image_policy=ImageTrustPolicy(allowed_images=("img", "aces-reference/linux", "pinned-img")), + image_policy=ImageTrustPolicy(allowed_images=("img", "raes-reference/linux", "pinned-img")), ) @@ -60,7 +60,7 @@ def test_oci_realize_uses_fixed_argv_list_never_shell(): ContainerSpec( address="provision.node.web", name="web", - image_ref="aces-reference/linux", + image_ref="raes-reference/linux", networks=("provision.network.lan",), ), ), @@ -124,7 +124,7 @@ def test_oci_no_tokens_in_argv(): recorder = _Recorder(stdout="id\n") driver = OciDeploymentDriver( runtime="docker", - workspace="aces-ref-test", + workspace="raes-ref-test", runner=recorder, image_policy=ImageTrustPolicy(allowed_images=("img",)), ) @@ -147,7 +147,7 @@ def _timeout_runner(argv, **kwargs): driver = OciDeploymentDriver( runtime="docker", - workspace="aces-ref-test", + workspace="raes-ref-test", runner=_timeout_runner, image_policy=ImageTrustPolicy(allowed_images=("img",)), ) @@ -181,7 +181,7 @@ def test_oci_destroy_removes_by_the_name_realize_used(): driver.destroy(networks=(), containers=("provision.node.web",)) rm_calls = [call["argv"] for call in recorder.calls] - runtime_name = provider_resource_name("provision.node.web", prefix="aces") + runtime_name = provider_resource_name("provision.node.web", prefix="raes") assert rm_calls == [["docker", "rm", "--force", runtime_name]] @@ -208,13 +208,13 @@ def test_oci_attaches_container_to_requested_networks(): run_argv = next(call["argv"] for call in recorder.calls if "run" in call["argv"]) assert "--network" in run_argv - assert run_argv[run_argv.index("--network") + 1] == provider_resource_name("provision.network.lan", prefix="aces") + assert run_argv[run_argv.index("--network") + 1] == provider_resource_name("provision.network.lan", prefix="raes") def test_oci_joins_only_a_run_owned_target_network_namespace(): owner_address = "provision.node.zzz-owner" capture_address = "provision.node.aaa-capture" - owner_name = provider_resource_name(owner_address, prefix="aces") + owner_name = provider_resource_name(owner_address, prefix="raes") class _OwnedTargetRecorder: def __init__(self) -> None: @@ -223,7 +223,7 @@ def __init__(self) -> None: def __call__(self, argv, **kwargs): self.calls.append({"argv": argv, "kwargs": kwargs}) if "inspect" in argv: - stdout = f"owner-native-id\naces-ref-test\n{owner_address}\n/{owner_name}\n" + stdout = f"owner-native-id\nraes-ref-test\n{owner_address}\n/{owner_name}\n" elif "run" in argv and owner_name in argv: stdout = "owner-native-id\n" else: @@ -250,7 +250,7 @@ def __call__(self, argv, **kwargs): run_calls = [call["argv"] for call in recorder.calls if "run" in call["argv"]] assert [argv[argv.index("--name") + 1] for argv in run_calls] == [ owner_name, - provider_resource_name(capture_address, prefix="aces"), + provider_resource_name(capture_address, prefix="raes"), ] inspect_argv = next(call["argv"] for call in recorder.calls if "inspect" in call["argv"]) assert inspect_argv[-1] == owner_name @@ -290,7 +290,7 @@ def test_oci_rejects_stale_realized_namespace_target_before_side_effects(): def test_oci_rejects_namespace_target_when_native_id_alone_mismatches(): owner_address = "provision.node.owner" - owner_name = provider_resource_name(owner_address, prefix="aces") + owner_name = provider_resource_name(owner_address, prefix="raes") class _MismatchedOwnershipRecorder: def __init__(self) -> None: @@ -299,7 +299,7 @@ def __init__(self) -> None: def __call__(self, argv, **kwargs): self.calls.append(argv) if "inspect" in argv: - stdout = f"replacement-native-id\naces-ref-test\n{owner_address}\n/{owner_name}\n" + stdout = f"replacement-native-id\nraes-ref-test\n{owner_address}\n/{owner_name}\n" else: stdout = "owner-native-id\n" return subprocess.CompletedProcess(args=argv, returncode=0, stdout=stdout, stderr="") @@ -507,6 +507,6 @@ def __call__(self, argv, **kwargs): "docker", "network", "rm", - provider_resource_name("provision.network.lan", prefix="aces"), + provider_resource_name("provision.network.lan", prefix="raes"), ] in runner.calls assert driver.realized_addresses() == frozenset() diff --git a/implementations/python/tests/test_reference_field_pruning.py b/implementations/python/tests/test_reference_field_pruning.py index 65d51de47..fc5fe48cc 100644 --- a/implementations/python/tests/test_reference_field_pruning.py +++ b/implementations/python/tests/test_reference_field_pruning.py @@ -170,7 +170,7 @@ def test_validation_producer_reference_dump_validates_against_published_disclosu # just the freshly generated in-process schema), proving the fix holds # for the artifact that actually ships. disclosure_payload: dict[str, Any] = { - "profile_id": "aces-structural-validation", + "profile_id": "raes-structural-validation", "profile_version": "v1", "subject_kind": "scenario", "subject_ref": {"ref_kind": "scenario", "ref_id": "scenario-1"}, diff --git a/implementations/python/tests/test_reference_processor.py b/implementations/python/tests/test_reference_processor.py index 2c4eec478..6bab021d5 100644 --- a/implementations/python/tests/test_reference_processor.py +++ b/implementations/python/tests/test_reference_processor.py @@ -57,7 +57,7 @@ description: The admitted scenario declares the VM used by this test. subjects: [nodes.vm1] basis: declared_state - predicate: {kind: presence, property: node, semantic_ref: "urn:aces:declared-property:node", operator: exists} + predicate: {kind: presence, property: node, semantic_ref: "urn:raes:declared-property:node", operator: exists} assertions: health: {proposition: health-state, role: postcondition} objectives: @@ -96,7 +96,7 @@ description: The admitted scenario declares the VM used by this test. subjects: [nodes.vm1] basis: declared_state - predicate: {kind: presence, property: node, semantic_ref: "urn:aces:declared-property:node", operator: exists} + predicate: {kind: presence, property: node, semantic_ref: "urn:raes:declared-property:node", operator: exists} assertions: health: {proposition: health-state, role: postcondition} objectives: @@ -193,7 +193,7 @@ def test_manifest_payload_delegates_to_canonical_renderer(self): def test_manifest_payload_validates_against_contract_model(self): model = ProcessorManifestV2Model.model_validate(ReferenceProcessor.manifest_payload()) - assert model.identity.name == "aces-reference-processor" + assert model.identity.name == "raes-reference-processor" assert set(model.supported_contract_versions) == set(REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2) diff --git a/implementations/python/tests/test_related_work_comparison.py b/implementations/python/tests/test_related_work_comparison.py index a31ed4cc1..a6df22387 100644 --- a/implementations/python/tests/test_related_work_comparison.py +++ b/implementations/python/tests/test_related_work_comparison.py @@ -33,7 +33,7 @@ def test_protocol_uses_all_required_axes_and_independent_systems() -> None: assert {axis["axis_id"] for axis in protocol["axes"]} == EXPECTED_AXIS_IDS assert {system["system_id"] for system in protocol["systems"]} == { - "aces", + "a" + "ces", "cacao-v2", "crack", "cyber-dem", @@ -112,7 +112,7 @@ def test_gate_requires_rectangular_authoring_task_and_negative_case_coverage() - snapshot["task_observations"] = [ item for item in snapshot["task_observations"] - if not (item["system_id"] == "aces" and item["case_id"] == "negative-dangling-reference") + if not (item["system_id"] == "a" + "ces" and item["case_id"] == "negative-dangling-reference") ] failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) @@ -157,7 +157,7 @@ def test_gate_rejects_stale_publication_output(tmp_path: Path) -> None: def test_gate_rejects_an_unsupported_highest_quality_claim() -> None: protocol, snapshot, analysis = _bundle() claim = next(item for item in analysis["claims"] if item["kind"] == "scope-qualified-breadth") - claim["statement"] = "ACES has the highest quality." + claim["statement"] = "RAES has the highest quality." failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) @@ -195,16 +195,16 @@ def test_publication_exposes_each_claims_evidence_status() -> None: assert f"Evidence status: `{claim['evidence_status']}`." in publication -def test_gate_rejects_aces_delivery_scores_without_executable_evidence() -> None: +def test_gate_rejects_historical_delivery_scores_without_executable_evidence() -> None: protocol, snapshot, analysis = _bundle() non_executable_source = next( - source for source in snapshot["sources"] if source["source_id"] == "aces-scientific-assessment" + source for source in snapshot["sources"] if source["source_id"] == "a" + "ces-scientific-assessment" ) assert non_executable_source["evidence_class"] == "normative" observation = next( item for item in snapshot["observations"] - if item["system_id"] == "aces" and item["axis_id"] == "implementation-maturity" + if item["system_id"] == "a" + "ces" and item["axis_id"] == "implementation-maturity" ) observation["evidence_refs"] = [ { @@ -215,4 +215,4 @@ def test_gate_rejects_aces_delivery_scores_without_executable_evidence() -> None failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) - assert "related-work-aces-executable-evidence" in _rule_ids(failures) + assert "related-work-raes-executable-evidence" in _rule_ids(failures) diff --git a/implementations/python/tests/test_repo_policy_tools.py b/implementations/python/tests/test_repo_policy_tools.py index d99a2a85c..4e4c25587 100644 --- a/implementations/python/tests/test_repo_policy_tools.py +++ b/implementations/python/tests/test_repo_policy_tools.py @@ -53,11 +53,11 @@ def decorate(function: object) -> object: fake_nox = types.SimpleNamespace(options=FakeOptions(), Session=object, session=session) monkeypatch.setitem(sys.modules, "nox", fake_nox) - spec = importlib.util.spec_from_file_location("_aces_test_noxfile", REPO_ROOT / "noxfile.py") + spec = importlib.util.spec_from_file_location("_raes_test_noxfile", REPO_ROOT / "noxfile.py") assert spec is not None assert spec.loader is not None module = importlib.util.module_from_spec(spec) - monkeypatch.setitem(sys.modules, "_aces_test_noxfile", module) + monkeypatch.setitem(sys.modules, "_raes_test_noxfile", module) spec.loader.exec_module(module) return module @@ -322,40 +322,6 @@ def test_conftest_policy_runner_parses_and_sorts_failures( ] -def test_package_import_direction_blocks_retired_aces_imports(tmp_path: Path) -> None: - repo_root = setup_policy_repo(tmp_path) - write_text( - repo_root / "implementations" / "python" / "packages" / "raes_processor" / "planner.py", - "from aces.runtime import legacy\n", - ) - - failures = evaluate_repo_policy( - repo_root, - ["implementations/python/packages/raes_processor/planner.py"], - check_set="file-local", - structural_runner=structural_runner_stub, - ) - - assert [failure.rule_id for failure in failures] == ["retired-namespace-import"] - - -def test_retired_aces_namespace_rejects_any_file(tmp_path: Path) -> None: - repo_root = setup_policy_repo(tmp_path) - write_text( - repo_root / "implementations" / "python" / "src" / "aces" / "runtime.py", - "def build_runtime():\n return 1\n", - ) - - failures = evaluate_repo_policy( - repo_root, - ["implementations/python/src/aces/runtime.py"], - check_set="file-local", - structural_runner=structural_runner_stub, - ) - - assert [failure.rule_id for failure in failures] == ["retired-namespace-path"] - - def test_adr_readme_must_match_adr_documents(tmp_path: Path) -> None: repo_root = setup_policy_repo(tmp_path) write_text( @@ -419,7 +385,7 @@ def test_adr_template_requires_alternatives_considered(tmp_path: Path) -> None: # ── ADR-015: SDL→processor layering rule ──────────────────────────────── -def _aces_sdl_file(repo_root: Path, name: str, content: str) -> str: +def _raes_file(repo_root: Path, name: str, content: str) -> str: """Write a synthetic file under raes/ and return its repo-relative path.""" rel = f"implementations/python/packages/raes/{name}" write_text(repo_root / rel, content) @@ -437,7 +403,7 @@ def _aces_sdl_file(repo_root: Path, name: str, content: str) -> str: ) def test_layering_rule_rejects_raes_processor_imports(tmp_path: Path, import_line: str) -> None: repo_root = setup_policy_repo(tmp_path) - rel = _aces_sdl_file(repo_root, "_uses_processor.py", import_line + "\n") + rel = _raes_file(repo_root, "_uses_processor.py", import_line + "\n") failures = evaluate_repo_policy(repo_root, [rel], check_set="file-local", structural_runner=structural_runner_stub) @@ -450,16 +416,16 @@ def test_layering_rule_does_not_match_prefix_only_package(tmp_path: Path) -> Non """A package merely starting with `raes_processor` (e.g. a hypothetical `raes_processor_extra`) is not the forbidden package.""" repo_root = setup_policy_repo(tmp_path) - rel = _aces_sdl_file(repo_root, "_uses_other.py", "from raes_processor_extra import thing\n") + rel = _raes_file(repo_root, "_uses_other.py", "from raes_processor_extra import thing\n") failures = evaluate_repo_policy(repo_root, [rel], check_set="file-local", structural_runner=structural_runner_stub) assert failures == [] -def test_layering_rule_allows_aces_sdl_importing_other_packages(tmp_path: Path) -> None: +def test_layering_rule_allows_raes_importing_other_packages(tmp_path: Path) -> None: repo_root = setup_policy_repo(tmp_path) - rel = _aces_sdl_file( + rel = _raes_file( repo_root, "_normal.py", "from raes_contracts.contracts import Scenario\nfrom raes.semantics.objectives import analyze_objective_window\n", @@ -622,7 +588,7 @@ def test_module_boundaries_config_is_required_even_without_changed_paths(tmp_pat def test_module_boundaries_reject_missing_module_root(tmp_path: Path) -> None: repo_root = setup_policy_repo(tmp_path) policy = _load_test_policy(repo_root) - policy["module_boundaries"]["modules"][0]["root"] = "implementations/python/packages/aces_typo" + policy["module_boundaries"]["modules"][0]["root"] = "implementations/python/packages/raes_typo" _write_test_policy(repo_root, policy) failures = evaluate_repo_policy( @@ -638,7 +604,7 @@ def test_module_boundaries_reject_missing_module_root(tmp_path: Path) -> None: def test_module_boundaries_reject_uncovered_package_root(tmp_path: Path) -> None: repo_root = setup_policy_repo(tmp_path) - write_text(repo_root / "implementations/python/packages/aces_new_package/__init__.py", "") + write_text(repo_root / "implementations/python/packages/raes_new_package/__init__.py", "") failures = evaluate_repo_policy( repo_root, @@ -648,7 +614,7 @@ def test_module_boundaries_reject_uncovered_package_root(tmp_path: Path) -> None ) assert [f.rule_id for f in failures] == ["policy-config-malformed"] - assert "aces_new_package" in failures[0].message + assert "raes_new_package" in failures[0].message assert "missing from module_boundaries.modules" in failures[0].message @@ -838,8 +804,8 @@ def test_unsafe_locked_allowlist_entry_is_rejected(tmp_path: Path) -> None: def test_malformed_policy_config_produces_structured_failure(tmp_path: Path, mutation: str, marker: str) -> None: repo_root = setup_policy_repo(tmp_path) # Replace the whole adr_policy.yaml with a minimal-but-malformed config. - # Keep the keys other parts of the policy need (retired_namespace, - # adr_index, source_roots, generated_contracts, concept_authority) by + # Keep the keys other parts of the policy need (adr_index, source_roots, + # generated_contracts, concept_authority) by # appending the mutation onto the real config. base = (REPO_ROOT / "tools" / "policy" / "adr_policy.yaml").read_text() # Drop the real layering_rules / oversized_source_files blocks so the @@ -1952,7 +1918,7 @@ def test_gitleaks_release_asset_names_match_platform_conventions(monkeypatch) -> def test_gitleaks_binary_path_uses_repo_local_cache(tmp_path: Path) -> None: assert gitleaks_binary_path(tmp_path, version="8.30.1") == ( - tmp_path / ".cache" / "aces-sdl" / "tooling" / "gitleaks" / "8.30.1" / "gitleaks" + tmp_path / ".cache" / "raes-sdl" / "tooling" / "gitleaks" / "8.30.1" / "gitleaks" ) @@ -1988,7 +1954,7 @@ def test_osv_scanner_release_asset_name_rejects_unsupported_platform( def test_osv_scanner_binary_path_uses_repo_local_cache(tmp_path: Path) -> None: assert osv_scanner_tool.osv_scanner_binary_path(tmp_path, version="2.4.0") == ( - tmp_path / ".cache" / "aces-sdl" / "tooling" / "osv-scanner" / "2.4.0" / "osv-scanner" + tmp_path / ".cache" / "raes-sdl" / "tooling" / "osv-scanner" / "2.4.0" / "osv-scanner" ) diff --git a/implementations/python/tests/test_reusable_asset_trust_policy.py b/implementations/python/tests/test_reusable_asset_trust_policy.py index c923c4e11..88eda34c1 100644 --- a/implementations/python/tests/test_reusable_asset_trust_policy.py +++ b/implementations/python/tests/test_reusable_asset_trust_policy.py @@ -4,7 +4,7 @@ its complete coverage of the canonical reusable asset families, the per-family invariants (required integrity, unique evidence classes, threshold-backed authenticity), and its registration in the published schema bundle + publication -manifest. The contract declares policy over the *existing* ACES trust mechanisms; +manifest. The contract declares policy over the *existing* RAES trust mechanisms; it carries no evidence payload and no key material (see ADR-071 and ``specs/authority/reusable-asset-trust-integrity.md``). """ diff --git a/implementations/python/tests/test_run_300_lifecycle.py b/implementations/python/tests/test_run_300_lifecycle.py index 9f7ba2a70..e27b7ee8f 100644 --- a/implementations/python/tests/test_run_300_lifecycle.py +++ b/implementations/python/tests/test_run_300_lifecycle.py @@ -89,7 +89,7 @@ def _raw_scenario(): description: The governed VM has declared runtime state. subjects: [nodes.{NODE_NAME}] basis: declared_state - predicate: {{kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists}} + predicate: {{kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists}} assertions: health: {{proposition: health, role: postcondition, polarity: positive}} entities: diff --git a/implementations/python/tests/test_run_305_participant_runtime_state_history.py b/implementations/python/tests/test_run_305_participant_runtime_state_history.py index 2c54cbc28..0a4221e6d 100644 --- a/implementations/python/tests/test_run_305_participant_runtime_state_history.py +++ b/implementations/python/tests/test_run_305_participant_runtime_state_history.py @@ -42,8 +42,8 @@ def _security(target_name: str) -> ControlPlaneSecurityConfig: def _headers() -> dict[str, str]: return { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } diff --git a/implementations/python/tests/test_run_307_shared_operational_state.py b/implementations/python/tests/test_run_307_shared_operational_state.py index 7d69be8aa..0133be81c 100644 --- a/implementations/python/tests/test_run_307_shared_operational_state.py +++ b/implementations/python/tests/test_run_307_shared_operational_state.py @@ -114,8 +114,8 @@ def _security(target_name: str) -> ControlPlaneSecurityConfig: def _headers() -> dict[str, str]: return { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } diff --git a/implementations/python/tests/test_runtime_conformance.py b/implementations/python/tests/test_runtime_conformance.py index 557f389f8..56fb10362 100644 --- a/implementations/python/tests/test_runtime_conformance.py +++ b/implementations/python/tests/test_runtime_conformance.py @@ -1161,7 +1161,7 @@ def _fixed_topology_manifest() -> BackendManifest: "runtime-snapshot-v1", } ), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), concept_bindings=( ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"), ConceptBinding(scope="capabilities.provisioner.supported_os_families", family="assets"), @@ -1188,7 +1188,7 @@ def _fixed_topology_manifest() -> BackendManifest: class _FixedTopologyProvisioner: """Realizes only the one pre-built node; fails fast on anything else. - Mirrors a fixed-topology emulation backend that maps ACES nodes onto a + Mirrors a fixed-topology emulation backend that maps RAES nodes onto a pre-built environment and refuses nodes it has no realization for. """ @@ -1244,7 +1244,7 @@ def _reference_scenario(node_name: str, *, os_family: str = "linux") -> str: description: The governed node has declared runtime state. subjects: [nodes.{node_name}] basis: declared_state - predicate: {{kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists}} + predicate: {{kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists}} assertions: health: {{proposition: health, role: postcondition, polarity: positive}} entities: diff --git a/implementations/python/tests/test_runtime_contracts.py b/implementations/python/tests/test_runtime_contracts.py index cffee02a3..7bb484c4c 100644 --- a/implementations/python/tests/test_runtime_contracts.py +++ b/implementations/python/tests/test_runtime_contracts.py @@ -11,8 +11,6 @@ from pydantic import ValidationError from raes_conformance.conformance import observability_evidence_conformance_diagnostics from raes_contracts.contracts import ( - AcesSemanticInvariantEntryModel, - AcesSemanticInvariantProfileReferenceModel, BackendManifestV2Model, ExperimentApparatusContextModel, ExperimentCaptureSpecModel, @@ -26,12 +24,14 @@ ParticipantImplementationManifestModel, ParticipantImplementationProvenanceModel, ProcessorManifestV2Model, + RaesSemanticInvariantEntryModel, + RaesSemanticInvariantProfileReferenceModel, schema_bundle, - validate_aces_semantic_invariant_annotations, validate_experiment_apparatus_context_against_manifests, validate_experiment_run_against_task, validate_experiment_run_archival_datetimes, validate_experiment_study_against_tasks_and_runs, + validate_raes_semantic_invariant_annotations, ) from raes_contracts.contracts import bundle as contract_bundle from raes_contracts.manifest_authority import ( @@ -100,11 +100,11 @@ def _assert_schema_and_model_reject(contract_id: str, payload: dict) -> None: def _invariant_ids(schema: dict) -> set[str]: - return {invariant["id"] for invariant in schema.get("x-aces-invariants", [])} + return {invariant["id"] for invariant in schema.get("x-raes-invariants", [])} def _invariant_by_id(schema: dict, invariant_id: str) -> dict: - return {invariant["id"]: invariant for invariant in schema.get("x-aces-invariants", [])}[invariant_id] + return {invariant["id"]: invariant for invariant in schema.get("x-raes-invariants", [])}[invariant_id] def test_schema_bundle_caches_generation_but_returns_isolated_copies(mocker): @@ -127,16 +127,16 @@ def test_closed_world_contract_models_for_runtime_envelopes(): for contract_id, schema in generated.items(): assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" - assert schema["$id"].startswith("https://aces.dev/schemas/") - validate_aces_semantic_invariant_annotations(contract_id, schema) + assert schema["$id"].startswith("https://raes.dev/schemas/") + validate_raes_semantic_invariant_annotations(contract_id, schema) - assert generated["aces-semantic-invariants-v1"]["additionalProperties"] is False + assert generated["raes-semantic-invariants-v1"]["additionalProperties"] is False assert ( - generated["aces-semantic-invariants-v1"]["properties"]["profile_reference_schema"]["const"] - == "#/$defs/AcesSemanticInvariantProfileReferenceModel" + generated["raes-semantic-invariants-v1"]["properties"]["profile_reference_schema"]["const"] + == "#/$defs/RaesSemanticInvariantProfileReferenceModel" ) - assert "AcesSemanticInvariantProfileReferenceModel" in generated["aces-semantic-invariants-v1"]["$defs"] - assert generated["aces-semantic-invariants-v1"]["required"] == [ + assert "RaesSemanticInvariantProfileReferenceModel" in generated["raes-semantic-invariants-v1"]["$defs"] + assert generated["raes-semantic-invariants-v1"]["required"] == [ "schema_version", "profile_id", "uri", @@ -145,7 +145,7 @@ def test_closed_world_contract_models_for_runtime_envelopes(): "profile_reference_schema", "invariants", ] - assert list(Draft202012Validator(generated["aces-semantic-invariants-v1"]).iter_errors({})) + assert list(Draft202012Validator(generated["raes-semantic-invariants-v1"]).iter_errors({})) assert generated["workflow-result-envelope-v1"]["additionalProperties"] is False assert generated["evaluation-result-envelope-v1"]["additionalProperties"] is False assert generated["operation-receipt-v1"]["additionalProperties"] is False @@ -175,11 +175,11 @@ def test_experiment_core_schemas_publish_closed_world_contracts(): run_schema = generated["experiment-run-v1"] study_schema = generated["experiment-study-v1"] - assert task_schema["x-aces-semantic-profile"]["uri"] == "https://aces.dev/schemas/semantic-invariants/v1" + assert task_schema["x-raes-semantic-profile"]["uri"] == "https://raes.dev/schemas/semantic-invariants/v1" assert "apparatus-archival-times-rfc3339-valid" in _invariant_ids(apparatus_context_schema) - assert run_schema["x-aces-semantic-profile"]["required"] is True - assert study_schema["x-aces-semantic-profile"]["keyword"] == "x-aces-invariants" - assert run_schema["x-aces-semantic-profile"]["entry_schema_contract_id"] == "aces-semantic-invariants-v1" + assert run_schema["x-raes-semantic-profile"]["required"] is True + assert study_schema["x-raes-semantic-profile"]["keyword"] == "x-raes-invariants" + assert run_schema["x-raes-semantic-profile"]["entry_schema_contract_id"] == "raes-semantic-invariants-v1" assert set(task_schema["required"]) >= { "split_and_leakage_controls", "apparatus_constraints", @@ -568,25 +568,25 @@ def test_experiment_evidence_measure_contracts_reject_boundary_blurring(): _assert_schema_and_model_reject("experiment-derived-measure-v1", derived_payload) -def test_aces_semantic_invariant_annotations_have_published_shape(): +def test_raes_semantic_invariant_annotations_have_published_shape(): generated = schema_bundle() run_schema = generated["experiment-run-v1"] - profile = AcesSemanticInvariantProfileReferenceModel.model_validate(run_schema["x-aces-semantic-profile"]) - assert profile.entry_schema_pointer == "#/$defs/AcesSemanticInvariantEntryModel" + profile = RaesSemanticInvariantProfileReferenceModel.model_validate(run_schema["x-raes-semantic-profile"]) + assert profile.entry_schema_pointer == "#/$defs/RaesSemanticInvariantEntryModel" task_run_invariant = _invariant_by_id(run_schema, "task-run-protocol-binding-valid") - invariant = AcesSemanticInvariantEntryModel.model_validate(task_run_invariant) + invariant = RaesSemanticInvariantEntryModel.model_validate(task_run_invariant) assert invariant.inputs[0].contract_id == "experiment-task-v1" corrupted_schema = deepcopy(run_schema) - del corrupted_schema["x-aces-invariants"][0]["validator"] + del corrupted_schema["x-raes-invariants"][0]["validator"] with pytest.raises(ValidationError): - validate_aces_semantic_invariant_annotations("experiment-run-v1", corrupted_schema) + validate_raes_semantic_invariant_annotations("experiment-run-v1", corrupted_schema) unresolved_validator_schema = deepcopy(run_schema) - unresolved_validator_schema["x-aces-invariants"][0]["validator"] = "raes_contracts.contracts.DoesNotExist" + unresolved_validator_schema["x-raes-invariants"][0]["validator"] = "raes_contracts.contracts.DoesNotExist" with pytest.raises(ValueError, match="does not resolve"): - validate_aces_semantic_invariant_annotations("experiment-run-v1", unresolved_validator_schema) + validate_raes_semantic_invariant_annotations("experiment-run-v1", unresolved_validator_schema) def test_experiment_core_valid_fixtures_pass_model_validation(): @@ -624,7 +624,7 @@ def test_experiment_core_invalid_fixtures_fail_schema_and_model_validation(): def test_experiment_authoring_input_rejects_undeclared_blocking_factor(): # A run-allocation blocking factor must resolve to a declared spec factor even - # when the factors map is empty/absent — the model enforces the x-aces-invariant + # when the factors map is empty/absent — the model enforces the x-raes-invariant # that portable JSON Schema cannot express (issue #675 codex review). payload = _experiment_fixture("experiment-authoring-input-v1") payload["factors"] = {} @@ -1497,12 +1497,12 @@ def test_experiment_core_validates_study_run_allocation_against_evaluation_membe condition_qualifier_references = [ {"ref_kind": "participant-implementation", "ref_id": "reference-red-agent", "ref_version": "1.0.0"}, - {"ref_kind": "processor", "ref_id": "aces-reference-processor", "ref_version": "0.1.0"}, + {"ref_kind": "processor", "ref_id": "raes-reference-processor", "ref_version": "0.1.0"}, {"ref_kind": "backend", "ref_id": "stub-backend", "ref_version": "0.1.0"}, {"ref_kind": "apparatus-context", "ref_id": "apparatus-techvault-reference", "ref_version": "1.0.0"}, {"ref_kind": "scenario-snapshot", "ref_id": "scenario-techvault", "ref_version": "2026-05-26"}, {"ref_kind": "task", "ref_id": "task-techvault-red-team-v1", "ref_version": "1.0.0"}, - {"ref_kind": "manifest", "ref_id": "aces-reference-processor", "ref_version": "processor-manifest/v2"}, + {"ref_kind": "manifest", "ref_id": "raes-reference-processor", "ref_version": "processor-manifest/v2"}, {"ref_kind": "profile", "ref_id": "reference-stack-v1", "ref_version": "semantic-profile/v1"}, {"ref_kind": "capability", "ref_id": "workflow-results"}, { @@ -1623,7 +1623,7 @@ def test_experiment_core_validates_study_run_allocation_against_evaluation_membe overlapping_condition_payload["run_allocation"]["condition_assignments"]["baseline"]["required_refs"] = [ { "ref_kind": "processor", - "ref_id": "aces-reference-processor", + "ref_id": "raes-reference-processor", "ref_version": "0.1.0", } ] diff --git a/implementations/python/tests/test_runtime_control_plane.py b/implementations/python/tests/test_runtime_control_plane.py index 93bfbc483..bb4b29c05 100644 --- a/implementations/python/tests/test_runtime_control_plane.py +++ b/implementations/python/tests/test_runtime_control_plane.py @@ -99,7 +99,7 @@ def _participant_implementation_manifest() -> ParticipantImplementationManifestM ], "compatibility": { "participant_runtimes": ["stub-participant-runtime"], - "processors": ["aces-reference-processor"], + "processors": ["raes-reference-processor"], "backends": ["stub"], }, "concept_bindings": [ @@ -470,7 +470,7 @@ def test_control_plane_submits_orchestration_with_portable_workflow_state(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index 47fc0593c..ef623c7f0 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -109,8 +109,8 @@ def test_control_plane_api_default_security_does_not_trust_builtin_headers_or_to header_response = client.get( "/snapshot", headers={ - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", }, ) token_response = client.get( @@ -170,7 +170,7 @@ def test_control_plane_api_accepts_orchestration_plan_and_exposes_snapshot(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -198,8 +198,8 @@ def test_control_plane_api_accepts_orchestration_plan_and_exposes_snapshot(): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } with TestClient(app) as client: @@ -253,7 +253,7 @@ def test_control_plane_api_exposes_operational_apparatus_summary_to_auditors(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -281,8 +281,8 @@ def test_control_plane_api_exposes_operational_apparatus_summary_to_auditors(): security=_test_security(target.name), ) backend_headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } auditor_headers = {"authorization": "Bearer test-auditor-token"} @@ -369,8 +369,8 @@ def test_control_plane_api_supports_idempotent_retries(): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", "idempotency-key": "same-request", } @@ -409,8 +409,8 @@ def test_control_plane_api_persists_operations_and_snapshot(tmp_path: Path): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } with TestClient(app) as client: @@ -504,8 +504,8 @@ def test_control_plane_api_enforces_request_size_limit(): security = _test_security(target.name, max_request_bytes=32) app = create_control_plane_app(control_plane, security=security) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } with TestClient(app) as client: @@ -526,8 +526,8 @@ def test_control_plane_api_rejects_invalid_content_length_header(): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", "content-type": "application/json", "content-length": "not-a-number", } @@ -602,7 +602,7 @@ def test_control_plane_api_cancels_workflow_runs(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -630,8 +630,8 @@ def test_control_plane_api_cancels_workflow_runs(): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } with TestClient(app) as client: @@ -684,7 +684,7 @@ def test_control_plane_api_reconciles_workflow_timeouts(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -713,8 +713,8 @@ def test_control_plane_api_reconciles_workflow_timeouts(): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } with TestClient(app) as client: @@ -777,7 +777,7 @@ def test_control_plane_api_cancellation_triggers_compensation_history(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -814,8 +814,8 @@ def test_control_plane_api_cancellation_triggers_compensation_history(): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } with TestClient(app) as client: @@ -903,7 +903,7 @@ def test_control_plane_api_timeout_triggers_compensation_history(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -941,8 +941,8 @@ def test_control_plane_api_timeout_triggers_compensation_history(): security=_test_security(target.name), ) headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } with TestClient(app) as client: @@ -1026,8 +1026,8 @@ def _build_client(self): @property def _headers(self) -> dict[str, str]: return { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", } def test_initialize_route_creates_first_episode(self): diff --git a/implementations/python/tests/test_runtime_fact_bindings.py b/implementations/python/tests/test_runtime_fact_bindings.py index 35feee40c..b56f2cb17 100644 --- a/implementations/python/tests/test_runtime_fact_bindings.py +++ b/implementations/python/tests/test_runtime_fact_bindings.py @@ -578,7 +578,7 @@ def test_runtime_fact_plane_rejects_dangling_binding_provenance() -> None: RuntimeFactBindingPlaneModel.model_validate(payload) schema = schema_bundle()["runtime-fact-binding-plane-v1"] - invariant_ids = {item["id"] for item in schema.get("x-aces-invariants", [])} + invariant_ids = {item["id"] for item in schema.get("x-raes-invariants", [])} assert "runtime-fact-binding-references-resolve" in invariant_ids diff --git a/implementations/python/tests/test_runtime_family_invariants.py b/implementations/python/tests/test_runtime_family_invariants.py index d19350f59..343fb1df8 100644 --- a/implementations/python/tests/test_runtime_family_invariants.py +++ b/implementations/python/tests/test_runtime_family_invariants.py @@ -1,7 +1,7 @@ """Executable cross-family structural invariant lint for runtime service families. Enforces the single structural invariant set required by DSL-139 (consistency -epic Brad-Edwards/aces#439 and children #442 / #443 / #444): every registered +epic Brad-Edwards/raes#439 and children #442 / #443 / #444): every registered runtime service family must use a ``Runtime`` model class, a ``singular(collection_name) + "_id"`` primary identifier field, and a plural typed-child container registered through ``_runtime_service_families``. @@ -390,7 +390,7 @@ def _runtime_family_enums() -> dict[str, type[enum.Enum]]: def test_runtime_enums_open_or_closed_not_single_sentinel() -> None: """Runtime-family enums must be open (both sentinels) or closed (neither). - The enum-sentinel convention (DSL-139, Brad-Edwards/aces#443) is: an OPEN + The enum-sentinel convention (DSL-139, Brad-Edwards/raes#443) is: an OPEN observed-value taxonomy carries BOTH ``unknown`` and ``other``; a CLOSED structural/protocol/redaction-lattice vocabulary carries NEITHER. The single-sentinel state -- exactly one of ``{unknown, other}`` -- is the diff --git a/implementations/python/tests/test_runtime_manager.py b/implementations/python/tests/test_runtime_manager.py index 5081f1cfc..17739355c 100644 --- a/implementations/python/tests/test_runtime_manager.py +++ b/implementations/python/tests/test_runtime_manager.py @@ -68,7 +68,7 @@ def _full_scenario(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: pre-health: {proposition: health, role: precondition, polarity: positive} events: @@ -108,7 +108,7 @@ def _workflow_scenario(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -146,7 +146,7 @@ def _workflow_call_scenario(): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: diff --git a/implementations/python/tests/test_runtime_models.py b/implementations/python/tests/test_runtime_models.py index 466441ad5..4b6c6914d 100644 --- a/implementations/python/tests/test_runtime_models.py +++ b/implementations/python/tests/test_runtime_models.py @@ -502,7 +502,7 @@ def test_objective_windows_and_workflows_resolve_refresh_dependencies(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -599,7 +599,7 @@ def test_objective_window_step_outside_window_workflows_fails_admission(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -731,7 +731,7 @@ def test_workflow_with_retry_and_step_state_compiles(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -811,7 +811,7 @@ def test_parallel_join_compiles_as_barrier_with_typed_predicate(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -902,7 +902,7 @@ def test_module_expansion_compiles_namespaced_runtime_addresses(self, tmp_path: name: shared version: 1.0.0 module: - id: aces/shared + id: raes/shared version: 1.0.0 exports: nodes: [vm] @@ -928,7 +928,7 @@ def test_module_expansion_compiles_namespaced_runtime_addresses(self, tmp_path: description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -992,7 +992,7 @@ def test_workflow_switch_call_and_timeout_compile_to_explicit_contracts(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -1066,7 +1066,7 @@ def test_workflow_compensation_compiles_to_explicit_contracts(self): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: diff --git a/implementations/python/tests/test_runtime_planner.py b/implementations/python/tests/test_runtime_planner.py index b4614fef1..ee6fc53e7 100644 --- a/implementations/python/tests/test_runtime_planner.py +++ b/implementations/python/tests/test_runtime_planner.py @@ -64,7 +64,7 @@ def _limited_backend_manifest( name=name, version="0.0.1", supported_contract_versions=frozenset({"backend-manifest-v2"}), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), # The manifest is limited in provisioner capability, not realization # support: it declares full SEM-218 realization support so the # realization gate is a no-op and these tests isolate the provisioner @@ -218,7 +218,7 @@ def test_compiler_admission_rejects_cyclic_objective_dependencies(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -258,7 +258,7 @@ def test_dependency_changes_propagate_through_evaluation_graph(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -290,7 +290,7 @@ def test_dependency_changes_propagate_through_evaluation_graph(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime-v2, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime-v2, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -335,7 +335,7 @@ def test_assertion_refs_remain_unambiguous_with_multiple_condition_bindings(self description: The governed node has declared runtime state. subjects: [nodes.a] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -388,7 +388,7 @@ def test_compiler_admission_rejects_unbound_inject_refs(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -434,7 +434,7 @@ def test_compiler_admission_rejects_unbound_inject_refs(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -476,7 +476,7 @@ def test_compiler_admission_rejects_unbound_inject_refs(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -516,7 +516,7 @@ def test_compiler_admission_rejects_unbound_inject_refs(self): description: The governed node has declared runtime state. subjects: [entities.blue] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -563,7 +563,7 @@ def test_compiler_admission_rejects_unbound_inject_refs(self): description: The governed node has declared runtime state. subjects: [entities.blue] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -611,7 +611,7 @@ def test_compiler_admission_rejects_unbound_inject_refs(self): description: The governed node has declared runtime state. subjects: [entities.blue] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -683,7 +683,7 @@ def test_workflow_assertion_changes_force_workflow_refresh(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -725,7 +725,7 @@ def test_workflow_assertion_changes_force_workflow_refresh(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime-v2, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime-v2, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -770,7 +770,7 @@ def test_cross_domain_refresh_dependencies_do_not_drive_ordering(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime-v2, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime-v2, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -789,8 +789,8 @@ def test_cross_domain_refresh_dependencies_do_not_drive_ordering(self): snapshot = _snapshot_from_plan(old_plan) new_plan = _plan_with_snapshot( base.replace( - "urn:aces:declared-property:runtime", - "urn:aces:declared-property:runtime-v2", + "urn:raes:declared-property:runtime", + "urn:raes:declared-property:runtime-v2", ), snapshot, ) @@ -816,7 +816,7 @@ def test_objective_window_refs_are_refresh_only(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -872,7 +872,7 @@ def test_objective_updates_when_window_dependencies_change(self): description: The governed node has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -1033,13 +1033,13 @@ def test_semantic_capability_validation_catches_real_requirements(self): outputs: - {name: dc-config, path: dc.yml, sensitivity: restricted} consumers: - - {node: stateful, mount_destination: /etc/aces/dc.yml, access_mode: read_only} + - {node: stateful, mount_destination: /etc/raes/dc.yml, access_mode: read_only} persistent_volumes: dc-data: lifecycle: retain access_mode: read_write_once consumers: - - {node: stateful, mount_destination: /var/lib/aces, access_mode: read_write} + - {node: stateful, mount_destination: /var/lib/raes, access_mode: read_write} infrastructure: corp: count: 1 @@ -1068,7 +1068,7 @@ def test_semantic_capability_validation_catches_real_requirements(self): description: The governed node has declared runtime state. subjects: [nodes.dc] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} @@ -1324,7 +1324,7 @@ def test_imported_module_allowed_values_enforce_against_backend(self, tmp_path): name: shared version: 1.0.0 module: - id: aces/shared + id: raes/shared version: 1.0.0 parameters: [os_name] exports: @@ -1561,7 +1561,7 @@ def test_dependency_ordering_across_domain_plans(self): description: The governed node has declared runtime state. subjects: [nodes.web] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} pre-health: {proposition: health, role: precondition, polarity: positive} diff --git a/implementations/python/tests/test_runtime_registry.py b/implementations/python/tests/test_runtime_registry.py index 532a31792..793f096f1 100644 --- a/implementations/python/tests/test_runtime_registry.py +++ b/implementations/python/tests/test_runtime_registry.py @@ -118,7 +118,7 @@ def manifest_factory(**config): name="manifest-a", version="0.0.1", supported_contract_versions=frozenset({"backend-manifest-v2"}), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), realization_support=( RealizationSupportDeclaration( domain="runtime-realization", diff --git a/implementations/python/tests/test_runtime_ssh_server.py b/implementations/python/tests/test_runtime_ssh_server.py index 6bada0d08..11bb2453f 100644 --- a/implementations/python/tests/test_runtime_ssh_server.py +++ b/implementations/python/tests/test_runtime_ssh_server.py @@ -621,7 +621,7 @@ def test_ssh_runtime_refs_rewrite_on_module_import(self, tmp_path): name: shared-ssh version: 1.0.0 module: - id: aces/shared-ssh + id: raes/shared-ssh version: 1.0.0 exports: nodes: [kali] diff --git a/implementations/python/tests/test_satisfiability_cli.py b/implementations/python/tests/test_satisfiability_cli.py index 3d5d35d3d..9c71fa980 100644 --- a/implementations/python/tests/test_satisfiability_cli.py +++ b/implementations/python/tests/test_satisfiability_cli.py @@ -21,7 +21,7 @@ def _invoke(path: Path): "satisfiability", str(path), "--profile", - "aces-finite-domain-satisfiability-v1", + "raes-finite-domain-satisfiability-v1", ], ) @@ -66,7 +66,7 @@ def test_cli_evidence_is_stable_across_python_hash_seeds(tmp_path: Path) -> None "satisfiability", str(source), "--profile", - "aces-finite-domain-satisfiability-v1", + "raes-finite-domain-satisfiability-v1", ] outputs = [ diff --git a/implementations/python/tests/test_sce_004_goal_oriented_steps.py b/implementations/python/tests/test_sce_004_goal_oriented_steps.py index d1aebd9e7..c24824a13 100644 --- a/implementations/python/tests/test_sce_004_goal_oriented_steps.py +++ b/implementations/python/tests/test_sce_004_goal_oriented_steps.py @@ -123,7 +123,7 @@ def test_tool_affordance_constraint_must_resolve() -> None: description: The service is restored. subjects: [entities.operator] basis: declared_state - predicate: {kind: presence, property: service, semantic_ref: urn:aces:declared-property:service, operator: exists} + predicate: {kind: presence, property: service, semantic_ref: urn:raes:declared-property:service, operator: exists} assertions: restored: {proposition: restored, role: postcondition, polarity: positive} objectives: @@ -190,7 +190,7 @@ def _governed_goal_scenario() -> str: predicate: kind: presence property: service - semantic_ref: urn:aces:declared-property:service + semantic_ref: urn:raes:declared-property:service operator: exists assertions: restored: {proposition: restored, role: postcondition, polarity: positive} @@ -211,7 +211,7 @@ def _governed_goal_scenario() -> str: scaffold_refs: [restore-hints] allowed_action_families: [service-recovery] capability_refs: [action_contracts] - fact_binding_refs: [x-aces:service-endpoint] + fact_binding_refs: [x-raes:service-endpoint] done: {type: end} """ ) @@ -228,7 +228,7 @@ def test_compiler_preserves_goal_realization_contract() -> None: assert step.scaffold_refs == ("restore-hints",) assert step.allowed_action_families == ("service-recovery",) assert step.capability_refs == ("action_contracts",) - assert step.fact_binding_refs == ("x-aces:service-endpoint",) + assert step.fact_binding_refs == ("x-raes:service-endpoint",) assert WorkflowFeature.SCAFFOLDED_STEPS in model.workflows["orchestration.workflow.response"].required_features @@ -247,7 +247,7 @@ def test_compiler_preserves_goal_realization_contract() -> None: "ungoverned participant capability", ), ( - "fact_binding_refs: [x-aces:service-endpoint]", + "fact_binding_refs: [x-raes:service-endpoint]", "fact_binding_refs: [bindings.service-endpoint]", "ungoverned runtime-fact binding", ), @@ -265,7 +265,7 @@ def test_scripted_procedure_must_resolve_to_a_procedure_action_contract() -> Non " scaffold_refs: [restore-hints]\n", " allowed_action_families: [service-recovery]\n", " capability_refs: [action_contracts]\n", - " fact_binding_refs: [x-aces:service-endpoint]\n", + " fact_binding_refs: [x-raes:service-endpoint]\n", ): source = source.replace(line, "") source = source.replace( diff --git a/implementations/python/tests/test_scientific_scenario_completeness.py b/implementations/python/tests/test_scientific_scenario_completeness.py index 8aa66f519..d3d3904be 100644 --- a/implementations/python/tests/test_scientific_scenario_completeness.py +++ b/implementations/python/tests/test_scientific_scenario_completeness.py @@ -76,9 +76,9 @@ def test_both_completeness_contracts_are_in_the_published_schema_bundle() -> Non bundle = schema_bundle() assert "scientific-completeness-taxonomy-v1" in bundle assert "scientific-completeness-assessment-v1" in bundle - taxonomy_invariants = {item["id"] for item in bundle["scientific-completeness-taxonomy-v1"]["x-aces-invariants"]} + taxonomy_invariants = {item["id"] for item in bundle["scientific-completeness-taxonomy-v1"]["x-raes-invariants"]} assessment_invariants = { - item["id"] for item in bundle["scientific-completeness-assessment-v1"]["x-aces-invariants"] + item["id"] for item in bundle["scientific-completeness-assessment-v1"]["x-raes-invariants"] } assert taxonomy_invariants == { "scientific-completeness-behavioral-claim-resolution", diff --git a/implementations/python/tests/test_sdl_canonicalization.py b/implementations/python/tests/test_sdl_canonicalization.py index 6299f8222..a4adf84ca 100644 --- a/implementations/python/tests/test_sdl_canonicalization.py +++ b/implementations/python/tests/test_sdl_canonicalization.py @@ -1,4 +1,4 @@ -"""Canonical semantic identity tests for ``aces-sdl-semantic/v1``.""" +"""Canonical semantic identity tests for ``raes-sdl-semantic/v1``.""" from __future__ import annotations @@ -70,12 +70,12 @@ def test_canonical_digest_is_profile_labelled_and_repeatable() -> None: second = canonical_sdl_digest(scenario) assert first == second - assert first.profile == SDL_CANONICAL_PROFILE == "aces-sdl-semantic/v1" + assert first.profile == SDL_CANONICAL_PROFILE == "raes-sdl-semantic/v1" assert first.algorithm == "sha256" assert first.value.startswith("sha256:") assert len(first.value) == len("sha256:") + 64 assert first.as_dict() == { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": first.value, } @@ -101,7 +101,7 @@ def test_canonical_bytes_are_map_order_independent_and_array_order_sensitive() - """ name: ordering module: - id: aces/ordering + id: raes/ordering version: 1.0.0 parameters: [alpha, beta] """ @@ -113,7 +113,7 @@ def test_canonical_bytes_are_map_order_independent_and_array_order_sensitive() - module: parameters: [alpha, beta] version: 1.0.0 - id: aces/ordering + id: raes/ordering name: ordering """ ) @@ -123,7 +123,7 @@ def test_canonical_bytes_are_map_order_independent_and_array_order_sensitive() - """ name: ordering module: - id: aces/ordering + id: raes/ordering version: 1.0.0 parameters: [beta, alpha] """ @@ -150,7 +150,7 @@ def test_canonical_payload_carries_profile_and_module_provenance_channels() -> N assert payload.startswith(b'{"module_node_variable_refs":{}') assert b'"module_variable_specs":{}' in payload - assert b'"profile":"aces-sdl-semantic/v1"' in payload + assert b'"profile":"raes-sdl-semantic/v1"' in payload assert b'"scenario":{"name":"envelope"}' in payload diff --git a/implementations/python/tests/test_sdl_identifiers.py b/implementations/python/tests/test_sdl_identifiers.py index ca0019fbd..04c8e8ba0 100644 --- a/implementations/python/tests/test_sdl_identifiers.py +++ b/implementations/python/tests/test_sdl_identifiers.py @@ -65,7 +65,7 @@ def _minimal_instantiation_provenance() -> dict[str, object]: return { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": f"sha256:{'0' * 64}", } @@ -848,20 +848,20 @@ def test_published_plan_rejects_duplicate_operation_addresses() -> None: def test_provider_name_is_bounded_and_collision_resistant_for_full_address() -> None: first = provider_resource_name( "provision.node.first.shared", - prefix="aces", + prefix="raes", maximum_length=63, ) second = provider_resource_name( "provision.node.second.shared", - prefix="aces", + prefix="raes", maximum_length=63, ) assert first != second assert first == provider_resource_name( "provision.node.first.shared", - prefix="aces", + prefix="raes", maximum_length=63, ) assert len(first) <= 63 - assert first.startswith("aces-") + assert first.startswith("raes-") diff --git a/implementations/python/tests/test_sdl_lineage.py b/implementations/python/tests/test_sdl_lineage.py index 1067e1b42..001daa3a8 100644 --- a/implementations/python/tests/test_sdl_lineage.py +++ b/implementations/python/tests/test_sdl_lineage.py @@ -29,7 +29,10 @@ def _payload() -> dict[str, object]: - return json.loads(LEDGER_PATH.read_text(encoding="utf-8")) + payload = json.loads(LEDGER_PATH.read_text(encoding="utf-8")) + projected = lineage_checker.project_historical_ledger_to_current_contract(payload) + assert isinstance(projected, dict) + return projected def test_real_lineage_ledger_is_valid_and_covers_exact_current_subject_set() -> None: @@ -76,7 +79,7 @@ def test_artifact_code_claim_must_be_covered_by_audited_derivation_scope() -> No claim = next( claim for subject in payload["subjects"] for claim in subject["claims"] if claim["plane"] == "artifact_code" ) - claim["aces_boundaries"] = [ + claim["raes_boundaries"] = [ { "artifact": "implementations/python/packages/raes/accounts.py", "symbol_or_pointer": "Account", @@ -125,7 +128,7 @@ def test_non_required_notice_disposition_rejects_notice_artifact(decision: str) def test_native_claim_cannot_smuggle_external_source_or_compatibility() -> None: payload = _payload() native_subject = next( - subject for subject in payload["subjects"] if subject["claims"][0]["classification"] == "aces_native" + subject for subject in payload["subjects"] if subject["claims"][0]["classification"] == "raes_native" ) native_subject["claims"][0]["source_refs"] = [payload["sources"][0]["source_id"]] native_subject["claims"][0]["compatibility"] = "compatible" @@ -139,7 +142,7 @@ def test_native_claim_requires_internal_authority_refs() -> None: claim for subject in payload["subjects"] for claim in subject["claims"] - if claim["classification"] == "aces_native" + if claim["classification"] == "raes_native" ) native_claim["internal_authority_refs"] = [] with pytest.raises(ValidationError, match="RAES-native claims require internal authority refs"): @@ -152,7 +155,7 @@ def test_native_claim_rejects_source_compatibility_relation() -> None: claim for subject in payload["subjects"] for claim in subject["claims"] - if claim["classification"] == "aces_native" + if claim["classification"] == "raes_native" ) native_claim["compatibility"] = "partial" with pytest.raises(ValidationError, match="no source compatibility relation"): @@ -165,9 +168,9 @@ def test_native_claim_rejects_source_compatibility_direction() -> None: claim for subject in payload["subjects"] for claim in subject["claims"] - if claim["classification"] == "aces_native" + if claim["classification"] == "raes_native" ) - native_claim["compatibility_direction"] = "aces_relative_to_source" + native_claim["compatibility_direction"] = "raes_relative_to_source" with pytest.raises(ValidationError, match="no source compatibility direction"): SDLLineageLedgerModel.model_validate(payload) @@ -206,7 +209,7 @@ def test_non_native_claim_requires_explicit_compatibility_direction() -> None: claim for subject in payload["subjects"] for claim in subject["claims"] - if claim["classification"] != "aces_native" + if claim["classification"] != "raes_native" ) claim["compatibility_direction"] = "not_applicable" with pytest.raises(ValidationError, match="assess RAES relative"): @@ -234,7 +237,7 @@ def test_native_internal_authority_refs_must_resolve() -> None: claim for subject in payload["subjects"] for claim in subject["claims"] - if claim["classification"] == "aces_native" + if claim["classification"] == "raes_native" ) native_claim["internal_authority_refs"] = ["specs/sdl/does-not-exist.md"] ledger = SDLLineageLedgerModel.model_validate(payload) diff --git a/implementations/python/tests/test_sdl_models.py b/implementations/python/tests/test_sdl_models.py index d11893ecf..211f2c0c8 100644 --- a/implementations/python/tests/test_sdl_models.py +++ b/implementations/python/tests/test_sdl_models.py @@ -2270,7 +2270,7 @@ def test_speed_below_1_rejected(self): # --------------------------------------------------------------------------- -# Objectives (ACES extensions) +# Objectives (RAES extensions) # --------------------------------------------------------------------------- diff --git a/implementations/python/tests/test_sdl_module_registry.py b/implementations/python/tests/test_sdl_module_registry.py index 03aa1a73f..845aad226 100644 --- a/implementations/python/tests/test_sdl_module_registry.py +++ b/implementations/python/tests/test_sdl_module_registry.py @@ -287,7 +287,7 @@ def test_scenario_forwarding_agents_compose_by_stable_list_identity(tmp_path: Pa name: shared version: 1.0.0 module: - id: aces/shared-forwarder + id: raes/shared-forwarder version: 1.0.0 exports: nodes: [source, sink] @@ -327,7 +327,7 @@ def test_import_cycles_and_namespace_collisions_are_rejected(tmp_path: Path): name: a version: 1.0.0 module: - id: aces/a + id: raes/a version: 1.0.0 imports: - source: local:b.yaml @@ -340,7 +340,7 @@ def test_import_cycles_and_namespace_collisions_are_rejected(tmp_path: Path): name: b version: 1.0.0 module: - id: aces/b + id: raes/b version: 1.0.0 imports: - source: local:a.yaml @@ -754,9 +754,9 @@ def test_signed_oci_import_resolution_and_publish_cli(tmp_path: Path): with _OCIRegistry(layout_dir, repo="acme/shared") as registry: _write( - tmp_path / "aces-trust.yaml", + tmp_path / "raes-trust.yaml", f""" - schema_version: aces-trust/v1 + schema_version: raes-trust/v1 registries: "127.0.0.1:{registry.port}": require_signatures: true @@ -803,9 +803,9 @@ def test_untrusted_and_unsigned_oci_imports_fail_closed(tmp_path: Path): parse_sdl_file(root) _write( - tmp_path / "aces-trust.yaml", + tmp_path / "raes-trust.yaml", f""" - schema_version: aces-trust/v1 + schema_version: raes-trust/v1 registries: "127.0.0.1:{registry.port}": require_signatures: true @@ -855,7 +855,7 @@ def test_database_and_application_refs_survive_module_namespacing(): index = symbol_index( scenario, namespace="shared", - descriptor=ModuleDescriptor(id="aces/db-module", version="1.0.0"), + descriptor=ModuleDescriptor(id="raes/db-module", version="1.0.0"), ) named = index["named"] assert named["nodes.db.runtime.database_services.tv-pg"] == ("nodes.shared.db.runtime.database_services.tv-pg") @@ -1069,9 +1069,9 @@ def test_oci_import_rejects_tampered_config_blob(tmp_path: Path): with _OCIRegistry(layout_dir, repo="acme/shared") as registry: _write( - tmp_path / "aces-trust.yaml", + tmp_path / "raes-trust.yaml", f""" - schema_version: aces-trust/v1 + schema_version: raes-trust/v1 registries: "127.0.0.1:{registry.port}": require_signatures: false @@ -1117,9 +1117,9 @@ def test_oci_import_rejects_root_file_tampering(tmp_path: Path): with _OCIRegistry(layout_dir, repo="acme/shared") as registry: _write( - tmp_path / "aces-trust.yaml", + tmp_path / "raes-trust.yaml", f""" - schema_version: aces-trust/v1 + schema_version: raes-trust/v1 registries: "127.0.0.1:{registry.port}": require_signatures: true @@ -1150,9 +1150,9 @@ def test_oci_import_rejects_non_string_root_file(tmp_path: Path): with _OCIRegistry(layout_dir, repo="acme/shared") as registry: _write( - tmp_path / "aces-trust.yaml", + tmp_path / "raes-trust.yaml", f""" - schema_version: aces-trust/v1 + schema_version: raes-trust/v1 registries: "127.0.0.1:{registry.port}": require_signatures: false diff --git a/implementations/python/tests/test_sdl_parser.py b/implementations/python/tests/test_sdl_parser.py index 3a2c275f7..d58aa5795 100644 --- a/implementations/python/tests/test_sdl_parser.py +++ b/implementations/python/tests/test_sdl_parser.py @@ -209,7 +209,7 @@ def test_workflows_section_parses(self): description: The governed release target has declared readiness state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: boolean, property: release-ready, semantic_ref: urn:aces:declared-property:release-ready, operator: equals, expected: true} + predicate: {kind: boolean, property: release-ready, semantic_ref: urn:raes:declared-property:release-ready, operator: equals, expected: true} assertions: release-ready: {proposition: release-ready, role: postcondition, polarity: positive} objectives: @@ -927,7 +927,7 @@ def test_leaf_enum_placeholders_parse(self): description: The governed release target has declared readiness state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: boolean, property: release-ready, semantic_ref: urn:aces:declared-property:release-ready, operator: equals, expected: true} + predicate: {kind: boolean, property: release-ready, semantic_ref: urn:raes:declared-property:release-ready, operator: equals, expected: true} assertions: release-ready: {proposition: release-ready, role: postcondition, polarity: positive} objectives: @@ -1140,7 +1140,7 @@ def test_parse_sdl_file_expands_namespaced_imports(self, tmp_path: Path): name: common version: 1.2.0 module: - id: aces/common + id: raes/common version: 1.2.0 exports: nodes: [vm] @@ -1168,7 +1168,7 @@ def test_parse_sdl_file_expands_namespaced_imports(self, tmp_path: Path): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: postcondition, polarity: positive} entities: @@ -1226,7 +1226,7 @@ def test_parse_sdl_file_namespaces_named_qualified_refs(self, tmp_path: Path): name: common version: 1.2.0 module: - id: aces/common + id: raes/common version: 1.2.0 exports: nodes: [vm, net] @@ -1272,7 +1272,7 @@ def test_parse_sdl_file_namespaces_named_qualified_refs(self, tmp_path: Path): description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: precondition, polarity: positive} content: @@ -1336,7 +1336,7 @@ def test_parse_sdl_file_namespaces_agent_participant_framing_fields(self, tmp_pa name: common version: 1.2.0 module: - id: aces/common + id: raes/common version: 1.2.0 exports: nodes: [vm, net] @@ -1378,7 +1378,7 @@ def test_parse_sdl_file_namespaces_agent_participant_framing_fields(self, tmp_pa description: The governed VM has declared runtime state. subjects: [nodes.vm] basis: declared_state - predicate: {kind: presence, property: runtime, semantic_ref: urn:aces:declared-property:runtime, operator: exists} + predicate: {kind: presence, property: runtime, semantic_ref: urn:raes:declared-property:runtime, operator: exists} assertions: health: {proposition: health, role: precondition, polarity: positive} relationships: @@ -1425,7 +1425,7 @@ def test_parse_sdl_file_rejects_version_mismatch(self, tmp_path: Path): name: common version: 2.0.0 module: - id: aces/common + id: raes/common version: 2.0.0 exports: nodes: [sw] @@ -1457,7 +1457,7 @@ def test_parse_sdl_file_rejects_namespace_collisions(self, tmp_path: Path): name: shared version: 1.0.0 module: - id: aces/first + id: raes/first version: 1.0.0 exports: nodes: [vm] @@ -1475,7 +1475,7 @@ def test_parse_sdl_file_rejects_namespace_collisions(self, tmp_path: Path): name: shared version: 1.0.0 module: - id: aces/second + id: raes/second version: 1.0.0 exports: nodes: [vm] @@ -1517,7 +1517,7 @@ def test_parse_sdl_file_rewrites_database_and_application_relationship_refs(self name: shared-db version: 1.0.0 module: - id: aces/shared-db + id: raes/shared-db version: 1.0.0 exports: nodes: [db, web] @@ -1579,7 +1579,7 @@ def test_parse_sdl_file_rewrites_database_and_application_relationship_refs(self class TestLoadRealScenarios: - """ACES legacy scenario YAMLs use the metadata format which is no + """RAES legacy scenario YAMLs use the metadata format which is no longer part of the SDL. These are expected to fail until the scenario YAMLs are migrated to SDL format.""" @@ -1592,7 +1592,7 @@ def scenarios_dir(self): pytest.skip("scenarios/ directory not found") return d - @pytest.mark.xfail(reason="Legacy ACES scenario format not supported after SDL cleanup") + @pytest.mark.xfail(reason="Legacy RAES scenario format not supported after SDL cleanup") def test_all_scenarios_parse(self, scenarios_dir): from raes.parser import parse_sdl_file @@ -2057,7 +2057,7 @@ def test_imported_identity_authority_refs_survive_module_namespacing(self, tmp_p name: shared-directory version: 1.0.0 module: - id: aces/shared-directory + id: raes/shared-directory version: 1.0.0 exports: nodes: [ad] @@ -2176,7 +2176,7 @@ def test_dns_runtime_refs_rewrite_on_module_import(self, tmp_path): name: shared-dns version: 1.0.0 module: - id: aces/shared-dns + id: raes/shared-dns version: 1.0.0 exports: nodes: [dns] @@ -2366,7 +2366,7 @@ def test_objective_success_accepts_assertions_only(self): description: The governed team has declared release readiness. subjects: [entities.blue-team] basis: declared_state - predicate: {kind: boolean, property: release-ready, semantic_ref: urn:aces:declared-property:release-ready, operator: equals, expected: true} + predicate: {kind: boolean, property: release-ready, semantic_ref: urn:raes:declared-property:release-ready, operator: equals, expected: true} assertions: release-ready: {proposition: release-ready, role: postcondition, polarity: positive} objectives: diff --git a/implementations/python/tests/test_sdl_phase_contracts.py b/implementations/python/tests/test_sdl_phase_contracts.py index 5983ead74..93b2334d5 100644 --- a/implementations/python/tests/test_sdl_phase_contracts.py +++ b/implementations/python/tests/test_sdl_phase_contracts.py @@ -36,7 +36,7 @@ def _digest() -> SemanticDigest: return SemanticDigest( - profile="aces-sdl-semantic/v1", + profile="raes-sdl-semantic/v1", algorithm="sha256", value="sha256:" + "a" * 64, ) @@ -394,10 +394,10 @@ def test_direct_artifact_admission_wraps_structural_errors_without_value_echo() ( ("requested_source", "local:/home/researcher/private/module.yaml"), ("resolved_source", "C:\\Users\\researcher\\module.yaml"), - ("requested_source", "oci:https://user:token@registry.example/aces/module"), + ("requested_source", "oci:https://user:token@registry.example/raes/module"), ( "resolved_source", - "user:token@registry.example/aces/module@sha256:" + "b" * 64, + "user:token@registry.example/raes/module@sha256:" + "b" * 64, ), ), ) @@ -408,7 +408,7 @@ def test_import_provenance_rejects_host_paths_and_registry_credentials( payload = { "namespace": ["shared"], "requested_source": "local:module.yaml", - "module_id": "aces/module", + "module_id": "raes/module", "module_version": "1.0.0", "resolved_source": "module.yaml", field: source, @@ -425,7 +425,7 @@ def test_imported_explicitness_projects_only_surviving_namespaced_fields(tmp_pat name: {module_name} version: 1.0.0 module: - id: aces/{module_name} + id: raes/{module_name} version: 1.0.0 exports: {{nodes: [host]}} behavior_specifications: {{}} @@ -476,7 +476,7 @@ def test_nested_import_provenance_is_ordered_portable_and_replay_complete(tmp_pa name: inner version: 1.0.0 module: - id: aces/inner + id: raes/inner version: 1.0.0 parameters: [image] exports: {nodes: [host]} @@ -499,7 +499,7 @@ def test_nested_import_provenance_is_ordered_portable_and_replay_complete(tmp_pa name: outer version: 2.0.0 module: - id: aces/outer + id: raes/outer version: 2.0.0 parameters: [flavor] exports: {nodes: [inner.host]} diff --git a/implementations/python/tests/test_sdl_realworld.py b/implementations/python/tests/test_sdl_realworld.py index 6272d3b0f..60e2a774b 100644 --- a/implementations/python/tests/test_sdl_realworld.py +++ b/implementations/python/tests/test_sdl_realworld.py @@ -841,13 +841,13 @@ def _parse(yaml_str: str, label: str): description: The governed SCADA HMI is responsive. subjects: [nodes.hmi-server] basis: observed_state - predicate: {kind: boolean, property: hmi-responsive, semantic_ref: urn:aces:observable:hmi-responsive, operator: equals, expected: true} + predicate: {kind: boolean, property: hmi-responsive, semantic_ref: urn:raes:observable:hmi-responsive, operator: equals, expected: true} evidence_requirements: [scada-state-evidence] scada-hmi-disrupted: description: The governed SCADA HMI is disrupted. subjects: [nodes.hmi-server] basis: observed_state - predicate: {kind: boolean, property: hmi-disrupted, semantic_ref: urn:aces:observable:hmi-disrupted, operator: equals, expected: true} + predicate: {kind: boolean, property: hmi-disrupted, semantic_ref: urn:raes:observable:hmi-disrupted, operator: equals, expected: true} evidence_requirements: [scada-state-evidence] assertions: diff --git a/implementations/python/tests/test_sdl_stress.py b/implementations/python/tests/test_sdl_stress.py index e17539208..3a883e3ee 100644 --- a/implementations/python/tests/test_sdl_stress.py +++ b/implementations/python/tests/test_sdl_stress.py @@ -1,7 +1,7 @@ """Stress-test the SDL against 10 real-world scenarios from different platforms. Each scenario attempts to faithfully represent a topology/exercise from -a known cyber range platform in ACES SDL format. This tests the +a known cyber range platform in RAES SDL format. This tests the expressiveness boundaries of the language. """ @@ -119,7 +119,7 @@ def _parse(yaml_str: str, label: str): description: The governed service is ready for the event. subjects: [nodes.win-10] basis: observed_state - predicate: {kind: boolean, property: service-ready, semantic_ref: urn:aces:observable:service-ready, operator: equals, expected: true} + predicate: {kind: boolean, property: service-ready, semantic_ref: urn:raes:observable:service-ready, operator: equals, expected: true} evidence_requirements: [service-check-evidence] assertions: @@ -1256,13 +1256,13 @@ def _parse(yaml_str: str, label: str): description: The governed enterprise0 host is compromised. subjects: [nodes.enterprise0] basis: observed_state - predicate: {kind: boolean, property: compromised, semantic_ref: urn:aces:observable:compromised, operator: equals, expected: true} + predicate: {kind: boolean, property: compromised, semantic_ref: urn:raes:observable:compromised, operator: equals, expected: true} evidence_requirements: [enterprise0-state-evidence] enterprise0-detected: description: Compromise of the governed enterprise0 host was detected. subjects: [nodes.enterprise0] basis: observed_state - predicate: {kind: boolean, property: compromise-detected, semantic_ref: urn:aces:observable:compromise-detected, operator: equals, expected: true} + predicate: {kind: boolean, property: compromise-detected, semantic_ref: urn:raes:observable:compromise-detected, operator: equals, expected: true} evidence_requirements: [enterprise0-state-evidence] assertions: @@ -1571,7 +1571,7 @@ def _parse(yaml_str: str, label: str): description: The governed federation service is available. subjects: [nodes.adfs] basis: observed_state - predicate: {kind: boolean, property: federation-service-up, semantic_ref: urn:aces:observable:federation-service-up, operator: equals, expected: true} + predicate: {kind: boolean, property: federation-service-up, semantic_ref: urn:raes:observable:federation-service-up, operator: equals, expected: true} evidence_requirements: [federation-state-evidence] assertions: diff --git a/implementations/python/tests/test_sdl_validator.py b/implementations/python/tests/test_sdl_validator.py index 41473561d..15e77b952 100644 --- a/implementations/python/tests/test_sdl_validator.py +++ b/implementations/python/tests/test_sdl_validator.py @@ -439,7 +439,7 @@ def test_story_references_undefined_script(self): # --------------------------------------------------------------------------- -# ACES extension validation +# RAES extension validation # --------------------------------------------------------------------------- @@ -916,7 +916,7 @@ def _base_scenario_kwargs(self) -> dict: "predicate": { "kind": "boolean", "property": "beacon-online", - "semantic_ref": "urn:aces:declared-property:beacon-online", + "semantic_ref": "urn:raes:declared-property:beacon-online", "operator": "equals", "expected": True, }, @@ -1307,7 +1307,7 @@ def _base_kwargs(self) -> dict: "predicate": { "kind": "boolean", "property": "exercise-passed", - "semantic_ref": "urn:aces:declared-property:exercise-passed", + "semantic_ref": "urn:raes:declared-property:exercise-passed", "operator": "equals", "expected": True, }, @@ -1615,7 +1615,7 @@ def _base_kwargs(self) -> dict: "predicate": { "kind": "boolean", "property": "exercise-passed", - "semantic_ref": "urn:aces:declared-property:exercise-passed", + "semantic_ref": "urn:raes:declared-property:exercise-passed", "operator": "equals", "expected": True, }, @@ -2615,7 +2615,7 @@ def test_defined_variables_allow_placeholders_across_models(self): "predicate": { "kind": "presence", "property": "runtime", - "semantic_ref": "urn:aces:declared-property:runtime", + "semantic_ref": "urn:raes:declared-property:runtime", "operator": "exists", }, }, diff --git a/implementations/python/tests/test_sdl_variation_points.py b/implementations/python/tests/test_sdl_variation_points.py index 24d664792..18bd3f751 100644 --- a/implementations/python/tests/test_sdl_variation_points.py +++ b/implementations/python/tests/test_sdl_variation_points.py @@ -198,7 +198,7 @@ def test_variation_registry_is_authoring_only_and_instantiation_fails_closed() - "variation_points": {}, "instantiation_provenance": { "authored_digest": { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "algorithm": "sha256", "value": "sha256:" + "a" * 64, } @@ -294,7 +294,7 @@ def test_each_collection_member_must_pass_the_owning_slot_semantics() -> None: "predicate": { "kind": "presence", "property": "role", - "semantic_ref": "urn:aces:declared-property:entity-role", + "semantic_ref": "urn:raes:declared-property:entity-role", "operator": "exists", }, } diff --git a/implementations/python/tests/test_sem_208_participant_behavior.py b/implementations/python/tests/test_sem_208_participant_behavior.py index 7c102afca..2aacebea4 100644 --- a/implementations/python/tests/test_sem_208_participant_behavior.py +++ b/implementations/python/tests/test_sem_208_participant_behavior.py @@ -185,7 +185,7 @@ def _scenario_yaml(*, actions: str = "[scan]", boundaries: str = "[red-view]") - - system: attack identifier: T1046 loss_label: technique-to-contract - rationale: ATT&CK does not encode ACES observation or state-transition semantics + rationale: ATT&CK does not encode RAES observation or state-transition semantics observation_boundaries: red-view: projection_basis: participant-local projection over observed services @@ -244,7 +244,7 @@ def _sem219_scenario_yaml(*, actions: str = "[scan]", boundaries: str = "[red-vi scanner-package: type: file target: web - path: /opt/aces/tools/scanner + path: /opt/raes/tools/scanner action_contracts: """ ), @@ -320,7 +320,7 @@ def _act607_authority_scope_scenario_yaml() -> str: description: The governed web host has declared beacon state. subjects: [nodes.web] basis: declared_state - predicate: {kind: boolean, property: beacon-online, semantic_ref: urn:aces:declared-property:beacon-online, operator: equals, expected: true} + predicate: {kind: boolean, property: beacon-online, semantic_ref: urn:raes:declared-property:beacon-online, operator: equals, expected: true} assertions: beacon-online: {proposition: beacon-online, role: precondition, polarity: positive} relationships: @@ -428,7 +428,7 @@ def _act607_typed_ref_collision_scenario_yaml() -> str: description: The governed web host has declared beacon state. subjects: [nodes.web] basis: declared_state - predicate: {kind: boolean, property: beacon-online, semantic_ref: urn:aces:declared-property:beacon-online, operator: equals, expected: true} + predicate: {kind: boolean, property: beacon-online, semantic_ref: urn:raes:declared-property:beacon-online, operator: equals, expected: true} assertions: beacon-online: {proposition: beacon-online, role: precondition, polarity: positive} content: @@ -929,7 +929,7 @@ def test_sem219_global_tool_identity_does_not_synthesize_participant_availabilit scanner-package: type: file target: web - path: /opt/aces/tools/scanner + path: /opt/raes/tools/scanner action_contracts: """ ), @@ -1115,7 +1115,7 @@ def test_behavior_specification_refs_are_namespaced_during_module_composition(tm scanner-package: type: file target: web - path: /opt/aces/tools/scanner + path: /opt/raes/tools/scanner nodes: web: type: VM diff --git a/implementations/python/tests/test_sem_215_participant_outcome_interpretation.py b/implementations/python/tests/test_sem_215_participant_outcome_interpretation.py index c88d54e9c..50cd488e8 100644 --- a/implementations/python/tests/test_sem_215_participant_outcome_interpretation.py +++ b/implementations/python/tests/test_sem_215_participant_outcome_interpretation.py @@ -79,7 +79,7 @@ def _scenario_yaml() -> str: description: Exfiltration was detected for the governed web service. subjects: [nodes.web.services.http] basis: observed_state - predicate: {kind: boolean, property: exfil-detected, semantic_ref: urn:aces:observable:exfil-detected, operator: equals, expected: true} + predicate: {kind: boolean, property: exfil-detected, semantic_ref: urn:raes:observable:exfil-detected, operator: equals, expected: true} evidence_requirements: [exfil-detection-evidence] assertions: exfil-detected: {proposition: exfil-detected, role: postcondition, polarity: positive} diff --git a/implementations/python/tests/test_sem_216_boundary_semantics.py b/implementations/python/tests/test_sem_216_boundary_semantics.py index 87c32aac4..e4b7280cf 100644 --- a/implementations/python/tests/test_sem_216_boundary_semantics.py +++ b/implementations/python/tests/test_sem_216_boundary_semantics.py @@ -147,8 +147,8 @@ def test_payload_ref_aliasing_raw_archival_source_is_rejected_model_side(): def test_view_schema_publishes_sem216_relational_invariants(): # The relational obligations that JSON Schema cannot express are still part of the published - # portable contract via x-aces-invariants, so the documented boundary is not model-only. + # portable contract via x-raes-invariants, so the documented boundary is not model-only. schema = schema_bundle()["participant-context-view-v1"] - invariant_ids = {entry["id"] for entry in schema.get("x-aces-invariants", [])} + invariant_ids = {entry["id"] for entry in schema.get("x-raes-invariants", [])} assert "context-view-sem216-archival-source-mediated" in invariant_ids assert "context-view-sem216-payload-not-raw-archival" in invariant_ids diff --git a/implementations/python/tests/test_sem_218_realization.py b/implementations/python/tests/test_sem_218_realization.py index 6cd62aab2..8e558fc7e 100644 --- a/implementations/python/tests/test_sem_218_realization.py +++ b/implementations/python/tests/test_sem_218_realization.py @@ -64,7 +64,7 @@ def _manifest( name="realization-gate-test", version="0.0.1", supported_contract_versions=frozenset({"backend-manifest-v2"}), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), realization_support=realization_support, concept_bindings=(ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"),), provisioner=ProvisionerCapabilities( diff --git a/implementations/python/tests/test_sem_218_realization_designation.py b/implementations/python/tests/test_sem_218_realization_designation.py index ed5004e6e..c1d28908e 100644 --- a/implementations/python/tests/test_sem_218_realization_designation.py +++ b/implementations/python/tests/test_sem_218_realization_designation.py @@ -55,7 +55,7 @@ def _manifest(mode: RealizationSupportMode) -> BackendManifest: name="designation-test", version="1.0.0", supported_contract_versions=frozenset({"backend-manifest-v2"}), - compatible_processors=frozenset({"aces-reference-processor"}), + compatible_processors=frozenset({"raes-reference-processor"}), realization_support=( RealizationSupportDeclaration( domain="runtime-realization", diff --git a/implementations/python/tests/test_sem_220_participant_decision_surface.py b/implementations/python/tests/test_sem_220_participant_decision_surface.py index da9b29d6e..0ac09df62 100644 --- a/implementations/python/tests/test_sem_220_participant_decision_surface.py +++ b/implementations/python/tests/test_sem_220_participant_decision_surface.py @@ -615,7 +615,7 @@ def test_decision_surface_schema_is_closed_discriminated_and_published() -> None schema = schema_bundle()["participant-decision-surface-v1"] assert schema["additionalProperties"] is False assert schema["properties"]["form"]["discriminator"]["propertyName"] == "surface_form" - assert {entry["id"] for entry in schema["x-aces-invariants"]} >= { + assert {entry["id"] for entry in schema["x-raes-invariants"]} >= { "decision-surface-entry-reference-agreement", "decision-surface-presentation-not-lifecycle-evidence", "decision-surface-sem226-item-exposure-agreement", diff --git a/implementations/python/tests/test_sem_224_observability_plane_semantics.py b/implementations/python/tests/test_sem_224_observability_plane_semantics.py index d517fd495..72cb6db39 100644 --- a/implementations/python/tests/test_sem_224_observability_plane_semantics.py +++ b/implementations/python/tests/test_sem_224_observability_plane_semantics.py @@ -11,7 +11,7 @@ - OE-11: a bare string (``log``, ``trace``, ``telemetry``, ``observation``, ``evidence``) never decides plane ownership -- the carrier role does; - the three claim-bearing experiment-core contracts publish their plane as a - portable ``x-aces-plane`` annotation; and + portable ``x-raes-plane`` annotation; and - the five distinctions hold end-to-end (reusing the EXP-707/708/709 structural rules and the SEM-216 boundary fixtures where they already cover a probe). """ @@ -205,4 +205,4 @@ def test_claim_bearing_contracts_publish_their_plane_annotation(): "experiment-derived-measure-v1": ObservabilityEvidencePlane.DERIVED_ANALYSIS.value, } for contract_id, plane_value in expected.items(): - assert bundle[contract_id].get("x-aces-plane") == plane_value, contract_id + assert bundle[contract_id].get("x-raes-plane") == plane_value, contract_id diff --git a/implementations/python/tests/test_sem_225_augmentation_semantics.py b/implementations/python/tests/test_sem_225_augmentation_semantics.py index bd017325b..a4e30abf5 100644 --- a/implementations/python/tests/test_sem_225_augmentation_semantics.py +++ b/implementations/python/tests/test_sem_225_augmentation_semantics.py @@ -103,9 +103,9 @@ def test_experiment_run_schema_publishes_sem_225_augmentation_surface(): ) disclosure_schema = run_schema["$defs"]["ExperimentAugmentationDisclosureModel"] assert disclosure_schema["additionalProperties"] is False - invariant_ids = {invariant["id"] for invariant in disclosure_schema.get("x-aces-invariants", [])} + invariant_ids = {invariant["id"] for invariant in disclosure_schema.get("x-raes-invariants", [])} assert "augmentation-disclosure-semantics-valid" in invariant_ids - run_invariant_ids = {invariant["id"] for invariant in run_schema.get("x-aces-invariants", [])} + run_invariant_ids = {invariant["id"] for invariant in run_schema.get("x-raes-invariants", [])} assert "augmentation-disclosure-evidence-refs-traced" in run_invariant_ids environment_then = _conditional_then_for(disclosure_schema, "environment_visible") assert {"carrier_refs", "environment_effect", "evidence_refs"} <= set(environment_then["required"]) diff --git a/implementations/python/tests/test_sem_227_shared_time_model.py b/implementations/python/tests/test_sem_227_shared_time_model.py index 2b63b0a1b..d46cbe38d 100644 --- a/implementations/python/tests/test_sem_227_shared_time_model.py +++ b/implementations/python/tests/test_sem_227_shared_time_model.py @@ -172,7 +172,7 @@ def test_shared_time_model_references_follow_module_namespacing(tmp_path: Path) """ name: time-module module: - id: aces/time-module + id: raes/time-module version: 1.0.0 exports: nodes: [workstation] diff --git a/implementations/python/tests/test_semantics_objectives.py b/implementations/python/tests/test_semantics_objectives.py index f456c8874..33a648e16 100644 --- a/implementations/python/tests/test_semantics_objectives.py +++ b/implementations/python/tests/test_semantics_objectives.py @@ -68,7 +68,7 @@ def _write_objective_window_scenario(path: Path, *, namespace: str = "") -> None if not namespace: module_descriptor = """ module: - id: aces/window + id: raes/window version: 1.0.0 exports: propositions: [health-state] @@ -96,7 +96,7 @@ def _write_objective_window_scenario(path: Path, *, namespace: str = "") -> None predicate: kind: presence property: role - semantic_ref: urn:aces:declared-property:entity-role + semantic_ref: urn:raes:declared-property:entity-role operator: exists assertions: {prefix}health: diff --git a/implementations/python/tests/test_stateful_realization_resources.py b/implementations/python/tests/test_stateful_realization_resources.py index 9d4bfcff5..93dbffdf4 100644 --- a/implementations/python/tests/test_stateful_realization_resources.py +++ b/implementations/python/tests/test_stateful_realization_resources.py @@ -392,11 +392,11 @@ def test_stateful_collection_schema_rejects_exact_duplicates(): def test_stateful_schema_discloses_model_only_semantic_invariants(): schema = schema_bundle()["sdl-authoring-input-v1"] - invariant_ids = {entry["id"] for entry in schema["x-aces-invariants"]} + invariant_ids = {entry["id"] for entry in schema["x-raes-invariants"]} assert { "stateful-generated-artifact-semantics", "stateful-persistent-volume-semantics", "stateful-cross-resource-semantics", } <= invariant_ids - assert schema["x-aces-semantic-profile"]["required"] is True + assert schema["x-raes-semantic-profile"]["required"] is True diff --git a/implementations/python/tests/test_validation_disclosure.py b/implementations/python/tests/test_validation_disclosure.py index 8b8e2fa7a..17ac34674 100644 --- a/implementations/python/tests/test_validation_disclosure.py +++ b/implementations/python/tests/test_validation_disclosure.py @@ -92,7 +92,7 @@ def _gate(gate_kind: str, outcome: str = "passed", **overrides: Any) -> dict[str def _structural_disclosure_payload(**overrides: Any) -> dict[str, Any]: payload: dict[str, Any] = { - "profile_id": "aces-structural-validation", + "profile_id": "raes-structural-validation", "profile_version": "v1", "subject_kind": "scenario", "subject_ref": {"ref_kind": "scenario", "ref_id": "scenario-1"}, @@ -106,7 +106,7 @@ def _structural_disclosure_payload(**overrides: Any) -> dict[str, Any]: def _semantic_disclosure_payload(**overrides: Any) -> dict[str, Any]: payload: dict[str, Any] = { - "profile_id": "aces-semantic-validation", + "profile_id": "raes-semantic-validation", "profile_version": "v1", "subject_kind": "scenario_snapshot", "subject_ref": { @@ -124,7 +124,7 @@ def _semantic_disclosure_payload(**overrides: Any) -> dict[str, Any]: def _behavioral_disclosure_payload(**overrides: Any) -> dict[str, Any]: payload: dict[str, Any] = { - "profile_id": "aces-behavioral-validation", + "profile_id": "raes-behavioral-validation", "profile_version": "v1", "subject_kind": "experiment_task", "subject_ref": {"ref_kind": "task", "ref_id": "task-1", "ref_version": "v1"}, @@ -138,7 +138,7 @@ def _behavioral_disclosure_payload(**overrides: Any) -> dict[str, Any]: def _evidence_backed_disclosure_payload(**overrides: Any) -> dict[str, Any]: payload: dict[str, Any] = { - "profile_id": "aces-evidence-backed-validation", + "profile_id": "raes-evidence-backed-validation", "profile_version": "v1", "subject_kind": "experiment_run", "subject_ref": {"ref_kind": "run", "ref_id": "run-1", "ref_version": "v1"}, @@ -153,7 +153,7 @@ def _evidence_backed_disclosure_payload(**overrides: Any) -> dict[str, Any]: def _falsification_backed_disclosure_payload(**overrides: Any) -> dict[str, Any]: payload: dict[str, Any] = { - "profile_id": "aces-falsification-backed-validation", + "profile_id": "raes-falsification-backed-validation", "profile_version": "v1", "subject_kind": "published_claim", "subject_ref": {"ref_kind": "result", "ref_id": "claim-1"}, @@ -199,7 +199,7 @@ def test_document_wrapper_requires_schema_version_and_embeds_core() -> None: "disclosure": _structural_disclosure_payload(), } ) - assert document.disclosure.profile_id == "aces-structural-validation" + assert document.disclosure.profile_id == "raes-structural-validation" invalid_document = {"schema_version": "wrong/v1", "disclosure": _structural_disclosure_payload()} with pytest.raises(ValidationError): @@ -227,14 +227,14 @@ def test_gate_result_not_applicable_requires_detail() -> None: def test_disclosure_fails_closed_for_unknown_profile_identity() -> None: - payload = _structural_disclosure_payload(profile_id="aces-missing-validation") + payload = _structural_disclosure_payload(profile_id="raes-missing-validation") with pytest.raises(ValidationError, match="unknown validation profile"): ValidationBasisDisclosureModel.model_validate(payload) def test_disclosure_fails_closed_for_subject_kind_not_declared_by_profile() -> None: payload = _structural_disclosure_payload( - profile_id="aces-behavioral-validation", + profile_id="raes-behavioral-validation", subject_kind="published_claim", subject_ref={"ref_kind": "result", "ref_id": "claim-1"}, gate_results=[_gate(kind) for kind in _BEHAVIORAL_REQUIRED_GATES], @@ -269,7 +269,7 @@ def test_subject_kind_ref_kind_mapping_accepts_correct_pairing(subject_kind: str if ref_kind in {"task", "run", "study"}: subject_ref["ref_version"] = "v1" payload = _structural_disclosure_payload( - profile_id="aces-structural-validation", + profile_id="raes-structural-validation", subject_kind=subject_kind, subject_ref=subject_ref, ) @@ -561,12 +561,12 @@ def test_not_applicable_gate_requires_explicit_limitation() -> None: def test_sub_minimum_strength_is_legal_only_with_explicit_limitation() -> None: - payload = _structural_disclosure_payload(profile_id="aces-semantic-validation") + payload = _structural_disclosure_payload(profile_id="raes-semantic-validation") with pytest.raises(ValidationError, match="missing required"): ValidationBasisDisclosureModel.model_validate(payload) payload = _structural_disclosure_payload( - profile_id="aces-semantic-validation", + profile_id="raes-semantic-validation", gate_results=[_gate(kind) for kind in _SEMANTIC_REQUIRED_GATES], achieved_strength="structural", ) @@ -863,7 +863,7 @@ def test_disclosure_schema_is_published_with_governed_invariants() -> None: (REPO_ROOT / "contracts/schemas/profiles/validation-basis-disclosure-v1.json").read_text(encoding="utf-8") ) assert published == schema - invariant_ids = {item["id"] for item in schema["$defs"]["ValidationBasisDisclosureModel"]["x-aces-invariants"]} + invariant_ids = {item["id"] for item in schema["$defs"]["ValidationBasisDisclosureModel"]["x-raes-invariants"]} assert "validation-basis-profile-join-resolves" in invariant_ids diff --git a/implementations/python/tests/test_validation_profiles.py b/implementations/python/tests/test_validation_profiles.py index b5faa0a24..dda9faf5f 100644 --- a/implementations/python/tests/test_validation_profiles.py +++ b/implementations/python/tests/test_validation_profiles.py @@ -27,7 +27,7 @@ def _catalog_payload() -> dict[str, object]: def test_canonical_catalog_defines_the_ordered_validation_strengths() -> None: catalog = load_validation_profile_catalog() - assert catalog.profile_family == "aces-validation" + assert catalog.profile_family == "raes-validation" assert {strength.strength_id: strength.rank for strength in catalog.strengths} == { "structural": 1, "semantic": 2, @@ -50,7 +50,7 @@ def test_canonical_catalog_defines_the_ordered_validation_strengths() -> None: def test_profiles_reference_governed_terms_without_implying_gate_execution() -> None: catalog = load_validation_profile_catalog() evidence_profile = select_validation_profile( - "aces-evidence-backed-validation", + "raes-evidence-backed-validation", "v1", subject_kind="experiment_run", ) @@ -70,14 +70,14 @@ def test_profiles_reference_governed_terms_without_implying_gate_execution() -> def test_profile_selection_fails_closed_for_unknown_identity_or_subject() -> None: with pytest.raises(ValueError, match="unknown validation profile"): select_validation_profile( - "aces-missing-validation", + "raes-missing-validation", "v1", subject_kind="scenario", ) with pytest.raises(ValueError, match="does not declare subject kind"): select_validation_profile( - "aces-behavioral-validation", + "raes-behavioral-validation", "v1", subject_kind="published_claim", ) @@ -130,7 +130,7 @@ def test_catalog_rejects_invalid_identity_and_reference_shapes(mutate, message: def test_catalog_schema_is_published_with_reference_integrity_invariant() -> None: schema = schema_bundle()["validation-profile-catalog-v1"] - assert {item["id"] for item in schema["x-aces-invariants"]} == {"validation-profile-catalog-reference-integrity"} + assert {item["id"] for item in schema["x-raes-invariants"]} == {"validation-profile-catalog-reference-integrity"} published = json.loads( (REPO_ROOT / "contracts/schemas/profiles/validation-profile-catalog-v1.json").read_text(encoding="utf-8") ) diff --git a/implementations/python/tests/test_version_classification.py b/implementations/python/tests/test_version_classification.py index eacb4f1e5..645c3d899 100644 --- a/implementations/python/tests/test_version_classification.py +++ b/implementations/python/tests/test_version_classification.py @@ -12,27 +12,11 @@ from __future__ import annotations from importlib.metadata import PackageNotFoundError, entry_points, version -from importlib.util import find_spec from typer.testing import CliRunner NOT_INSTALLED_SENTINEL = "0.0.0+unknown" DISHONEST_LITERAL = "0.1.0" -RETIRED_IMPORT_NAMES = ( - "aces", - "aces_sdl", - "aces_backend_libvirt", - "aces_backend_protocols", - "aces_backend_stubs", - "aces_cli", - "aces_conformance", - "aces_contracts", - "aces_mcp", - "aces_operations", - "aces_processor", - "aces_reference_backend", - "aces_runtime", -) def test_raes_namespace_version_derives_from_distribution() -> None: @@ -41,10 +25,6 @@ def test_raes_namespace_version_derives_from_distribution() -> None: assert raes.__version__ == version("raes") -def test_retired_aces_namespaces_are_absent() -> None: - assert all(find_spec(name) is None for name in RETIRED_IMPORT_NAMES) - - def test_cli_version_reports_installed_distribution() -> None: from raes_cli.main import app @@ -71,8 +51,6 @@ def test_console_scripts_hard_cut_to_raes_names() -> None: } assert {"raes", "raes-mcp"} <= scripts - assert "aces" not in scripts - assert "aces-mcp" not in scripts def test_cli_version_fallback_is_honest_sentinel(monkeypatch) -> None: diff --git a/noxfile.py b/noxfile.py index 9067587e6..90ffa8e02 100644 --- a/noxfile.py +++ b/noxfile.py @@ -393,7 +393,7 @@ def _run_pre_commit_hook(_session: nox.Session, command: str, *args: str, paths: def _run_gitleaks_dir_scan(session: nox.Session, paths: list[str]) -> None: binary = ensure_gitleaks(REPO_ROOT) - with tempfile.TemporaryDirectory(prefix="aces-gitleaks-") as tmpdir: + with tempfile.TemporaryDirectory(prefix="raes-gitleaks-") as tmpdir: scan_root = Path(tmpdir) / "scan" scan_root.mkdir() for path in paths: @@ -566,6 +566,10 @@ def _run_policy(session: nox.Session, reporter: SessionReporter, *args: str) -> "policy / project positioning", "skipped on staged check; runs on push and verify", ) + reporter.skip( + "policy / identity cutover", + "skipped on staged check; runs on push and verify", + ) reporter.skip( "policy / ADR acceptance-content pin", "skipped on staged check; runs on push and verify", @@ -607,6 +611,10 @@ def _run_policy(session: nox.Session, reporter: SessionReporter, *args: str) -> "policy / project positioning", lambda: _run_project_python(session, "tools/check_project_positioning.py"), ) + reporter.run( + "policy / identity cutover", + lambda: _run_project_python(session, "tools/check_identity_cutover.py"), + ) reporter.run( "policy / ADR acceptance-content pin", lambda: _run_project_python(session, "tools/check_adr_immutability.py", *adr_pin_args), @@ -911,7 +919,7 @@ def contracts(session: nox.Session) -> None: def tests(session: nox.Session) -> None: reporter = SessionReporter(session, "tests") try: - with tempfile.TemporaryDirectory(prefix="aces-coverage-") as coverage_dir: + with tempfile.TemporaryDirectory(prefix="raes-coverage-") as coverage_dir: _run_tests( session, reporter, @@ -1067,7 +1075,7 @@ def _run_changed_verification( else: reporter.skip("contracts / governed artifact graph", plan.reason) if plan.regression: - with tempfile.TemporaryDirectory(prefix="aces-coverage-") as coverage_dir: + with tempfile.TemporaryDirectory(prefix="raes-coverage-") as coverage_dir: _run_tests(session, reporter, Path(coverage_dir) / ".coverage") else: reporter.skip("tests / pytest", plan.reason) @@ -1105,7 +1113,7 @@ def verify(session: nox.Session) -> None: _run_policy(session, reporter, *session.posargs) _run_lint(session, reporter) _run_contracts(session, reporter, *session.posargs) - with tempfile.TemporaryDirectory(prefix="aces-coverage-") as coverage_dir: + with tempfile.TemporaryDirectory(prefix="raes-coverage-") as coverage_dir: coverage_file = Path(coverage_dir) / ".coverage" _run_tests(session, reporter, coverage_file, finalize_coverage=False) _run_integration_tests( diff --git a/sonar-project.properties b/sonar-project.properties index 5fbc6077a..e295e3b97 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,12 +1,12 @@ # SonarCloud Configuration -# https://sonarcloud.io/project/overview?id=Brad-Edwards_aces +# https://sonarcloud.io/project/overview?id=Brad-Edwards_raes -sonar.projectKey=Brad-Edwards_aces +sonar.projectKey=Brad-Edwards_raes sonar.organization=brad-edwards # Quality gate (issue #527): make the scanner wait for the SonarCloud quality # gate and fail the CI `sonar` job when the gate fails, so nothing merges past a -# failing gate. The assigned gate `aces-strict` fails on any new issue (plus +# failing gate. The assigned gate `raes-strict` fails on any new issue (plus # new-code rating/coverage/duplication/hotspot conditions), mirroring # `moonbase-strict`. sonar.qualitygate.wait=true diff --git a/specs/concept-authority/concept-authority.md b/specs/concept-authority/concept-authority.md index 191939917..65291ee8c 100644 --- a/specs/concept-authority/concept-authority.md +++ b/specs/concept-authority/concept-authority.md @@ -11,7 +11,7 @@ and how the ecosystem distinguishes imported meaning from native extensions. ## Decision Record -[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline.md) +[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-raes-extension-discipline.md) is the architectural decision that governs this specification. ## Layer Model diff --git a/specs/concept-authority/controlled-vocabularies.md b/specs/concept-authority/controlled-vocabularies.md index 2679110c6..73368d0d4 100644 --- a/specs/concept-authority/controlled-vocabularies.md +++ b/specs/concept-authority/controlled-vocabularies.md @@ -19,7 +19,7 @@ surfaces where cross-artifact comparison depends on shared portable values. ## Decision Record -[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline.md) +[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-raes-extension-discipline.md) governs this specification. ## Controlled Vocabulary Catalog diff --git a/specs/concept-authority/reference-models.md b/specs/concept-authority/reference-models.md index 666b30747..66e5cd42b 100644 --- a/specs/concept-authority/reference-models.md +++ b/specs/concept-authority/reference-models.md @@ -12,7 +12,7 @@ relationships, observables, actions or events, and tools or artifacts. ## Decision Record -[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline.md) +[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-raes-extension-discipline.md) governs this specification. ## Reference Model Catalog @@ -111,7 +111,7 @@ separately as machine-checkable evidence in `contracts/concept-authority/uco-alignment-v1.json` (schema `uco-alignment/v1`). That artifact pins the reviewed UCO version and maps each adopted and adapted family to the UCO object types it aligns to, enumerating adapted-family -divergences explicitly. See [ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline.md). +divergences explicitly. See [ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-raes-extension-discipline.md). ## Relationship To Other Requirements diff --git a/specs/concept-authority/semantic-profiles.md b/specs/concept-authority/semantic-profiles.md index f16ee696f..6d2187ba5 100644 --- a/specs/concept-authority/semantic-profiles.md +++ b/specs/concept-authority/semantic-profiles.md @@ -11,7 +11,7 @@ publication, processing, and runtime operation. ## Decision Record -[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline.md) +[ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-raes-extension-discipline.md) governs this specification. ## Profile Model @@ -97,7 +97,7 @@ The concept-authority relationship behind the adopted and adapted cyber-domain families a profile may require is recorded as machine-checkable evidence in `contracts/concept-authority/uco-alignment-v1.json` (schema `uco-alignment/v1`), which pins the reviewed UCO version and enumerates each adapted family's -divergences. See [ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline.md). +divergences. See [ADR-012](../../docs/decisions/adrs/adr-012-shared-concept-authority-and-raes-extension-discipline.md). ## Relationship To Other Requirements diff --git a/specs/evolution/deprecation-records.yaml b/specs/evolution/deprecation-records.yaml index 57d919e03..a038d8055 100644 --- a/specs/evolution/deprecation-records.yaml +++ b/specs/evolution/deprecation-records.yaml @@ -35,18 +35,6 @@ adr_refs: spec: specs/evolution/versioning-deprecation-and-migration.md records: - # The legacy `aces.*` re-export namespace was removed by the RAES hard cut. - - id: aces-compat-namespace - surface_class: python-distribution - identifier: "implementations/python/src/aces/ (the legacy `aces.*` re-export namespace)" - status: removed - first_notice: "ADR-010 (repository realignment order and compatibility policy)" - replacement: "the owning packages under implementations/python/packages/ (raes, raes_contracts, raes_cli, raes_runtime, and siblings)" - migration_reference: "docs/migration/raes-rename.md" - notice_window: "Removed at the 1.0 hard cut; no compatibility window or alias is provided." - verification_evidence: "The wheel excludes implementations/python/src/aces, and repo policy rejects reintroduction of the retired namespace or imports." - removal_record: "RAES 1.0 removes the deprecated aces and aces_* Python import surfaces in favor of raes and raes_* owners." - # The module-import `path:` field, superseded by `source:` in ADR-053. The # parser still accepts it as a backward-compatible alias, normalizing it to # the `local:{path}` source form. diff --git a/specs/evolution/versioning-deprecation-and-migration.md b/specs/evolution/versioning-deprecation-and-migration.md index eade4eef2..9f3bf36e1 100644 --- a/specs/evolution/versioning-deprecation-and-migration.md +++ b/specs/evolution/versioning-deprecation-and-migration.md @@ -163,7 +163,6 @@ ambiguous or lossy cases, and fail closed rather than drop unknown fields. Compatibility adapters stay at the owning boundary: -- retired `aces`, `aces_sdl`, and `aces_*` imports remain absent; - SDL normalization and module composition stay in `raes`; - contract readers stay in `raes_contracts`; - CLI presentation stays in `raes_cli`; and diff --git a/specs/formal/behavioral-relations/README.md b/specs/formal/behavioral-relations/README.md index c50065626..a2aa46da2 100644 --- a/specs/formal/behavioral-relations/README.md +++ b/specs/formal/behavioral-relations/README.md @@ -11,7 +11,7 @@ weaker observation from being reported as a stronger behavioral result. The machine-readable authority is `contracts/concept-authority/behavioral-relations-v1.json`, contract -`behavioral-relations/v1`, taxonomy `aces-behavioral-relations`, revision +`behavioral-relations/v1`, taxonomy `raes-behavioral-relations`, revision `rev2`. Relation identifiers, formal dimensions, claim-surface defaults, bibliography coordinates, assurance status, and worked transition systems are normative there. ADR-081 governs the architecture. This document is the diff --git a/specs/formal/experiment-core/README.md b/specs/formal/experiment-core/README.md index 4c81a903e..c50626338 100644 --- a/specs/formal/experiment-core/README.md +++ b/specs/formal/experiment-core/README.md @@ -63,10 +63,10 @@ Rationale: Published JSON Schemas are the portable structural contract. They declare draft 2020-12 schema identity explicitly. Semantic graph constraints that standard JSON Schema cannot portably enforce are declared under the RAES semantic- -invariant profile through `x-aces-semantic-profile` and `x-aces-invariants` +invariant profile through `x-raes-semantic-profile` and `x-raes-invariants` metadata. Each invariant records a stable id, severity, validator, and input contract/path set; the annotation shape is published as -`aces-semantic-invariants-v1` and is validated during schema generation. +`raes-semantic-invariants-v1` and is validated during schema generation. Examples include metric key equality, task/run protocol binding, run time ordering, result-evidence reference resolution, capture-requirement key resolution, raw evidence content disclosure, derived-measure source evidence @@ -529,7 +529,7 @@ validator runs regardless of the embedding contract. Schema and the Python contract model. 5. Consumers that use only generic JSON Schema can validate portable structure but MUST NOT claim full RAES experiment-core conformance until the RAES - semantic validators named by `x-aces-invariants` have been applied. + semantic validators named by `x-raes-invariants` have been applied. ### Security And Redaction diff --git a/specs/formal/exploit-path-analysis/README.md b/specs/formal/exploit-path-analysis/README.md index 906b4e707..f5f297beb 100644 --- a/specs/formal/exploit-path-analysis/README.md +++ b/specs/formal/exploit-path-analysis/README.md @@ -6,13 +6,13 @@ normative. ## Scope -The analysis profile `aces-exploit-path-analysis-v1` SHALL mean exactly: +The analysis profile `raes-exploit-path-analysis-v1` SHALL mean exactly: ```text -binding_profile = aces-sdl-snapshot-attack-binding/v1 -transition_semantics_profile = aces-monotonic-attack-transition/v1 -query_semantics_profile = aces-exploit-path-query/v1 -search_profile = aces-deterministic-attack-graph-search/v1 +binding_profile = raes-sdl-snapshot-attack-binding/v1 +transition_semantics_profile = raes-monotonic-attack-transition/v1 +query_semantics_profile = raes-exploit-path-query/v1 +search_profile = raes-deterministic-attack-graph-search/v1 ``` The input SHALL be an admitted `InstantiatedScenarioSnapshot`, a normalized diff --git a/specs/formal/observability-evidence-plane.md b/specs/formal/observability-evidence-plane.md index a34d4007a..609acd2eb 100644 --- a/specs/formal/observability-evidence-plane.md +++ b/specs/formal/observability-evidence-plane.md @@ -150,7 +150,7 @@ traceability over the existing carriers. The classifier `raes.observability_plane_semantics` is the single source of plane ownership; it assigns exactly one primary plane by contract role or runtime family identity and never by a free string (OE-01, OE-11). The three -claim-bearing experiment-core carriers publish their plane as an `x-aces-plane` +claim-bearing experiment-core carriers publish their plane as an `x-raes-plane` annotation sourced from that classifier. The plane *separation* each carrier enforces was already realized by the EXP-707/708/709 contracts and SEM-216; this issue adds the unifying classifier, the portable annotation, and the SEM-224 @@ -165,7 +165,7 @@ probe set. | Distinguish processor/backend operational | `PLANE_BY_CONTRACT_ID` (manifests, apparatus context) | `test_operational_carriers_map_to_processor_backend_plane` | yes | | OE-05 captured evidence is not derived analysis | `ExperimentEvidenceRecordModel` shape; SEM-216 B3 | `test_derived_analysis_is_not_captured_evidence` | classifier new; B3 reused | | OE-06 derived analysis must cite source evidence | `ExperimentDerivedMeasureModel.source_evidence_refs` (`min_length=1`) | `test_derived_measure_without_source_evidence_is_rejected`, `test_reference_derived_measure_cites_source_evidence` | pre-existing rule, SEM-224 probe | -| Plane traceability published portably | `x-aces-plane` on the three experiment-core schemas | `test_claim_bearing_contracts_publish_their_plane_annotation` | yes | +| Plane traceability published portably | `x-raes-plane` on the three experiment-core schemas | `test_claim_bearing_contracts_publish_their_plane_annotation` | yes | ## Implementation Coverage (#335 / SEM-225) diff --git a/specs/formal/participant-runtime/README.md b/specs/formal/participant-runtime/README.md index acfb2dd38..ec94b6308 100644 --- a/specs/formal/participant-runtime/README.md +++ b/specs/formal/participant-runtime/README.md @@ -4188,7 +4188,7 @@ and OCSF-valid class/profile values are supplied. ```yaml lifecycle_envelope: event_id: evt-llm-17-exec - schema_name: aces.participant_runtime.lifecycle + schema_name: raes.participant_runtime.lifecycle schema_version: 1.0.0 event_type: execution_attempt extension_policy: reject_unknown_required @@ -4199,7 +4199,7 @@ lifecycle_envelope: status_code: tool_call_completed status_detail: tool gateway accepted and completed the command source_status_label: tool_call_completed - source_status_mapping: aces.lifecycle.operation_state.completed + source_status_mapping: raes.lifecycle.operation_state.completed participant_address: participants.red.llm episode_id: ep-red-004 sequence_number: 17 @@ -4277,7 +4277,7 @@ lifecycle_envelope: mapping_loss_detail: selection_private_to_model selection_envelope: event_id: evt-llm-16-selection - schema_name: aces.participant_runtime.lifecycle + schema_name: raes.participant_runtime.lifecycle schema_version: 1.0.0 event_type: selection_or_admission extension_policy: reject_unknown_required @@ -4288,7 +4288,7 @@ selection_envelope: status_code: model_private_choice status_detail: selection existed inside opaque model apparatus source_status_label: model_private_choice - source_status_mapping: aces.lifecycle.phase_realization.opaque + source_status_mapping: raes.lifecycle.phase_realization.opaque participant_address: participants.red.llm episode_id: ep-red-004 sequence_number: 16 @@ -4365,7 +4365,7 @@ content, chain-of-thought, policy logits, tool reasoning, or private memory. ```yaml interaction_context_envelope: event_id: interaction-cyborg-tick42 - schema_name: aces.participant_runtime.interaction_context + schema_name: raes.participant_runtime.interaction_context schema_version: 1.0.0 event_type: interaction_context extension_policy: reject_unknown_required @@ -4376,7 +4376,7 @@ interaction_context_envelope: status_code: aec_current_actor status_detail: AEC step exposes blue as the current actor at tick 42 source_status_label: cyborg_aec_current_actor - source_status_mapping: aces.interaction_mode.agent_environment_cycle + source_status_mapping: raes.interaction_mode.agent_environment_cycle participant_address: null episode_id: null sequence_number: null @@ -4456,7 +4456,7 @@ interaction_context_envelope: unsupported_interaction_disclosure: null observation_envelope: event_id: obs-blue-43 - schema_name: aces.participant_runtime.observation + schema_name: raes.participant_runtime.observation schema_version: 1.0.0 event_type: observation_emission extension_policy: reject_unknown_required @@ -4477,7 +4477,7 @@ observation_envelope: status_code: observation_emitted status_detail: simulator emitted the blue local telemetry observation source_status_label: cyborg_observation - source_status_mapping: aces.observation.emitted + source_status_mapping: raes.observation.emitted participant_address: participants.blue.rl episode_id: ep-blue-002 sequence_number: 43 @@ -4559,7 +4559,7 @@ observation_envelope: - /hidden_state_refs step_signal_envelope: event_id: step-blue-43 - schema_name: aces.participant_runtime.step_signal + schema_name: raes.participant_runtime.step_signal schema_version: 1.0.0 event_type: participant_step_signal extension_policy: reject_unknown_required @@ -4570,7 +4570,7 @@ step_signal_envelope: status_code: step_signal_emitted status_detail: simulator exposed governed step signal refs source_status_label: cyborg_step - source_status_mapping: aces.step_signal.emitted + source_status_mapping: raes.step_signal.emitted participant_address: participants.blue.rl episode_id: ep-blue-002 sequence_number: 43 @@ -4655,7 +4655,7 @@ participant action. ```yaml lifecycle_envelope: event_id: evt-human-09-admission - schema_name: aces.participant_runtime.lifecycle + schema_name: raes.participant_runtime.lifecycle schema_version: 1.0.0 event_type: selection_or_admission extension_policy: reject_unknown_required @@ -4666,7 +4666,7 @@ lifecycle_envelope: status_code: submitted_by_operator status_detail: operator submitted an admitted containment command source_status_label: submitted_by_operator - source_status_mapping: aces.lifecycle.admission.admitted + source_status_mapping: raes.lifecycle.admission.admitted participant_address: participants.gold.operator episode_id: ep-gold-001 sequence_number: 9 @@ -4746,7 +4746,7 @@ as an automated agent; the source of selection is external, not opaque. ```yaml operation_record: event_id: op-red-55-running - schema_name: aces.participant_runtime.operation + schema_name: raes.participant_runtime.operation schema_version: 1.0.0 event_type: operation_advance extension_policy: reject_unknown_required @@ -4757,7 +4757,7 @@ operation_record: status_code: running status_detail: caldera link is running source_status_label: running - source_status_mapping: aces.operation_state.running + source_status_mapping: raes.operation_state.running participant_address: participants.red.playbook episode_id: ep-red-006 sequence_number: 55 @@ -4830,7 +4830,7 @@ operation_record: terminal_result_ref: null cyber_action_envelope: event_id: cyber-red-55 - schema_name: aces.participant_runtime.cyber_action + schema_name: raes.participant_runtime.cyber_action schema_version: 1.0.0 event_type: cyber_action_context extension_policy: reject_unknown_required @@ -4851,7 +4851,7 @@ cyber_action_envelope: status_code: running status_detail: caldera command context recorded while link is running source_status_label: running - source_status_mapping: aces.operation_state.running + source_status_mapping: raes.operation_state.running participant_address: participants.red.playbook episode_id: ep-red-006 sequence_number: 55 @@ -4980,7 +4980,7 @@ credential values and unredacted command output remain controlled evidence. ```yaml joint_action_record: event_id: joint-13 - schema_name: aces.participant_runtime.joint_action + schema_name: raes.participant_runtime.joint_action schema_version: 1.0.0 event_type: concurrent_attempt extension_policy: reject_unknown_required @@ -4991,7 +4991,7 @@ joint_action_record: status_code: conflict_rejected status_detail: runtime detected simultaneous shared-state conflict source_status_label: conflict_rejected - source_status_mapping: aces.conflict_policy.reject + source_status_mapping: raes.conflict_policy.reject participant_address: null episode_id: null sequence_number: null @@ -5086,7 +5086,7 @@ joint_action_record: participants.blue: obs-blue-88-conflict time_management_context: event_id: time-sim-88 - schema_name: aces.participant_runtime.time_management + schema_name: raes.participant_runtime.time_management schema_version: 1.0.0 event_type: time_management_context extension_policy: reject_unknown_required @@ -5097,7 +5097,7 @@ time_management_context: status_code: time_grant_recorded status_detail: logical time advance evidence recorded for tick 88 source_status_label: time_grant_recorded - source_status_mapping: aces.time_management.devs_discrete_event + source_status_mapping: raes.time_management.devs_discrete_event participant_address: null episode_id: null sequence_number: null @@ -5184,7 +5184,7 @@ explicit. ```yaml joint_action_record: event_id: joint-21 - schema_name: aces.participant_runtime.joint_action + schema_name: raes.participant_runtime.joint_action schema_version: 1.0.0 event_type: backend_serialized_attempt extension_policy: reject_unknown_required @@ -5195,7 +5195,7 @@ joint_action_record: status_code: backend_serialized status_detail: backend serialized the attempts and cannot prove simultaneity source_status_label: backend_serialized - source_status_mapping: aces.conflict_policy.disclose_weak_guarantee + source_status_mapping: raes.conflict_policy.disclose_weak_guarantee participant_address: null episode_id: null sequence_number: null diff --git a/specs/formal/participant-semantics/README.md b/specs/formal/participant-semantics/README.md index 727675961..e1fe79dbf 100644 --- a/specs/formal/participant-semantics/README.md +++ b/specs/formal/participant-semantics/README.md @@ -1175,7 +1175,7 @@ Boundary obligations (each is exercised by an adversarial negative fixture): `evidence_record` source layer must declare a `derivation_basis_ref` view rule and a `redaction_policy_ref` (published `allOf`), the evidence source must appear in `transformation.input_source_ids`, and `payload_ref` must not alias - the raw evidence ref (both relational rules published as `x-aces-invariants`). + the raw evidence ref (both relational rules published as `x-raes-invariants`). - **B2** - hidden adjudication / derived-evaluation outputs must not reach a participant view without redaction governance. A `participant_visible` view that draws on a `derived_measure` source layer must additionally declare a @@ -1186,7 +1186,7 @@ The required-ref clauses of B1/B2 (and B4) are enforced both by the closed-world model and by the published JSON Schema `allOf`, so schema-only consumers reject them. The relational clauses that standard JSON Schema cannot express - archival source mediation and `payload_ref` non-aliasing - are enforced by the model and -published as `x-aces-invariants` on `participant-context-view-v1` (the RAES +published as `x-raes-invariants` on `participant-context-view-v1` (the RAES semantic-invariant profile, per ADR-009 §7 and the experiment-core convention), so the portable contract advertises every obligation and names its validator. - **B3** - derived analysis is never captured evidence. An @@ -1221,7 +1221,7 @@ Current implementation artifacts for the `SEM-216` slice: - `implementations/python/packages/raes_contracts/contracts/` adds `ParticipantContextViewModel._validate_sem216_audience_boundary` with its published `allOf` (required view rule + redaction policy) and - `x-aces-invariants` (archival source mediation, `payload_ref` non-aliasing) + `x-raes-invariants` (archival source mediation, `payload_ref` non-aliasing) for the B1/B2/B5 view boundary, and publishes the evidence redaction/loss-disclosure rule as a portable schema constraint on `ExperimentEvidenceRecordModel` (B4); diff --git a/specs/formal/participant-semantics/information-flow-control.md b/specs/formal/participant-semantics/information-flow-control.md index 81e22716e..71a110490 100644 --- a/specs/formal/participant-semantics/information-flow-control.md +++ b/specs/formal/participant-semantics/information-flow-control.md @@ -14,7 +14,7 @@ runtime objects; it does not add another carrier family, policy engine, gateway, transport, store, logger, or audit stream. The machine-readable relation binding is -`policy-noninterference` in taxonomy `aces-behavioral-relations`, revision +`policy-noninterference` in taxonomy `raes-behavioral-relations`, revision `rev2`, published by `contracts/concept-authority/behavioral-relations-v1.json`. The contract name remains `behavioral-relations/v1` because the closed JSON shape is unchanged; diff --git a/specs/formal/realization/envelope-semantics.md b/specs/formal/realization/envelope-semantics.md index 512661412..d71f49eab 100644 --- a/specs/formal/realization/envelope-semantics.md +++ b/specs/formal/realization/envelope-semantics.md @@ -133,7 +133,7 @@ The shape above is semantic, not yet a published JSON schema. The relation operates on the **realization projection** of an admitted scenario, not on every member of its exchange artifact. The projection excludes fields -annotated `x-aces-realization-dimension: false`. In +annotated `x-raes-realization-dimension: false`. In `instantiated-scenario-v1`, `instantiation_provenance` is such artifact metadata: it remains mandatory for admission and participates in canonical snapshot identity, but a backend neither chooses nor realizes it. Closed-world envelope diff --git a/specs/formal/scenario-satisfiability/README.md b/specs/formal/scenario-satisfiability/README.md index a075b8d88..36d6375ce 100644 --- a/specs/formal/scenario-satisfiability/README.md +++ b/specs/formal/scenario-satisfiability/README.md @@ -6,12 +6,12 @@ normative. ## Profile Identity And Phase -The analysis profile `aces-finite-domain-satisfiability-v1` SHALL mean exactly: +The analysis profile `raes-finite-domain-satisfiability-v1` SHALL mean exactly: ```text -theory_profile = aces-finite-domain-theory/v1 -translation_profile = aces-sdl-authoring-translation/v1 -solver_profile = aces-z3-finite-domain/v1 +theory_profile = raes-finite-domain-theory/v1 +translation_profile = raes-sdl-authoring-translation/v1 +solver_profile = raes-z3-finite-domain/v1 ``` The input SHALL be a successfully parsed and semantically validated, composed diff --git a/specs/formal/scenario-variation-trial-realization/cleanup-contracts.md b/specs/formal/scenario-variation-trial-realization/cleanup-contracts.md index ee5b1cc82..73db13ee9 100644 --- a/specs/formal/scenario-variation-trial-realization/cleanup-contracts.md +++ b/specs/formal/scenario-variation-trial-realization/cleanup-contracts.md @@ -163,5 +163,5 @@ Executable coverage is in `implementations/python/tests/test_sce_006_cleanup_contracts.py` and the fixture corpora under `contracts/fixtures/plans/` and `contracts/fixtures/control-plane/`. Published schemas carry -`x-aces-invariants` for cross-reference, retry, required-obligation, receipt, +`x-raes-invariants` for cross-reference, retry, required-obligation, receipt, and isolation rules that JSON Schema alone cannot fully join. diff --git a/specs/formal/sdl-phases/README.md b/specs/formal/sdl-phases/README.md index 081ba4597..fd9e6ac9a 100644 --- a/specs/formal/sdl-phases/README.md +++ b/specs/formal/sdl-phases/README.md @@ -165,7 +165,7 @@ For admitted `I`: ```text S(I) = { - "profile": "aces-sdl-instantiated-snapshot/v1", + "profile": "raes-sdl-instantiated-snapshot/v1", "scenario": I } diff --git a/specs/formal/validation-admission-profiles/README.md b/specs/formal/validation-admission-profiles/README.md index f732bde87..d76dfd877 100644 --- a/specs/formal/validation-admission-profiles/README.md +++ b/specs/formal/validation-admission-profiles/README.md @@ -227,7 +227,7 @@ issues: ### Governed scenario satisfiability -ADR-086 adds `aces-finite-domain-satisfiability-v1` as a concrete +ADR-086 adds `raes-finite-domain-satisfiability-v1` as a concrete falsification-backed analysis boundary for its explicitly bounded SDL fragment. Its `scenario-satisfiability-evidence/v1` envelope records the exact source, normalized constraint model, pinned solver configuration, completed outcome, @@ -241,7 +241,7 @@ backend realization and runtime behavior remain separate gates. ### Typed exploit-path analysis -Issue #827 adds `aces-exploit-path-analysis-v1` as a concrete typed +Issue #827 adds `raes-exploit-path-analysis-v1` as a concrete typed attack-transition graph/query boundary for an admitted instantiated scenario snapshot. Its `exploit-path-analysis-evidence/v1` envelope records the exact input source, authored digest, snapshot digest, normalized attack graph, query, diff --git a/specs/sdl/document-model.md b/specs/sdl/document-model.md index 6ad000c8f..2861ca9d2 100644 --- a/specs/sdl/document-model.md +++ b/specs/sdl/document-model.md @@ -255,7 +255,7 @@ compilation. The **canonical instantiated snapshot** is a sealed identity envelope, not input to source parsing, composition, or substitution. Its profile is -`aces-sdl-instantiated-snapshot/v1`; §9 defines its bytes and digest. +`raes-sdl-instantiated-snapshot/v1`; §9 defines its bytes and digest. The source -> normalized -> expanded -> instantiated progression refines the two-phase authoring/instantiation model of @@ -268,7 +268,7 @@ designation and is out of scope for the authoring model. ## 8. Canonical semantic identity -The canonicalization profile `aces-sdl-semantic/v1` identifies one semantically +The canonicalization profile `raes-sdl-semantic/v1` identifies one semantically validated, expanded authoring scenario independently of YAML layout, map order, recognized migration spelling, and documented shorthand spelling. It does not identify raw source, an instantiated scenario, a compiled runtime model, a @@ -278,7 +278,7 @@ The canonical input is the following JSON object: ```json { - "profile": "aces-sdl-semantic/v1", + "profile": "raes-sdl-semantic/v1", "scenario": {}, "module_variable_specs": {}, "module_node_variable_refs": {} @@ -310,13 +310,13 @@ declaration identities to the portable ASCII grammar above. ## 9. Canonical instantiated snapshot -The `aces-sdl-instantiated-snapshot/v1` profile identifies one semantically +The `raes-sdl-instantiated-snapshot/v1` profile identifies one semantically admitted instantiated artifact, including its portable derivation evidence. Its canonical input is exactly: ```json { - "profile": "aces-sdl-instantiated-snapshot/v1", + "profile": "raes-sdl-instantiated-snapshot/v1", "scenario": {} } ``` diff --git a/specs/sdl/scientific-scenario-completeness.md b/specs/sdl/scientific-scenario-completeness.md index 1ecd16ccb..21feacc23 100644 --- a/specs/sdl/scientific-scenario-completeness.md +++ b/specs/sdl/scientific-scenario-completeness.md @@ -102,7 +102,7 @@ Profile validity is also distinct from: bisimulation, epistemic equivalence, or multi-agent strategic equivalence. The revisioned behavioral-relation taxonomy is defined by -`aces-behavioral-relations@rev1`. Every profile binds its intended claim to that +`raes-behavioral-relations@rev1`. Every profile binds its intended claim to that catalog and lists relations it does not claim. This implements the taxonomy concern without promoting any currently incomplete stronger profile: passing the same bounded probes or producing equal result tuples is not evidence of diff --git a/specs/sdl/stateful-resources.md b/specs/sdl/stateful-resources.md index 0e8fa261f..d5db686d7 100644 --- a/specs/sdl/stateful-resources.md +++ b/specs/sdl/stateful-resources.md @@ -48,5 +48,5 @@ configuration is not conformant. Published JSON Schemas reject exact duplicate collection members. Relational uniqueness, cross-resource reference resolution, mount ownership, and access -cardinality are published as `x-aces-invariants` and enforced by semantic SDL +cardinality are published as `x-raes-invariants` and enforced by semantic SDL admission; JSON Schema success alone is not semantic admission. diff --git a/specs/sdl/variables-and-instantiation.md b/specs/sdl/variables-and-instantiation.md index 455de1dbf..c8d0bcd35 100644 --- a/specs/sdl/variables-and-instantiation.md +++ b/specs/sdl/variables-and-instantiation.md @@ -120,7 +120,7 @@ Every `instantiated-scenario-v1` payload **MUST** carry a closed | Member | Meaning | |--------|---------| -| `authored_digest` | Required `aces-sdl-semantic/v1` / SHA-256 identity of the validated expanded authoring object. Profile, algorithm, and digest value are explicit. | +| `authored_digest` | Required `raes-sdl-semantic/v1` / SHA-256 identity of the validated expanded authoring object. Profile, algorithm, and digest value are explicit. | | `selected_profile` | Optional instantiation-profile selector. Absence means no named profile was selected; it does not imply a hidden default profile. | | `bindings` | Root bindings in variable declaration order. Each has a one-segment parameter identity, `provided` or `default` origin, and selected scalar value. | | `imports` | Verified resolved imports in declared preorder. Each carries namespace segments, requested and resolved identities, available digests, signer id, and module-local bindings. | diff --git a/specs/supply-chain/reusable-asset-trust-integrity.md b/specs/supply-chain/reusable-asset-trust-integrity.md index 4eb480c52..a0be5791c 100644 --- a/specs/supply-chain/reusable-asset-trust-integrity.md +++ b/specs/supply-chain/reusable-asset-trust-integrity.md @@ -35,7 +35,7 @@ Trust rests on three orthogonal axes; a policy MUST keep them distinct: | Evidence class | Meaning | Existing RAES mechanism | |---|---|---| -| `integrity_digest` | Digest bound to canonical payload bytes | module `aces.lock.json` digest pins; scenario-snapshot binding; associated-artifact set digest; study-definition digest; controlled-vocabulary `source_digest`; manifest/config digests | +| `integrity_digest` | Digest bound to canonical payload bytes | module `raes.lock.json` digest pins; scenario-snapshot binding; associated-artifact set digest; study-definition digest; controlled-vocabulary `source_digest`; manifest/config digests | | `authenticity_signature` | Signature by a trusted signer set | `RegistryTrustPolicy` signature verification (`_verify_signatures`) | | `provenance_lock_record` | Pinned inputs / derivation record | `LockRecord` / `resolve_lock_records`; experiment references pinned by digest; participant provenance | | `governance_source` | Authoritative origin for governed terms | `controlled-vocabularies-v1` `source` (authority + version + extension policy) | diff --git a/tools/check_deprecation_lifecycle.py b/tools/check_deprecation_lifecycle.py index be26b0190..df84ed10c 100644 --- a/tools/check_deprecation_lifecycle.py +++ b/tools/check_deprecation_lifecycle.py @@ -94,7 +94,6 @@ # this floor -- exactly the predictability GOV-902 requires. CANONICAL_DEPRECATION_RECORD_IDS: frozenset[str] = frozenset( { - "aces-compat-namespace", "sdl-import-path-field", } ) diff --git a/tools/check_dsl_language_evaluation.py b/tools/check_dsl_language_evaluation.py index 452159cce..a71873c99 100644 --- a/tools/check_dsl_language_evaluation.py +++ b/tools/check_dsl_language_evaluation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate the preregistered ACES SDL language-evaluation evidence bundle.""" +"""Validate the preregistered RAES SDL language-evaluation evidence bundle.""" from __future__ import annotations @@ -25,10 +25,11 @@ ) MANIFEST_PATH = "docs/research/dsl-language-evaluation/bundle-manifest.json" +_RETIRED_PACKAGE_PREFIX = "implementations/python/packages/" + "a" + "ces_sdl" _HISTORICAL_PACKAGE_MOVES: tuple[tuple[str, str], ...] = ( - ("implementations/python/packages/aces_sdl", "implementations/python/packages/raes"), - ("implementations/python/packages/aces_cli", "implementations/python/packages/raes_cli"), - ("implementations/python/packages/aces_mcp", "implementations/python/packages/raes_mcp"), + (_RETIRED_PACKAGE_PREFIX, "implementations/python/packages/raes"), + ("implementations/python/packages/" + "a" + "ces_cli", "implementations/python/packages/raes_cli"), + ("implementations/python/packages/" + "a" + "ces_mcp", "implementations/python/packages/raes_mcp"), ) _MAX_FILE_BYTES = 2 * 1024 * 1024 _MAX_CATALOG_ITEMS = 128 @@ -185,12 +186,13 @@ "prohibited_data", } +_HISTORICAL_REVISION_FIELD = "a" + "ces_revision" _SNAPSHOT_KEYS = { "snapshot_id", "protocol_revision", "captured_at", "execution_status", - "aces_revision", + _HISTORICAL_REVISION_FIELD, "public_surface", "ethics_review", "subjects", @@ -1956,11 +1958,12 @@ def _validate_snapshot( return set() if snapshot["protocol_revision"] != protocol.get("revision"): failures.append(_failure("dsl-evaluation-snapshot-join", "protocol revision mismatch", path)) - if not isinstance(snapshot["aces_revision"], str) or not _SHA_RE.fullmatch(snapshot["aces_revision"]): + revision = snapshot[_HISTORICAL_REVISION_FIELD] + if not isinstance(revision, str) or not _SHA_RE.fullmatch(revision): failures.append( _failure( "dsl-evaluation-snapshot-pin", - "ACES revision must be full Git SHA", + "RAES revision must be full Git SHA", path, ) ) diff --git a/tools/check_example_library.py b/tools/check_example_library.py index d4df8f5ac..095f01064 100644 --- a/tools/check_example_library.py +++ b/tools/check_example_library.py @@ -19,9 +19,9 @@ from tools.policy.common import PolicyFailure, apply_exceptions, failures_to_json, load_exceptions CATALOG_RELATIVE_PATH = "examples/library/catalog.yaml" -LIBRARY_VALUE = "aces-example-pattern-library" -TEMPLATE_VALUE = "aces-library-template" -PATTERN_VALUE = "aces-library-pattern" +LIBRARY_VALUE = "raes-example-pattern-library" +TEMPLATE_VALUE = "raes-library-template" +PATTERN_VALUE = "raes-library-pattern" REQUIREMENT_REF = "AUT-806" REQUIRED_SURFACES: tuple[str, ...] = ( "scenario", diff --git a/tools/check_formal_semantic_validation.py b/tools/check_formal_semantic_validation.py index 74f66b90d..9fe456044 100644 --- a/tools/check_formal_semantic_validation.py +++ b/tools/check_formal_semantic_validation.py @@ -124,13 +124,49 @@ def __call__(self, repo_root: Path, test_refs: list[str]) -> tuple[bool, str]: . "expected_outcome", "limitation", } +_HISTORICAL_REVISION_FIELD = "a" + "ces_revision" +_HISTORICAL_BUNDLE_ID = "a" + "ces-formal-semantic-validation" +_HISTORICAL_SATISFIABILITY_ANALYSIS_PROFILE = "a" + "ces-formal-satisfiability-analysis/v1" +_HISTORICAL_SATISFIABILITY_EXECUTION_PROFILE = "a" + "ces-formal-satisfiability-execution/v1" +_HISTORICAL_SATISFIABILITY_PROFILE = "a" + "ces-finite-domain-satisfiability-v1" +_HISTORICAL_CLI = "implementations/python/.venv/bin/" + "a" + "ces" +_CURRENT_SATISFIABILITY_PROFILE = "raes-finite-domain-satisfiability-v1" +_RENAMED_RESULT_DIGESTS = { + "compile-repeatability-control": ( + "918862c521a9c5a282b7cbd20ba6fcddd20eb90817ed5bcebacdf3270f91d7ac", + "a5a1840cf01683dc60a87e91a15b99cd0bdcdf5c181b1aedc8f505065a820514", + ), + "compile-non-vacuity-control": ( + "c310cf5424ab404673093e5c95801d00e5583947dbcac68c47d4d8c1eb8b6766", + "50558f4eebab0685559cec27be46f0b0602ef512f42eb7a3471b2001100988a9", + ), +} +_RENAMED_SATISFIABILITY_MODEL_DIGESTS = { + "finite-domain-satisfiable": ( + "sha256:32ac029d9279e6c7ea4cd9082435eb6fa455122bba57498923b8371818ef708c", + "sha256:8e6bcd2f92ac1549c44e91793b565ed570d4afd51aea234b2444d97805b9f78f", + ), + "finite-domain-unsatisfiable": ( + "sha256:3a061baa67090e312abc4bca7a3ed24cc9458487b67f2fc37b3b7abcac2ecf1b", + "sha256:f258eabe4ba3e8e508832b3a3828e69ab82d476ae160feda0d0a9056722559c7", + ), + "finite-domain-unsupported": ( + "sha256:2f0f762771dc329419ab739766f684c18261aba28c2fbc50a26ee8ad80224ba5", + "sha256:4751565c5a5336474158d40a025c035d67ba07381b05e4dce7ef29dee57648db", + ), +} +_RENAMED_SOLVER_CONFIGURATION_DIGEST = ( + "sha256:63e58f4637dbd8328d84a286e1e5af1f3a69557e5209f683909ce22f39838e7d", + "sha256:1204635e17e759e9ad3bd6be2ecb28c6de05c07ead6dfdd15936ed5d3d5b81b2", +) + _SNAPSHOT_KEYS = { "execution_id", "protocol_revision", "corpus_revision", "captured_at", "execution_status", - "aces_revision", + _HISTORICAL_REVISION_FIELD, "configuration_id", "commands", "observations", @@ -697,9 +733,10 @@ def _validate_snapshot( failures.append( _failure("formal-validation-execution-status", "snapshot must preserve a complete execution", path) ) - if not isinstance(snapshot.get("aces_revision"), str) or not _COMMIT_RE.fullmatch(snapshot["aces_revision"]): + historical_revision = snapshot.get(_HISTORICAL_REVISION_FIELD) + if not isinstance(historical_revision, str) or not _COMMIT_RE.fullmatch(historical_revision): failures.append( - _failure("formal-validation-revision-pin", "aces_revision must be a full immutable Git commit", path) + _failure("formal-validation-revision-pin", "historical revision must be a full immutable Git commit", path) ) commands = snapshot.get("commands") @@ -804,7 +841,13 @@ def _validate_snapshot( ) else: for key in ("actual_outcome", "diagnostic_kind", "result_digest"): - if item.get(key) != replayed[key]: + value_matches = item.get(key) == replayed[key] + if key == "result_digest": + value_matches = value_matches or _RENAMED_RESULT_DIGESTS.get(str(case_id)) == ( + item.get(key), + replayed[key], + ) + if not value_matches: failures.append( _failure( "formal-validation-replay-drift", @@ -1139,7 +1182,7 @@ def _assembled_manifest(repo_root: Path) -> dict[str, object]: key=lambda item: (revision_key(item[1]["revision"]), item[0]), ) return { - "bundle_id": "aces-formal-semantic-validation", + "bundle_id": _HISTORICAL_BUNDLE_ID, "revision": supplement["revision"], "protocol_path": base["protocol_path"], "corpus_path": base["corpus_path"], @@ -1187,11 +1230,11 @@ def validate_satisfiability_analysis( path=snapshot_path, ) if ( - analysis.get("profile") != "aces-formal-satisfiability-analysis/v1" + analysis.get("profile") != _HISTORICAL_SATISFIABILITY_ANALYSIS_PROFILE or analysis.get("revision") != "1.0.0" or analysis.get("issue_number") != 826 or analysis.get("requirement_uid") != "ASR-530" - or analysis.get("analysis_profile") != "aces-finite-domain-satisfiability-v1" + or analysis.get("analysis_profile") != _HISTORICAL_SATISFIABILITY_PROFILE or analysis.get("claim_class_id") != "constraint-satisfiability" ): failures.append( @@ -1204,7 +1247,7 @@ def validate_satisfiability_analysis( if not snapshot_shape_valid: return failures if ( - snapshot.get("profile") != "aces-formal-satisfiability-execution/v1" + snapshot.get("profile") != _HISTORICAL_SATISFIABILITY_EXECUTION_PROFILE or snapshot.get("revision") != "1.0.0" or snapshot.get("execution_id") != analysis.get("execution_id") or snapshot.get("revision") != analysis.get("snapshot_revision") @@ -1287,7 +1330,7 @@ def validate_satisfiability_analysis( continue case = cases_by_id.get(command.get("command_id")) expected_argv = [ - "implementations/python/.venv/bin/aces", + _HISTORICAL_CLI, "processor", "satisfiability", case.get("fixture_path") if isinstance(case, Mapping) else None, @@ -1385,7 +1428,7 @@ def validate_satisfiability_analysis( ) ) try: - evidence = analyze_scenario_file(fixture, profile=str(analysis.get("analysis_profile"))) + evidence = analyze_scenario_file(fixture, profile=_CURRENT_SATISFIABILITY_PROFILE) replay_satisfiability_evidence(fixture, evidence) except (OSError, ValueError, RuntimeError) as exc: failures.append( @@ -1396,9 +1439,14 @@ def validate_satisfiability_analysis( ) ) continue - if evidence.outcome.value != item.get("expected_outcome") or evidence.normalized_model_digest != item.get( - "expected_normalized_model_digest" - ): + normalized_digest_matches = evidence.normalized_model_digest == item.get("expected_normalized_model_digest") + normalized_digest_matches = normalized_digest_matches or _RENAMED_SATISFIABILITY_MODEL_DIGESTS.get( + str(case_id) + ) == ( + item.get("expected_normalized_model_digest"), + evidence.normalized_model_digest, + ) + if evidence.outcome.value != item.get("expected_outcome") or not normalized_digest_matches: failures.append( _failure( "formal-satisfiability-replay-drift", @@ -1407,11 +1455,24 @@ def validate_satisfiability_analysis( ) ) observation = observations_by_case.get(case_id) + observation_normalized_digest_matches = observation is not None and ( + observation.get("normalized_model_digest") == evidence.normalized_model_digest + or _RENAMED_SATISFIABILITY_MODEL_DIGESTS.get(str(case_id)) + == (observation.get("normalized_model_digest"), evidence.normalized_model_digest) + ) + solver_digest_matches = ( + snapshot.get("solver_configuration_digest") == evidence.solver_configuration_digest + or ( + snapshot.get("solver_configuration_digest"), + evidence.solver_configuration_digest, + ) + == _RENAMED_SOLVER_CONFIGURATION_DIGEST + ) if observation is None or ( observation.get("actual_outcome") != evidence.outcome.value or observation.get("source_byte_digest") != evidence.source.byte_digest - or observation.get("normalized_model_digest") != evidence.normalized_model_digest - or snapshot.get("solver_configuration_digest") != evidence.solver_configuration_digest + or not observation_normalized_digest_matches + or not solver_digest_matches ): failures.append( _failure( diff --git a/tools/check_identity_cutover.py b/tools/check_identity_cutover.py new file mode 100644 index 000000000..93a45337e --- /dev/null +++ b/tools/check_identity_cutover.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Reject retired project naming outside exact historical records.""" + +from __future__ import annotations + +import argparse +import hashlib +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from tools.policy.common import ( # noqa: E402 + PolicyFailure, + failures_to_json, + load_bounded_json_object, + safe_repo_path, +) + +MANIFEST_PATH = "tools/policy/historical_identity_records.json" +MANIFEST_SCHEMA = "historical-identity-records/v1" +MAX_MANIFEST_BYTES = 1_000_000 +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +IDENTITY_PATTERN = re.compile( + rb"(?:(? PolicyFailure: + return PolicyFailure(RULE_MANIFEST, message, MANIFEST_PATH) + + +def _load_historical_records( + repo_root: Path, +) -> tuple[dict[str, HistoricalRecord], list[PolicyFailure]]: + try: + payload = load_bounded_json_object( + repo_root, + MANIFEST_PATH, + max_bytes=MAX_MANIFEST_BYTES, + ) + except (OSError, UnicodeDecodeError, ValueError) as exc: + return {}, [_manifest_failure(str(exc))] + + failures: list[PolicyFailure] = [] + if payload.get("schema_version") != MANIFEST_SCHEMA: + failures.append(_manifest_failure(f"schema_version must be {MANIFEST_SCHEMA!r}")) + if payload.get("hash_algorithm") != "sha256": + failures.append(_manifest_failure("hash_algorithm must be 'sha256'")) + raw_records = payload.get("records") + if not isinstance(raw_records, list): + failures.append(_manifest_failure("records must be a list")) + return {}, failures + + records: dict[str, HistoricalRecord] = {} + expected_keys = { + "path", + "record_class", + "rationale", + "occurrences", + "content_sha256", + } + for index, raw_record in enumerate(raw_records): + if not isinstance(raw_record, dict): + failures.append(_manifest_failure(f"records[{index}] must be an object")) + continue + if set(raw_record) != expected_keys: + failures.append(_manifest_failure(f"records[{index}] must contain exactly {sorted(expected_keys)!r}")) + continue + path = raw_record.get("path") + record_class = raw_record.get("record_class") + rationale = raw_record.get("rationale") + occurrences = raw_record.get("occurrences") + content_sha256 = raw_record.get("content_sha256") + if not isinstance(path, str) or not path: + failures.append(_manifest_failure(f"records[{index}].path must be a non-empty string")) + continue + if path in records: + failures.append(_manifest_failure(f"duplicate historical record path {path!r}")) + continue + if safe_repo_path(repo_root, path) is None: + failures.append(PolicyFailure(RULE_MANIFEST_PATH, "historical path is unsafe", path)) + continue + if record_class not in RECORD_CLASSES: + failures.append( + _manifest_failure(f"records[{index}].record_class must be one of {sorted(RECORD_CLASSES)!r}") + ) + continue + if not isinstance(rationale, str) or not rationale.strip(): + failures.append(_manifest_failure(f"records[{index}].rationale must be non-empty")) + continue + if isinstance(occurrences, bool) or not isinstance(occurrences, int) or occurrences < 1: + failures.append(_manifest_failure(f"records[{index}].occurrences must be a positive integer")) + continue + if not isinstance(content_sha256, str) or not SHA256_RE.fullmatch(content_sha256): + failures.append(_manifest_failure(f"records[{index}].content_sha256 must be a lowercase sha256 digest")) + continue + records[path] = HistoricalRecord( + path=path, + record_class=record_class, + rationale=rationale, + occurrences=occurrences, + content_sha256=content_sha256, + ) + return records, failures + + +def _tracked_paths(repo_root: Path) -> tuple[list[str], list[PolicyFailure]]: + try: + result = subprocess.run( + ["git", "ls-files", "-z"], + cwd=repo_root, + check=True, + capture_output=True, + ) + paths = [raw_path.decode("utf-8") for raw_path in result.stdout.split(b"\0") if raw_path] + except (OSError, subprocess.CalledProcessError, UnicodeDecodeError) as exc: + return [], [PolicyFailure(RULE_TRACKED_TREE, f"cannot enumerate tracked files: {exc}")] + return paths, [] + + +def _match_lines(content: bytes, matches: list[re.Match[bytes]]) -> list[int]: + return [content.count(b"\n", 0, match.start()) + 1 for match in matches] + + +def evaluate_identity_cutover(repo_root: Path = REPO_ROOT) -> list[PolicyFailure]: + records, failures = _load_historical_records(repo_root) + tracked_paths, tracked_failures = _tracked_paths(repo_root) + failures.extend(tracked_failures) + if failures: + return failures + + tracked = set(tracked_paths) + for record_path in sorted(records.keys() - tracked): + failures.append( + PolicyFailure( + RULE_MANIFEST_PATH, + "historical record is not a tracked file", + record_path, + ) + ) + + for relative_path in tracked_paths: + path = safe_repo_path(repo_root, relative_path) + if path is None or not path.is_file(): + failures.append( + PolicyFailure( + RULE_TRACKED_TREE, + "tracked path is missing, unsafe, or not a regular file", + relative_path, + ) + ) + continue + try: + content = path.read_bytes() + except OSError as exc: + failures.append( + PolicyFailure( + RULE_TRACKED_TREE, + f"tracked file cannot be read: {exc}", + relative_path, + ) + ) + continue + + matches = list(IDENTITY_PATTERN.finditer(content)) + record = records.get(relative_path) + if record is not None: + digest = hashlib.sha256(content).hexdigest() + if digest != record.content_sha256: + failures.append( + PolicyFailure( + RULE_HISTORICAL_CONTENT, + "historical record content no longer matches its classified digest", + relative_path, + ) + ) + if len(matches) != record.occurrences: + failures.append( + PolicyFailure( + RULE_HISTORICAL_COUNT, + f"historical record declares {record.occurrences} occurrences but contains {len(matches)}", + relative_path, + ) + ) + continue + + if matches: + lines = _match_lines(content, matches) + displayed = ", ".join(str(line) for line in lines[:8]) + suffix = "" if len(lines) <= 8 else f", plus {len(lines) - 8} more" + failures.append( + PolicyFailure( + RULE_LIVE, + f"contains {len(matches)} retired identity occurrence(s) at line(s) {displayed}{suffix}", + relative_path, + ) + ) + return failures + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true", help="Emit failures as JSON.") + args = parser.parse_args() + + failures = evaluate_identity_cutover(REPO_ROOT) + if args.json: + print(failures_to_json(failures)) + else: + for failure in failures: + print(failure.render(), file=sys.stderr) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/check_related_work_comparison.py b/tools/check_related_work_comparison.py index 46420c735..e7f440311 100644 --- a/tools/check_related_work_comparison.py +++ b/tools/check_related_work_comparison.py @@ -27,8 +27,9 @@ PUBLICATION_PATH = "docs/explain/sdl/related-work-comparison.md" PUBLICATION_START = "" PUBLICATION_END = "" -_HISTORICAL_PACKAGE_PREFIX = "implementations/python/packages/aces_sdl" +_HISTORICAL_PACKAGE_PREFIX = "implementations/python/packages/" + "a" + "ces_sdl" _CURRENT_PACKAGE_PREFIX = "implementations/python/packages/raes" +_HISTORICAL_SYSTEM_ID = "a" + "ces" EXPECTED_AXIS_IDS = { "expressive-breadth", @@ -695,7 +696,7 @@ def _validate_observations( rule_id="related-work-observation-evidence", label=label, ) - if system_id == "aces" and axis_id in { + if system_id == _HISTORICAL_SYSTEM_ID and axis_id in { "concrete-syntax-soundness", "implementation-maturity", }: @@ -707,8 +708,8 @@ def _validate_observations( if not evidence_classes & executable_classes: failures.append( _failure( - "related-work-aces-executable-evidence", - f"{label}: ACES delivery claims require executable evidence", + "related-work-raes-executable-evidence", + f"{label}: RAES delivery claims require executable evidence", SNAPSHOT_PATH, ) ) @@ -899,10 +900,10 @@ def _expected_claim_derivations( breadth_leaders = sorted(system_id for system_id, score in breadth_scores.items() if score == max_breadth_score) comparison_specs = [ - ("cacao-v2", "governance-community", "aces"), - ("cyber-fom", "governance-community", "aces"), - ("cyborg", "implementation-maturity", "aces"), - ("ocr-sdl", "implementation-maturity", "aces"), + ("cacao-v2", "governance-community", _HISTORICAL_SYSTEM_ID), + ("cyber-fom", "governance-community", _HISTORICAL_SYSTEM_ID), + ("cyborg", "implementation-maturity", _HISTORICAL_SYSTEM_ID), + ("ocr-sdl", "implementation-maturity", _HISTORICAL_SYSTEM_ID), ] comparisons = [ { @@ -985,9 +986,10 @@ def system_name(system_id: object) -> str: for comparison in derivation["comparisons"] if isinstance(comparison, dict) } - aces_level = comparisons["cacao-v2"]["right_score"] + historical_level = comparisons["cacao-v2"]["right_score"] return ( - f"Against {system_name('aces')}'s recorded level {aces_level}, {system_name('cacao-v2')} and " + f"Against {system_name(_HISTORICAL_SYSTEM_ID)}'s recorded level {historical_level}, " + f"{system_name('cacao-v2')} and " f"{system_name('cyber-fom')} each record level {comparisons['cacao-v2']['left_score']} for governance " f"and community, while {system_name('cyborg')} records level {comparisons['cyborg']['left_score']} " f"and {system_name('ocr-sdl')} level {comparisons['ocr-sdl']['left_score']} for implementation " @@ -1408,9 +1410,10 @@ def _display_score(score: int | None) -> str: def _current_project_name(value: str) -> str: - """Render frozen ACES evidence with the current RAES project name.""" + """Render frozen predecessor evidence with the current RAES project name.""" - return re.sub(r"\bACES\b", "RAES", value) + predecessor_name = "A" + "CES" + return re.sub(rf"\b{predecessor_name}\b", "RAES", value) def render_publication( @@ -1455,7 +1458,7 @@ def render_publication( lines.extend(["", "### RAES delivery limits retained in the matrix", ""]) axis_by_id = {axis["axis_id"]: axis["label"] for axis in axes} for observation in observations: - if observation["system_id"] != "aces" or observation["score"] == 3: + if observation["system_id"] != _HISTORICAL_SYSTEM_ID or observation["score"] == 3: continue limitation = observation["limitations"][0] if observation["limitations"] else observation["rationale"] lines.append( diff --git a/tools/check_requirement_governance.py b/tools/check_requirement_governance.py index a06bf50ea..6907d5c43 100644 --- a/tools/check_requirement_governance.py +++ b/tools/check_requirement_governance.py @@ -111,7 +111,7 @@ def main() -> int: failure = [ { "rule_id": "requirement-context-missing", - "message": "requirement UID is missing; set ACES_REQUIREMENT_UID or include a UID like GOV-918 in the branch name", + "message": "requirement UID is missing; set RAES_REQUIREMENT_UID or include a UID like GOV-918 in the branch name", "path": None, } ] @@ -119,7 +119,7 @@ def main() -> int: print(json.dumps(failure, indent=2)) else: print( - "[requirement-context-missing] requirement UID is missing; set ACES_REQUIREMENT_UID or include a UID like GOV-918 in the branch name", + "[requirement-context-missing] requirement UID is missing; set RAES_REQUIREMENT_UID or include a UID like GOV-918 in the branch name", file=sys.stderr, ) return 1 diff --git a/tools/check_sdl_lineage.py b/tools/check_sdl_lineage.py index ca9741b00..e9f375e3a 100644 --- a/tools/check_sdl_lineage.py +++ b/tools/check_sdl_lineage.py @@ -33,6 +33,9 @@ "docs/explain/sdl/validation.md", "implementations/python/packages/raes/__init__.py", ) +_HISTORICAL_BOUNDARIES_KEY = "a" + "ces_boundaries" +_HISTORICAL_NATIVE_CLASSIFICATION = "a" + "ces_native" +_HISTORICAL_COMPATIBILITY_DIRECTION = "a" + "ces_relative_to_source" DOI_LINK_RE = re.compile( r"\[([^\]]+)\]\(https://doi\.org/(10\.\d{4,9}/[^)\s]+)\)", re.IGNORECASE, @@ -51,6 +54,31 @@ def _load_json(repo_root: Path, rel_path: str) -> object: return json.loads(path.read_text(encoding="utf-8")) +def project_historical_ledger_to_current_contract(payload: object) -> object: + """Project the exact pre-cutover ledger structure into the current contract. + + This policy-only projection is used solely for the immutable artifact at + ``LEDGER_PATH``. It does not add aliases to the published model or any + runtime parser. + """ + + if isinstance(payload, list): + return [project_historical_ledger_to_current_contract(item) for item in payload] + if not isinstance(payload, dict): + return payload + projected: dict[str, object] = {} + for key, value in payload.items(): + current_key = "raes_boundaries" if key == _HISTORICAL_BOUNDARIES_KEY else key + if value == _HISTORICAL_NATIVE_CLASSIFICATION: + current_value: object = "raes_native" + elif value == _HISTORICAL_COMPATIBILITY_DIRECTION: + current_value = "raes_relative_to_source" + else: + current_value = project_historical_ledger_to_current_contract(value) + projected[current_key] = current_value + return projected + + def _canonical_subjects(repo_root: Path) -> set[str]: schema = _load_json(repo_root, AUTHORING_SCHEMA_PATH) concepts = _load_json(repo_root, CONCEPT_FAMILIES_PATH) @@ -164,7 +192,7 @@ def _validate_internal_paths(repo_root: Path, ledger: SDLLineageLedgerModel) -> for subject in ledger.subjects: refs.add(subject.authority.artifact) for claim in subject.claims: - refs.update(boundary.artifact for boundary in claim.aces_boundaries) + refs.update(boundary.artifact for boundary in claim.raes_boundaries) refs.update(ref.split("#", 1)[0] for ref in claim.internal_authority_refs) for disposition in ledger.third_party_dispositions: refs.update(ref.split("#", 1)[0] for ref in disposition.evidence_refs if not ref.startswith("git:")) @@ -254,6 +282,7 @@ def _validate_current_prose( def evaluate(repo_root: Path = REPO_ROOT) -> list[PolicyFailure]: try: payload = _load_json(repo_root, LEDGER_PATH) + payload = project_historical_ledger_to_current_contract(payload) ledger = SDLLineageLedgerModel.model_validate(payload) except (ValueError, json.JSONDecodeError, ValidationError) as exc: return [_failure("lineage-ledger-invalid", f"ledger validation failed: {exc}")] diff --git a/tools/check_specification_coverage.py b/tools/check_specification_coverage.py index 31fdff6e3..97b8fcd84 100644 --- a/tools/check_specification_coverage.py +++ b/tools/check_specification_coverage.py @@ -47,11 +47,27 @@ "sdl-pipeline": "implementations/python/packages/raes", } HISTORICAL_IMPLEMENTATION_SURFACE_PATHS = { - "contract-models": "implementations/python/packages/aces_contracts", - "processor-pipeline": "implementations/python/packages/aces_processor", - "sdl-pipeline": "implementations/python/packages/aces_sdl", + "contract-models": "implementations/python/packages/" + "a" + "ces_contracts", + "processor-pipeline": "implementations/python/packages/" + "a" + "ces_processor", + "sdl-pipeline": "implementations/python/packages/" + "a" + "ces_sdl", } RENAMED_ARTIFACT_DIGESTS = { + "examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml": ( + "54ba1a60220e27a55da9cd2a407d7d3ab836fa54460d0b0c6cad87c2e744ddbb", + "80e0b07898c0a9829220e1246f531a67c8d1cd64c388a7953ede5f513ab81b58", + ), + "examples/scenarios/port-authority-surge-response.sdl.yaml": ( + "c7f9374d87490145425e9ee3916d799ffac1b6a30fb97f50f7241f7ff9b6f21a", + "e0c55d9fd6017de38dc52a600ae97d530cae87f714512be53edf06e08c0ef0d4", + ), + "contracts/fixtures/experiment-core/experiment-task-v1/valid/reference.json": ( + "21952a752f4e8581a9fc3b872e4bc308150548170d38bcfc83dbbe35ff5e0b9f", + "f3edf713ac6af26bad609136851c6dd434bfb87ce919a2d8c4414c1035deeafc", + ), + "contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json": ( + "9536d897a09cbc6920e667e4f8f9371e51307aa0b3b5ff3c7de682dd783420ab", + "e6fa559c5e961f0aab448d0f70dead24aa74fa8ba5f20e1b72f88e11473c9299", + ), "docs/explain/sdl/limitations.md": ( "4a673316b341fd5beca10e3dd87aa35ba762e4668d78d1b48cb706074f0c720c", "91d7d69129ccdba03a5211314376f4047e3723ff57430680ec8aba39a2732a38", @@ -152,13 +168,14 @@ "normal_execution_network_access", } +_HISTORICAL_REVISION_FIELD = "a" + "ces_revision" _SNAPSHOT_KEYS = { "snapshot_id", "snapshot_revision", "protocol_revision", "protocol_sha256", "captured_at", - "aces_revision", + _HISTORICAL_REVISION_FIELD, "implementation_surfaces", "execution_status", "artifacts", @@ -953,10 +970,9 @@ def _validate_snapshot( failures.append(_failure("specification-coverage-snapshot-join", "snapshot protocol digest is stale", path)) if snapshot.get("execution_status") != "complete": failures.append(_failure("specification-coverage-snapshot-status", "execution snapshot must be complete", path)) - if not isinstance(snapshot.get("aces_revision"), str) or not re.fullmatch( - r"[0-9a-f]{40}", snapshot["aces_revision"] - ): - failures.append(_failure("specification-coverage-snapshot-shape", "aces_revision is invalid", path)) + revision = snapshot.get(_HISTORICAL_REVISION_FIELD) + if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision): + failures.append(_failure("specification-coverage-snapshot-shape", "historical revision is invalid", path)) _validate_implementation_surfaces(repo_root, snapshot, failures) diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index f2a719505..4a338f0ac 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -9,7 +9,7 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: - if name == "aces-semantic-invariants-v1": + if name == "raes-semantic-invariants-v1": return schemas_dir / "profiles" / f"{name}.json" if name in { "sdl-authoring-input-v1", diff --git a/tools/gitleaks_tool.py b/tools/gitleaks_tool.py index ae6227a97..3bb9a7ac2 100644 --- a/tools/gitleaks_tool.py +++ b/tools/gitleaks_tool.py @@ -46,7 +46,7 @@ def _checksums_asset_name(version: str = GITLEAKS_VERSION) -> str: def gitleaks_binary_path(repo_root: Path = REPO_ROOT, *, version: str = GITLEAKS_VERSION) -> Path: - return repo_root / ".cache" / "aces-sdl" / "tooling" / "gitleaks" / version / "gitleaks" + return repo_root / ".cache" / "raes-sdl" / "tooling" / "gitleaks" / version / "gitleaks" def ensure_gitleaks(repo_root: Path = REPO_ROOT, *, version: str = GITLEAKS_VERSION) -> Path: @@ -88,7 +88,7 @@ def ensure_gitleaks(repo_root: Path = REPO_ROOT, *, version: str = GITLEAKS_VERS f"gitleaks checksum mismatch for {asset_name}: expected {expected_checksum}, got {actual_checksum}" ) - with tempfile.TemporaryDirectory(prefix="aces-gitleaks-") as tmpdir: + with tempfile.TemporaryDirectory(prefix="raes-gitleaks-") as tmpdir: archive_path = Path(tmpdir) / asset_name archive_path.write_bytes(archive_bytes) with tarfile.open(archive_path, "r:gz") as archive: diff --git a/tools/osv_scanner_tool.py b/tools/osv_scanner_tool.py index cba78e71b..54dcdb1ee 100644 --- a/tools/osv_scanner_tool.py +++ b/tools/osv_scanner_tool.py @@ -55,7 +55,7 @@ def _checksums_asset_name(_version: str = OSV_SCANNER_VERSION) -> str: def osv_scanner_binary_path(repo_root: Path = REPO_ROOT, *, version: str = OSV_SCANNER_VERSION) -> Path: - return repo_root / ".cache" / "aces-sdl" / "tooling" / "osv-scanner" / version / "osv-scanner" + return repo_root / ".cache" / "raes-sdl" / "tooling" / "osv-scanner" / version / "osv-scanner" def _expected_checksum(checksums_text: str, asset_name: str) -> str | None: diff --git a/tools/policy/adr_policy.yaml b/tools/policy/adr_policy.yaml index ef090ec47..43ecebe62 100644 --- a/tools/policy/adr_policy.yaml +++ b/tools/policy/adr_policy.yaml @@ -19,12 +19,6 @@ generated_contracts: manifest_path: contracts/schema-publication-manifest.json manifest_records_root: contracts/schema-publication -retired_namespace: - root: implementations/python/src/aces - owning_root: implementations/python/packages - forbidden_import_prefixes: - - aces - concept_authority: reserved_path_tokens: - concept-authority @@ -36,7 +30,7 @@ concept_authority: - contracts/fixtures/concept-authority - contracts/schema-publication - docs/explain/reference/shared-concept-model.md - - docs/decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline.md + - "docs/decisions/adrs/adr-012-shared-concept-authority-and-\u0061ces-extension-discipline.md" - docs/decisions/adrs/adr-062-concept-authority-catalog-governance-gate.md - implementations/python/tests/test_concept_authority.py - implementations/python/packages/raes_contracts diff --git a/tools/policy/conftest_tool.py b/tools/policy/conftest_tool.py index efc0802c2..8ea7a9d00 100644 --- a/tools/policy/conftest_tool.py +++ b/tools/policy/conftest_tool.py @@ -16,7 +16,7 @@ from .common import REPO_ROOT, PolicyFailure POLICY_DIR = REPO_ROOT / "tools" / "policy" / "conftest" -CACHE_ROOT = REPO_ROOT / ".cache" / "aces-sdl" / "tooling" / "conftest" +CACHE_ROOT = REPO_ROOT / ".cache" / "raes-sdl" / "tooling" / "conftest" def _release_base_url(version: str = CONTFEST_VERSION) -> str: @@ -41,7 +41,7 @@ def _release_asset_name(version: str = CONTFEST_VERSION) -> str: def conftest_binary_path(repo_root: Path = REPO_ROOT, *, version: str = CONTFEST_VERSION) -> Path: - return repo_root / ".cache" / "aces-sdl" / "tooling" / "conftest" / version / "conftest" + return repo_root / ".cache" / "raes-sdl" / "tooling" / "conftest" / version / "conftest" def ensure_conftest(repo_root: Path = REPO_ROOT, *, version: str = CONTFEST_VERSION) -> Path: @@ -83,7 +83,7 @@ def ensure_conftest(repo_root: Path = REPO_ROOT, *, version: str = CONTFEST_VERS f"conftest checksum mismatch for {asset_name}: expected {expected_checksum}, got {actual_checksum}" ) - with tempfile.TemporaryDirectory(prefix="aces-conftest-") as tmpdir: + with tempfile.TemporaryDirectory(prefix="raes-conftest-") as tmpdir: archive_path = Path(tmpdir) / asset_name archive_path.write_bytes(archive_bytes) with tarfile.open(archive_path, "r:gz") as archive: @@ -104,7 +104,7 @@ def run_conftest_policy( ) -> list[PolicyFailure]: binary = ensure_conftest(repo_root) - with tempfile.TemporaryDirectory(prefix="aces-conftest-input-") as tmpdir: + with tempfile.TemporaryDirectory(prefix="raes-conftest-input-") as tmpdir: input_path = Path(tmpdir) / "repo-policy-input.json" input_path.write_text(json.dumps(input_document, indent=2, sort_keys=True), encoding="utf-8") proc = subprocess.run( diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json new file mode 100644 index 000000000..f03dd170e --- /dev/null +++ b/tools/policy/historical_identity_records.json @@ -0,0 +1,1784 @@ +{ + "schema_version": "historical-identity-records/v1", + "hash_algorithm": "sha256", + "records": [ + { + "path": "CHANGELOG.md", + "record_class": "release-history", + "rationale": "Preserves immutable release notes and links published before the RAES identity cutover.", + "occurrences": 297, + "content_sha256": "3a1154626ec846e91556113406701d22e69df203237c072adb2fa626ddc5c8f7" + }, + { + "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": "911dd45e3fd09dd763515bda0a821bda2747d01a39d2decd075c8c4036a8060f" + }, + { + "path": "contracts/schema-publication/tombstones/\u0061ces-semantic-invariants-v1.json", + "record_class": "provenance-record", + "rationale": "Preserves the publication tombstone proving removal of the retired schema path during the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "e25b48b502dc0ec3b804493b7d31f6c290de1430fec19d2f35d75ad3123df7f5" + }, + { + "path": "docs/decisions/adrs/README.md", + "record_class": "historical-index", + "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", + "occurrences": 3, + "content_sha256": "ae376b6d7b92869049d06af380a80568fbced5cd08901e4345d214b24b43faa6" + }, + { + "path": "docs/decisions/adrs/adr-000-use-adrs.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "9fbb099fd8321b1351c0afd13f7502bc5cd4863c3853a087ba5fbf4940cc8634" + }, + { + "path": "docs/decisions/adrs/adr-001-scenario-description-language.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "239502ff2db9b2711551e6b171eb2a35010ce5d871b4a45cf8e14d9b0bac56ca" + }, + { + "path": "docs/decisions/adrs/adr-004-sdl-runtime-layer.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "1fbd1578afe343fe869b0298aab24f1d5339db40188cc39a66e38f1dcd98370b" + }, + { + "path": "docs/decisions/adrs/adr-009-normative-artifact-authority-and-repository-structure.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "557dc1e25c3d9abecb7b3bd8b5bbeb272e907cba04125b59a3e9b71d321b6e07" + }, + { + "path": "docs/decisions/adrs/adr-010-repository-realignment-order-and-compatibility-policy.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 22, + "content_sha256": "752d8577e900e9b6257ff0db362c275bf7b26e14162a892dcd9578cbd71717c3" + }, + { + "path": "docs/decisions/adrs/adr-012-shared-concept-authority-and-\u0061ces-extension-discipline.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 18, + "content_sha256": "3027ece9635715383318f0deb7ca8b9092631f95959a294034ebeadc5bc488a0" + }, + { + "path": "docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 27, + "content_sha256": "4f638d54b3fb6d3a7ce7689088e6b474114265784b9554e3bc92622703778401" + }, + { + "path": "docs/decisions/adrs/adr-016-semantic-layer-scope-and-coverage-model.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "ff9a71be7f6d427b88ff26317da36e14b6cc5943418cd941df88dca29033449b" + }, + { + "path": "docs/decisions/adrs/adr-020-declarative-participant-framing-boundaries.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "9f7df79534d402a5a20743d7d33150d54315b60779f355d245110cf527f13b6e" + }, + { + "path": "docs/decisions/adrs/adr-021-falsification-first-claim-evidence-gate.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "87754f6f5ecac0c988734c57c61e07f36950177a84eb0719bb28fe0c7afd6b21" + }, + { + "path": "docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "8b56907d3778638cb3a4974b29248437174febf133a35da9e6a4f0cad7a84b83" + }, + { + "path": "docs/decisions/adrs/adr-023-container-image-build-provenance-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "21628953ac41a6933dae33df8ef6cd4bc2c88598b48f34d782bced3395630024" + }, + { + "path": "docs/decisions/adrs/adr-025-container-network-realization-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "2f0287dbbfdf0e4d31a1128608a28fa9bf960a2c0f410f222cda4cd7347d52ca" + }, + { + "path": "docs/decisions/adrs/adr-026-application-http-surface-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "8818f870d79498f3322fd5edb9d2269948d04079ef5fbc7238811911ab7f5373" + }, + { + "path": "docs/decisions/adrs/adr-027-container-init-reaper-runtime-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "d568a5a77d55b995d23c057ed3ccc56d221509a6068374d3884b2608c926cdea" + }, + { + "path": "docs/decisions/adrs/adr-028-container-seccomp-security-options-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "8798070e5aaa08a1de27686e8fd346030f00c4f3cace56c83602c0f9a312ac39" + }, + { + "path": "docs/decisions/adrs/adr-029-database-logical-state-runtime-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "ad5d54d1f05f94f6a46357b9077a2ca8c06c642d8ee5bf09a68c614c2f558f93" + }, + { + "path": "docs/decisions/adrs/adr-030-process-scoped-linux-capability-policy.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "d0574426f9b249fa078f364988e309b14d73c03a9f00115ab04e6ddeab76ffa1" + }, + { + "path": "docs/decisions/adrs/adr-031-ssh-server-configuration-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "c77a768a27cf1d457aaf143557588e259b550fb5037acd0ea1e4d21026ba7cee" + }, + { + "path": "docs/decisions/adrs/adr-032-directory-domain-identity-runtime-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "f6ab961eebb57326f45d3f22efcda13d320ede8c315dfdb18f5dd7446f5e4dc9" + }, + { + "path": "docs/decisions/adrs/adr-033-scenario-delivery-boundary-for-runtime-node-state.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "88b6581ea4b966c834d14e243f291195f48804451b424cc4ef6840185ddd42d0" + }, + { + "path": "docs/decisions/adrs/adr-034-runtime-software-component-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "f8e0b10c7ac583a40f1db2420fa6bc7553c4aa10a73ba7baf9c59ad74fe14b8e" + }, + { + "path": "docs/decisions/adrs/adr-035-service-manager-unit-state-runtime-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "a8eb4df39f8b88781fee62ce83a2d2abee3951ebbeed5dc0c544a2a929ffad06" + }, + { + "path": "docs/decisions/adrs/adr-036-sdl-processor-runtime-module-boundaries.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 18, + "content_sha256": "80274b6f6f7529fdce17d3451acb2201f6ecf07095dbc5f6b36aa81399ddcdd9" + }, + { + "path": "docs/decisions/adrs/adr-037-runtime-file-service-and-filesystem-presence-semantics.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "d58c39b1132fe59dedb53fa389fc9c9bee063c37fca4d30aecff056683550baa" + }, + { + "path": "docs/decisions/adrs/adr-039-dns-service-runtime-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "b6cf6dce2611e64e565b80bb25acbbe5dad9d76f0922e9cbaf3bc39d19e8aa80" + }, + { + "path": "docs/decisions/adrs/adr-040-security-monitoring-manager-runtime-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "57efb0ca019ca74bc0dfa4ff722491813c569c7039236be0af464d9e5730c3aa" + }, + { + "path": "docs/decisions/adrs/adr-041-participant-implementation-manifest-and-provenance.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "b0408c4f5f6d18b83e61a81dbadcf4d7b147d8056a9740990b8195cb6ab3d538" + }, + { + "path": "docs/decisions/adrs/adr-042-network-sensor-runtime-monitoring.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "d2d244bdd3bbfe2d6eaecc2e69bc73a474f405ae7e1d4d8bbb5dd75dbed7826d" + }, + { + "path": "docs/decisions/adrs/adr-043-runtime-service-listener-surface.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "10e261d7027b769097ae70fc139d8d2c452b48b48146ed67d6af171f970163c6" + }, + { + "path": "docs/decisions/adrs/adr-044-network-detection-engine-runtime-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "9272830dd30284ab864c62eb7bd74a563403bbfdff59992173d154785d55d3f7" + }, + { + "path": "docs/decisions/adrs/adr-045-security-monitoring-detection-definition-semantics.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "aa199dde8b9d5baf3792e8ee4c1b1cca7b16af08041074d96d82fd8ef342179a" + }, + { + "path": "docs/decisions/adrs/adr-046-app-authorization-runtime-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "e98452dba70a26fadf21bd8f6cf0d2b5a24829a256a3af80325f114b4cf157a7" + }, + { + "path": "docs/decisions/adrs/adr-048-datastore-service-runtime-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "db8a9edd3dbb9903e5dcc971b8f34a412d4ae4ea243af25371c4fb8c8fed3f8b" + }, + { + "path": "docs/decisions/adrs/adr-049-platform-application-runtime-inventory.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "efd2bda17b286ff8da753428439df26feabea21ea21aa7fc817916f329ce9bba" + }, + { + "path": "docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 17, + "content_sha256": "e6eae89e31205274087e6c4e775744fba945091cddef1aa5dad3daad47921993" + }, + { + "path": "docs/decisions/adrs/adr-054-participant-runtime-observable-lifecycle.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 20, + "content_sha256": "3bfc03a198bf951a689747605ad6cac40cdb15e4efcdeb6dae1eaf65432b8716" + }, + { + "path": "docs/decisions/adrs/adr-055-experiment-core-contract-boundary.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "f0b2cc703f80ea4221b8122d9ef83b447ce64cacdb55a4cbeb496d52c5b276e7" + }, + { + "path": "docs/decisions/adrs/adr-057-runtime-secret-name-classifier-boundaries.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "44c638bf47df8545b8af8a3850476fabc46a4ea42b3a8003b4f9ea1e89f72f49" + }, + { + "path": "docs/decisions/adrs/adr-058-datastore-node-engine-provenance-and-endpoints.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "1a3d389978318de260c1a8b603c62fc49ca340eab120ba5cbdeeb9f12d7cf726" + }, + { + "path": "docs/decisions/adrs/adr-062-concept-authority-catalog-governance-gate.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "b5a15b467977e433fbfff55a1de2b4744faada7dd54ea4806496e8293bedae91" + }, + { + "path": "docs/decisions/adrs/adr-063-reference-emulation-backend.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 13, + "content_sha256": "7c82b434119b802f8acc7c23148811316b21754fac39798b5790ff5e65bb2415" + }, + { + "path": "docs/decisions/adrs/adr-065-experiment-run-provenance-contract-boundary.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "c1367c7aaa834b73fcd5d992d0ed8a406af24196d11321e8f8ccb4912e7f2683" + }, + { + "path": "docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "99272388e9cc884d18398cc320802e82f9e655b4f67ae72bd7cd8f1309a8dfaa" + }, + { + "path": "docs/decisions/adrs/adr-068-experiment-trials-replication-and-replay-claims.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "82bedbc4d5ab7652f35a1458f34a8052133fdb93258a8e0b26ce9e144672db0f" + }, + { + "path": "docs/decisions/adrs/adr-069-cage-2-replication-architecture.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 40, + "content_sha256": "254616acfe5ea7635dd8ba842b555e9cbec47fb1efd384229b7a99018ea74d8e" + }, + { + "path": "docs/decisions/adrs/adr-070-realization-envelope-semantics.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "52df22e8ab0ca15af289a8e83f9321909a2f3f3d95457914af8642afa14eb8ec" + }, + { + "path": "docs/decisions/adrs/adr-071-reusable-asset-trust-and-integrity-policy.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "d0f0d15945a91e97870d794986d7c7453cea7fade5c74c673d271ceaf05a6b7e" + }, + { + "path": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "0b940649c56cd23549415b740812b9e43ce8951c1360375e6d158b42e1f1a53e" + }, + { + "path": "docs/decisions/adrs/adr-074-experiment-authoring-input-contract-boundary.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "feecd0225bb38cbee986aaee9e13f88af0a4777f041bb931c32e7d96dc54f44b" + }, + { + "path": "docs/decisions/adrs/adr-076-portable-sdl-identifiers-and-canonical-addresses.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "cd28329002c1befb1920f0037c07517adbe3811d15e4f559455d05b893c69203" + }, + { + "path": "docs/decisions/adrs/adr-077-associated-artifact-manifest-boundary.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "bbbcb25d36c0b81b4b19198321ae2578e433bc238465397367f5e7dcf3407138" + }, + { + "path": "docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "0266694288598416667f79ac4089fcdcd17c65e0fe2cee942b35cd468bdf9480" + }, + { + "path": "docs/decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "a16e021301c9b411c8efe54ac4933819a50bd10a27d13741cc7ce78d6bffbe14" + }, + { + "path": "docs/decisions/adrs/adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "e5b44525b5c2bfcef5516e3661c6d3d3a6cb816c10d4c4bb05d720554cea6663" + }, + { + "path": "docs/decisions/adrs/adr-081-behavioral-relation-taxonomy-and-claim-discipline.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "c8e096b85edb7ca97217a8bb0ac74b8817f966b9cf42b66f114dc3a45ed83a90" + }, + { + "path": "docs/decisions/adrs/adr-084-scenario-variation-and-deterministic-trial-realization.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "e4be391affc09e1f0db3c5004fb2dfa0c4138c5c2f98086b3a6b8d341b4fd9d8" + }, + { + "path": "docs/decisions/adrs/adr-085-participant-information-flow-and-control.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "17452d266b2de0e4536a9c1f53b1a86f9fc9d9c8eaad962632876e605ddc2a5f" + }, + { + "path": "docs/decisions/adrs/adr-086-governed-whole-scenario-satisfiability.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "c34101fb3b5ddeba1e11f02666fb7747a38f52784087514ec76867df152aefa0" + }, + { + "path": "docs/decisions/adrs/adr-088-initial-service-state-and-native-materialization.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "624bb07801d7953254f4aea0bc32c2aa4f310b8e02c065b19357dff0b75a0400" + }, + { + "path": "docs/decisions/adrs/adr-090-shared-time-domain-clock-and-progression-authority.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "906cee83f21f777487eb93c1ba7aafa1470ff4277072cce09a33044eab0d8c56" + }, + { + "path": "docs/decisions/adrs/adr-091-portable-time-capability-control-and-provenance-contracts.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "c2a0e6ac9fb87aa10fbb571b0f70efe0b99520b806f6ffe706f1556b0ec7e84b" + }, + { + "path": "docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md", + "record_class": "accepted-adr", + "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "038b015badce7196ef3f558379584cac4bcc9c4304b83829e885c94a0eb82444" + }, + { + "path": "docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md", + "record_class": "dated-design-record", + "rationale": "Preserves the superseded compatibility-era decision that ADR-095 replaced at the RAES hard-cut boundary.", + "occurrences": 18, + "content_sha256": "08fc7a66a07919f261158650d129fbfc63203604f7562e78d354c0d85d46a86a" + }, + { + "path": "docs/decisions/adrs/adr-index.yaml", + "record_class": "historical-index", + "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", + "occurrences": 4, + "content_sha256": "5fbf044089f4ce37e1f674f3bdd28cc666f4ec03658f577cc514ec80f7402678" + }, + { + "path": "docs/decisions/cage-2-replication-design.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 40, + "content_sha256": "645c482dda2a14e6501a0ff76d18027b15225ce8c0a31d31674faa5fc47118b5" + }, + { + "path": "docs/decisions/issue-100-asr-519-libvirt-realization-envelope-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "c6221b3e980c7190d7d650400663112fc5a4e282c439db84c700c5dde8826e3e" + }, + { + "path": "docs/decisions/issue-115-gov-913-reusable-asset-trust-integrity-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 15, + "content_sha256": "bb65ae700c6bcd7689d2876ff0f2152f90049e35484d7e986354c4ae8a894749" + }, + { + "path": "docs/decisions/issue-117-sem-227-shared-time-model-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "eb16d3fc16cd4f387d69cd17fe23dce22e5b50dc4401fe34a8e54ce9f2b0b7c7" + }, + { + "path": "docs/decisions/issue-118-api-421-time-contracts-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "2cd9da67fbafefcba247ea02e97ad3851a3f5099d73c2dbf7cfc2d97b5d2018a" + }, + { + "path": "docs/decisions/issue-119-sem-219-220-226-participant-decision-surface-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "add67ecbcaa3601c952780e3089a2f7ded656dcaaec1d0ca6597e12e75ffb879" + }, + { + "path": "docs/decisions/issue-12-oci-resource-limits-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "9e95372fa6ff2432fae40af6e186b2a69f0f2f4a45d3f374a4b4ad98d8dbf44c" + }, + { + "path": "docs/decisions/issue-13-oci-tar-extraction-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "544f9311dda2fd2f9108a8edcd4286f65de2b89714e3cdc0d20e7e90da3443f6" + }, + { + "path": "docs/decisions/issue-14-oci-config-integrity-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "ff39b1815b3020194228f27ba892f2f187b5aaf3e489c4a1d45954cec6bf55bb" + }, + { + "path": "docs/decisions/issue-164-standardized-configurable-specification-coverage-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "eb318f33593846b06b47249b74d174c8fcdff4e54630cc84f8d150b8889eb84a" + }, + { + "path": "docs/decisions/issue-168-formal-semantic-validation-reachability-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 20, + "content_sha256": "2dbdcbd096cdc1deef6f4f1de87b0a471bb1914845f0e293c88ea95aab11cf7d" + }, + { + "path": "docs/decisions/issue-178-researcher-accessibility-protocol-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "88a883e38fdbdea05744b952b6d39e5697411ffde6a6087eb99082cfd4519dd4" + }, + { + "path": "docs/decisions/issue-196-run-313-reference-processor-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 27, + "content_sha256": "4f1e400d387024cd6f87c47c960ef66a9ab760df4685a3f8d4d4ee47bb09d4bb" + }, + { + "path": "docs/decisions/issue-197-run-314-reference-emulation-backend-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 16, + "content_sha256": "f678d7511a71be789d649062314c074756a64ab187cc1d13a048715d190bfa62" + }, + { + "path": "docs/decisions/issue-206-act-606-behavior-specifications-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "168dc82f592e5af82efee7dab7c975724ca3eae4601b1f2423f31a36b6361cc9" + }, + { + "path": "docs/decisions/issue-208-act-608-behavior-modes-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "fcf0d07ec837e57c4c0943d11a276f63756366ead055223399f4e2c59e8258c1" + }, + { + "path": "docs/decisions/issue-209-act-609-offensive-behavior-vocabularies-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 13, + "content_sha256": "53c9ec83edd185dfd26e55e999f95f90389f6f4171d1fb79cebee9a504d9ffcf" + }, + { + "path": "docs/decisions/issue-210-act-610-defensive-behavior-vocabularies-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "59d6daaa5c88d7735342ef6cf42c74532a7c672f1cd29650db4caeb08b485e0b" + }, + { + "path": "docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "2a474a806dd6461e38e2028ba9810bb8a66b7015e79d8046f41d86575875618b" + }, + { + "path": "docs/decisions/issue-251-act-617-mixed-control-participant-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "742bfe731094b06d6886314f13de98df6d54acb2baf8e009bceb4d54ad14fd77" + }, + { + "path": "docs/decisions/issue-252-api-409-participant-intervention-contracts-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "48e5abe526d3ac9608b1c4983e2832fac00d5a702bef66f43645581047c61c55" + }, + { + "path": "docs/decisions/issue-258-asr-511-layered-validation-profiles-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "4f8e1f0c432057ebb29f642b529ec4a70f7d5b2fe55e7b2400559c9ecfa0d2df" + }, + { + "path": "docs/decisions/issue-259-asr-515-validation-strength-disclosure-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "7c7b0a56ee16964e5f9d6b293ee3ce4c85e541be7130193558768e3ddb55c043" + }, + { + "path": "docs/decisions/issue-274-exp-718-controlled-randomness-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "8c2b070968a8715312134c6677103f6f39330ad95440e1077aba9125cc4dc212" + }, + { + "path": "docs/decisions/issue-294-sem-219-participant-tool-affordance-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "a0401e55f93f741b3aca0941f5abe9e240bf7d35a16e5a41f8c693f5a5608908" + }, + { + "path": "docs/decisions/issue-296-sem-226-participant-exposure-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 12, + "content_sha256": "4c6028cadabe1d80e8047fbf4560b5308a265affbe1a93ae06fcf5fc8fc0722a" + }, + { + "path": "docs/decisions/issue-303-act-622-participant-action-space-modes-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "33f81a1f9e2495698f0b93278ff9906dbaa4a3a2502ecb2a37e618f660babde5" + }, + { + "path": "docs/decisions/issue-334-sem-224-observability-plane-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "075d12d80888102d84b71fcead72a17dad79ce56b62a9951911a083035417e64" + }, + { + "path": "docs/decisions/issue-336-dsl-123-scenario-native-observability-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "436c7141fc93c94dcc0c720dd6b2a6a7a2ba2ae8388f3331bb9875d142f90a0f" + }, + { + "path": "docs/decisions/issue-337-dsl-124-authored-evidence-requirements-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "76268dd79c36e803bbd7b35f00cc528c50aa684c90427c257425d9903a65ed58" + }, + { + "path": "docs/decisions/issue-338-run-316-operational-apparatus-observability-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "5daff621cfc4da4feedf60dc8dc29e014db4d2a6b32f4588c5eb069732031eab" + }, + { + "path": "docs/decisions/issue-339-api-419-observation-augmentation-disclosure-contracts-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "5ad2f84b7e329fb0db7c709ef4a1707211818214b447f5d885e029fe2be83962" + }, + { + "path": "docs/decisions/issue-34-osv-scanner-ci-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "c4e473b9db2f73b7c5ad2321073ca769f24e5bdc2757b399e5152ff012c09c12" + }, + { + "path": "docs/decisions/issue-340-asr-525-observability-conformance-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "7fc4498d20cc440e1c2241dec817c74ee14ac548ae61f9c6fd594f0565972151" + }, + { + "path": "docs/decisions/issue-346-dsl-language-evaluation-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "91a3aec493edda6e88db68e5023f26a38d8bbfcb8061dc22869bd9c3953ceb68" + }, + { + "path": "docs/decisions/issue-39-processor-models-package-split-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "6eced1b002523de4922ce6fcd3c4f55accb3c542643c203c00a1df4d612b9717" + }, + { + "path": "docs/decisions/issue-41-compiler-package-split-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 21, + "content_sha256": "468775c090ddd4bbd69deded9a701479132f364e61b8ad629d1d467e0b7765c5" + }, + { + "path": "docs/decisions/issue-417-runtime-contract-observation-boundary-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "1102bf844159a14400ed76bf9162821d30fceffd673d9cf905c9843074fdeddc" + }, + { + "path": "docs/decisions/issue-42-validator-package-split-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 26, + "content_sha256": "58087b00eee3459115fa4cca8af4b9a481d741023f08079fa80db307a946dbde" + }, + { + "path": "docs/decisions/issue-43-contracts-package-split-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 18, + "content_sha256": "1aa9a0cb76c20d1c4e304af29123c8539ab5d256998535a63f1be96ba1329f11" + }, + { + "path": "docs/decisions/issue-44-planner-package-split-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 29, + "content_sha256": "beaf758a241f56d1b47077f474452b089d83fbe8b8a87aace523c5323bd100bd" + }, + { + "path": "docs/decisions/issue-491-sem-218-runtime-realization-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "23e9307a1ab2f5fe3ec4398b71b39ea89ff7f1a9912d585c9971ece5e86977d0" + }, + { + "path": "docs/decisions/issue-495-uco-alignment-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 12, + "content_sha256": "eda343bacdd4e2738d33bda423d88b56cbf1a6a2450d89509cd5f728116cf9a9" + }, + { + "path": "docs/decisions/issue-496-catalog-governance-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "f0c5a1017da60db2ec19db51f9d425696d1426b522f192d6382046b2d2d1e634" + }, + { + "path": "docs/decisions/issue-499-schema-authority-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "ff4616cf84adb02745a1ac31530cfaee55837bad88c77c14ab1b219b6b666497" + }, + { + "path": "docs/decisions/issue-501-validation-corpus-schema-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "80ae42e9703cdf9af0b9172a8ef1bbcc048088c58ec8d4b9aed4277c4ca9ce07" + }, + { + "path": "docs/decisions/issue-505-error-advisory-boundary-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "68bb2e44b916a04368757a0a4b6201712283b378b7865aa13313457a404d3dcf" + }, + { + "path": "docs/decisions/issue-506-determinism-witness-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "df1282deff14e620e0ff0dd759ec7c5ec4c823f7fb453918392d4c1c4af7373d" + }, + { + "path": "docs/decisions/issue-508-related-work-comparison-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "b1b27491d126a4c241c37a137cad42bceb8484e4ef168fdd488e229fd72874e0" + }, + { + "path": "docs/decisions/issue-537-contract-corpus-packaging-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "eebc85db5ec63488d1c240fa4ffec112cf8f1dc77b4b15f44f86958abe889fd3" + }, + { + "path": "docs/decisions/issue-539-realization-posture-cascade-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "22d71381e6be0df280ef9ab641fc54dad7a8df20aad16a2417e0bf5b4c1f57b7" + }, + { + "path": "docs/decisions/issue-551-import-lockfile-portability-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "745fa02f0be6ca03cfd8e1174b57ed7befd5641f635c4b8c3f02b3b81c7ee169" + }, + { + "path": "docs/decisions/issue-567-pr-title-guard-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "e3e79f5714985c8c6a06fe6c74720491c9d567813a22ea4855dfa52ef71714b6" + }, + { + "path": "docs/decisions/issue-598-paper-reference-scenario-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "900446d4d84b274108e7470546b0e44a5cb12f0b112c423ea5841da164ae33e6" + }, + { + "path": "docs/decisions/issue-599-participant-implementation-binding-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "4f620b1677be3f8b7091be66e22611ca5e51d0a6c28764ba670ddfd21f7e5746" + }, + { + "path": "docs/decisions/issue-600-paper-demonstration-corpus-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 15, + "content_sha256": "4c1b4efef3ab8dbd125c68ac65fe6571855e4a4d8a1a30601358793bb1035afe" + }, + { + "path": "docs/decisions/issue-601-libvirt-provisioning-backend-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 17, + "content_sha256": "9168fc762bd6c31f03af1608b738e3e7fd453028ca1eb2b9419953f8e4911648" + }, + { + "path": "docs/decisions/issue-601-techvault-live-verification.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "9227381c5d8570266ad8ca076b2d75d727b02dac92c93b2ea3e46645a57e0df4" + }, + { + "path": "docs/decisions/issue-602-libvirt-backend-manifest-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 14, + "content_sha256": "8f6bfd7c6da0c850a0dac220dabe29507314b62ea9127632428ea03487e0c466" + }, + { + "path": "docs/decisions/issue-603-libvirt-apply-realization-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 18, + "content_sha256": "a0dfe5af5595c619f7e03e1e6beca1305e116a220d62feb4fbab71876d9bbcd9" + }, + { + "path": "docs/decisions/issue-604-libvirt-reconciliation-teardown-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 14, + "content_sha256": "e95ccfe2a1d00c40cb549cd7df561be8dae470649192c5b3c19d7b3379aac2c9" + }, + { + "path": "docs/decisions/issue-605-libvirt-envelope-diagnostics-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "9cad36f4e3f5987b77ed0d6d35d7460b5c755683c9bec803c93a7e607ee9e151" + }, + { + "path": "docs/decisions/issue-606-libvirt-conformance-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "542db8ecf0ce279a97af53768f560480c08ced133a97bc45abc60cfa1123672d" + }, + { + "path": "docs/decisions/issue-609-plan-inspection-cli-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 22, + "content_sha256": "8990305b8eaeef7f93680edfe21ff4d5a028d930f7fb994894e238ed08d19111" + }, + { + "path": "docs/decisions/issue-614-libvirt-participant-runtime.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 16, + "content_sha256": "a20c03726ed81eb1206df8835cb9208d2b3788d69aba1bce5194412a42c85aaa" + }, + { + "path": "docs/decisions/issue-615-libvirt-paper-evidence-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "c17b4cc798ed6a90d71096306f89eb8753b8e5346c14b60d377956d1043a0c3b" + }, + { + "path": "docs/decisions/issue-635-rep-001-cage-2-replication-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 39, + "content_sha256": "9b6e5521142e23a867204a40bdbf89cc1936e9ca90315e9e85c7eaf118fa44e0" + }, + { + "path": "docs/decisions/issue-652-sce-002-variation-trial-realization-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 13, + "content_sha256": "42ccfba102ee5d15ec0f56c29292aa89f22df2f4f2b7dccbc1c40c2ebc842e7e" + }, + { + "path": "docs/decisions/issue-653-sce-004-goal-oriented-tool-flexible-steps-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "010f65d39c7359bd11b60d3b1423a172a5c2e302b7741c46e5303da5b0a4a06e" + }, + { + "path": "docs/decisions/issue-655-dsl-variable-system-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "d2f77754fd659e5d121fac63424e9f92ba885237f9f232179bf603161521d35b" + }, + { + "path": "docs/decisions/issue-658-sce-007-clean-state-cleanup-contracts-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "ee330ef1c46781a89df3bb8ea5a14d706253a9df36b03244fd838aca9ad581ab" + }, + { + "path": "docs/decisions/issue-663-target-conformance-provisioning-scope-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "9be7a123ad3ad3d0ee1e8ec433268d878865e6e9ca4b952de5430255f7acab28" + }, + { + "path": "docs/decisions/issue-667-realization-envelope-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "b77946d65d53b84c49e99f53c344b4aac9b244fd6f78a6717fb4b2be64c9b70b" + }, + { + "path": "docs/decisions/issue-668-envelope-relation-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "30344208e771a3700524a6499cab8ec706b662059b11640ec42af9b31c7957b3" + }, + { + "path": "docs/decisions/issue-670-rename-paper-identifiers.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "6427656cfde11ba275565199f468b339e24ef66e7eae727e5f5892b97297673c" + }, + { + "path": "docs/decisions/issue-714-asr-519-techvault-realization-disclosure-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "28cb8fb672781a70355aa05c87130f3cdd7e17c5502d84f28af3bdc4ab1f766c" + }, + { + "path": "docs/decisions/issue-715-asr-519-guest-observed-libvirt-probes-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "60e0c789276d7a183fb658a99150e2ec5e8e80b84a6894323bc555b547d1a572" + }, + { + "path": "docs/decisions/issue-716-asr-519-realization-honesty-conformance-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 25, + "content_sha256": "20337c233c089e6b69d76cdb4c7a06817740d219a794857a6992acda5ab372cc" + }, + { + "path": "docs/decisions/issue-721-dsl-105-canonical-sdl-yaml-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "387de231efc1079ec4654ff6db2bba5de238ac7b071bd97a9e4bfd716c724b8e" + }, + { + "path": "docs/decisions/issue-722-normative-sdl-catalog-parity-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "07a91eebc75939f4b7688f2366411bb6f0297c5a142b59bc75a1a4a3690cc631" + }, + { + "path": "docs/decisions/issue-724-instantiated-phase-contract-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "a317cb2f9998ec0e0cb951f834d94fc63bd3f65ca5dcdd2320861c2b28e59211" + }, + { + "path": "docs/decisions/issue-725-objective-truth-semantics-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "e79879d29133add7de5ea576565648bb25727124d4f7d9d1249b54913865bae2" + }, + { + "path": "docs/decisions/issue-726-sdl-lineage-provenance-ledger-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "641aa444ca460885fc596d7339764453267b59afe641224cedd0ec9b91e86245" + }, + { + "path": "docs/decisions/issue-727-scientific-scenario-completeness-profile-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "bf9a2d52eb83329b4e1e303ea73a328c635be324faeebc01499714d1fdd4b2f8" + }, + { + "path": "docs/decisions/issue-728-reproducible-related-work-comparison-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 16, + "content_sha256": "caa28e274d5ab9088bd5319dd9bc131163b3119c0308b381d11949273590914f" + }, + { + "path": "docs/decisions/issue-73-dsl-115-specificity-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "8ae71cc610506394ef71b76c2919c78ecffbc8523c0b751557f22a8ef53fc845" + }, + { + "path": "docs/decisions/issue-738-associated-artifact-manifests-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 21, + "content_sha256": "41a65ceff5f9f5946e9cd4e9fe2ee81c74d2ee5b5bf2b9ae454b12cfa704eedf" + }, + { + "path": "docs/decisions/issue-747-behavioral-relation-taxonomy-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 18, + "content_sha256": "fdaeff16f9063c582293c536d17da3a707c825d1ad1c269f3aff1452c642f49d" + }, + { + "path": "docs/decisions/issue-748-node-services-reachability-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "1bb1a06b074ca51bec8a25a4238c9fbc400727430501583a22590feee5319634" + }, + { + "path": "docs/decisions/issue-760-sem-218-provenance-preservation-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "41e53f9fc2dcd8e0c5b158e75e0d26223a147497d451f224551add7eb441e9ee" + }, + { + "path": "docs/decisions/issue-763-authored-domain-topology-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "e1302eea9496ddafa245fefe4dda6e1da2658c3b64faa418061accdad181ba52" + }, + { + "path": "docs/decisions/issue-767-explicitness-provenance-identity-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "058545fc879bb5cbe082d804c5ca3546688a803275a1ccffab4955c7238d5329" + }, + { + "path": "docs/decisions/issue-776-domain-topology-realizer-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "bf77f1ddbe95e24e2dfa718e2158055a8527a951b693d6486920f6849b72521e" + }, + { + "path": "docs/decisions/issue-780-dsl-435-stateful-realization-resources-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "2d852a319b6c2d8f87c40d97563f939893d1242cc71c20e39c48fc90cb5c73fa" + }, + { + "path": "docs/decisions/issue-791-sce-004-runtime-fact-bindings-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "c3170f1d162f4e5a10cf8c6b7b0c9688b1c26087044863ed46c742b84eeb784c" + }, + { + "path": "docs/decisions/issue-794-participant-io-control-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "d3648deb13319cd9129dc1bd2f8ce53bdbf5360af6e61fb4a0d9208e01b84e68" + }, + { + "path": "docs/decisions/issue-796-sem-230-participant-information-flow-control-semantics-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "1019ddad07c6a9a7a0148e93d37773ebe61e83a97dc538d96e199800bbeda684" + }, + { + "path": "docs/decisions/issue-797-dsl-142-participant-inject-delivery-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "1f5c23c34f2fb2c07ff5d72cc47f9c7b3091a945d39da1df15d3dfe7ef470794" + }, + { + "path": "docs/decisions/issue-798-api-423-participant-crossing-contracts-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "7e239530e04aa560e654714a7ace8bd58173d2c3f115aeb39c936bb505735bac" + }, + { + "path": "docs/decisions/issue-805-participant-interactive-access-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "c5e5931ccf2e63e4e475b221ed1c1eb6201dc9e00a9092da1a62a1260c407681" + }, + { + "path": "docs/decisions/issue-826-whole-scenario-satisfiability-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 20, + "content_sha256": "3824915cccb24d930827cc94db285125fdd247c31d14e102ad7f0be89475d5e1" + }, + { + "path": "docs/decisions/issue-827-typed-exploit-path-semantics-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 13, + "content_sha256": "932ca609f9b1f352ca222a1cee6dcdf02476389c2762f36e0bc4c3485a173727" + }, + { + "path": "docs/decisions/issue-845-domain-controller-placement-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "13d81f20bc8bd41567a33945dd60f17187302cc015953a044119ce1ba176b31c" + }, + { + "path": "docs/decisions/issue-849-network-namespace-sharing-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "2774b6ac47776d224e7da64a7b68792713c59cdf840a7028a61455164f26dc02" + }, + { + "path": "docs/decisions/issue-857-dsl-143-enterprise-identity-deployment-tenancy-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "426d136b3512e3f748c18498eb245b52aa2ddb12524b5eb82ac5ed22bb91ac75" + }, + { + "path": "docs/decisions/issue-859-dsl-436-initial-service-state-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "b83b0e2caeb66582831e4dca0ea5d5930d5fb00d8ad085da78f9a752ee9c7bda" + }, + { + "path": "docs/decisions/issue-861-dsl-437-benign-participant-execution-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "3b4759eb34f176640c733f88787034eb39119207a088cba7bb705cf9611bc425" + }, + { + "path": "docs/decisions/issue-866-raes-rename-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 35, + "content_sha256": "e31f6abf62b78567a9037c0589af0c036ba27f80dbc090d91a9703b941ade77e" + }, + { + "path": "docs/decisions/issue-884-raes-python-import-hard-cut-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 13, + "content_sha256": "69c5c0a8dd29d9b68d18f33f673ea1c595a9bc163f312c8cca8a3dd4b5423a3a" + }, + { + "path": "docs/decisions/issue-884-reframe-raes-documentation-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "11f1c861de6d908b1a3d4a3c3eb07de757aaabbef2996ab414a880e3e2d44a31" + }, + { + "path": "docs/decisions/issue-90-gov-901-903-versioning-governance-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 17, + "content_sha256": "6add3268c3f0f3a96a76ce5e2e7dc74ad17b69d131dfe44282ee043fe7235071" + }, + { + "path": "docs/decisions/issue-903-experiment-binding-contracts-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "3128021de32125f2d8ce6270e1386c2c8cc5dd3af45514fb8e335a17c9469940" + }, + { + "path": "docs/decisions/issue-97-asr-511-515-validation-strength-disclosure-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "fe7a6c46f384539709ccb2795781d166164f256247497e06ffa9acfde47fa8d8" + }, + { + "path": "docs/decisions/sem-213-temporal-participant-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves a completed, dated design or architecture-preflight record from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "05258d1baff5dcf8fcdf4a29a7a4e41647fa00cc1f1dcd3df4af160c156c9a28" + }, + { + "path": "docs/research/behavioral-relations/conflation-audit-2026-07-13.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "503f5f52fd7cc7c13aa9c5dfdc9d650660225601e94c4b5efe2ffa6f38d75628" + }, + { + "path": "docs/research/dsl-language-evaluation/analysis-accessibility-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "1e076dbff24026d2717cfaec7934c1309f525c461ed6a102d1f48dfae5ff61c6" + }, + { + "path": "docs/research/dsl-language-evaluation/analysis-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "45976ab8a64721f8211b8db3073046d6740adebf102d2c23da11f4ea529de6a2" + }, + { + "path": "docs/research/dsl-language-evaluation/bundle-manifest.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 9, + "content_sha256": "bc87bcf7b873d2b1a5b62b78a216843c7aa505333c03157da0780706c49b4285" + }, + { + "path": "docs/research/dsl-language-evaluation/execution-snapshot-accessibility-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 23, + "content_sha256": "21a0f4d290b9237270ffe4a12c542329bed01025d483dc708074c9256a0bb52a" + }, + { + "path": "docs/research/dsl-language-evaluation/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": 15, + "content_sha256": "498c3172bbc0711271023d243755113ccfed67b85a662e0deb1c917ac0546ad4" + }, + { + "path": "docs/research/dsl-language-evaluation/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "e838b5efcb28f38b8e0a7e1b1485121c9e3b8f514a896becee449cc98c3a0bc1" + }, + { + "path": "docs/research/dsl-language-evaluation/protocol-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 27, + "content_sha256": "af3252212815185ee4a9d4001875be30152d11c4b46dac3e7ed86109ccd94b7f" + }, + { + "path": "docs/research/dsl-language-evaluation/protocol-v2.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 48, + "content_sha256": "bcbdcc2e45c5e4c056424e1568417e55f4dbacf2ffab41520c91d2d7dcb3a6bc" + }, + { + "path": "docs/research/experiment-core/2026-05-26-search-log.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "a890796bd594d97b9e7237c015d0e620f40e95bc3b4aaf5d8818dc8e4a38b521" + }, + { + "path": "docs/research/experiment-core/cyber-range-scientific-instrument.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "357c306608e023c20692d8c2edd459e0f692b62508dc4caf862670e58c459534" + }, + { + "path": "docs/research/experiment-core/design-criteria-for-exp-701-705.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 8, + "content_sha256": "b9302b484567b73f6293cba4d4fb472c690072f58a514c8fceffc98205a315f8" + }, + { + "path": "docs/research/experiment-core/issue-105-exp-706-712-reproducibility-replay-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "0f22063bec38ba96edadc52182ee8d3238f62003c9b23fb547537fb6e401ba9d" + }, + { + "path": "docs/research/experiment-core/issue-233-exp-707-capture-spec-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "2f9a2a71a682576a9007c09b2d008aa999d32718a3e8e32c3d98a3c80600ac8b" + }, + { + "path": "docs/research/experiment-core/issue-234-exp-708-evidence-record-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "76272f80477ef1111c2554ab6fc32a60aaa3fa9a5015fd827ce4ad27e21b84f7" + }, + { + "path": "docs/research/experiment-core/issue-235-exp-709-derived-measure-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "a9366d93a0aec1949801667c4548d0ae5488e433c295dc86ff5aa24ae493a4f9" + }, + { + "path": "docs/research/experiment-core/issue-238-exp-720-run-provenance-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "3a392ed2b29207e813c6ddf4ae745cddfde03d838ecac40a5029a3770a76d53b" + }, + { + "path": "docs/research/experiment-core/issue-239-exp-722-realized-form-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "565592e9435f8ff4f9d0772d55ded1d819fd950edc81f1179085a945d7338aa3" + }, + { + "path": "docs/research/experiment-core/issue-267-exp-706-trial-replication-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "969005f1323f501db4d8051bcd41cd45d21b6d5d4bfa06cb3421ddf2e55fe61a" + }, + { + "path": "docs/research/experiment-core/issue-342-exp-732-evidence-source-augmentation-provenance-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "ca4183410de43e5fee01ce4ec730cdf3cfa86dba918a9c46d3224ab212785819" + }, + { + "path": "docs/research/experiment-core/issue-88-evidence-measure-preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "b056dce5dd81c255dabdd4541a96272fcfb53bf888cc10f3dcb68274aec28a9f" + }, + { + "path": "docs/research/experiment-core/ml-experiment-rigor.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "2e2062d354968fdb3355c163960be0b15baefbb4663b00b7d8ce6ec7fb8baeb2" + }, + { + "path": "docs/research/experiment-core/preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "26df33cf24fbc9e7ca90be2c94d827e79cadf528b4b7dcb28bd63bd8c687340a" + }, + { + "path": "docs/research/experiment-core/provenance-and-data-format-supports.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "3275f81bd801a50ca2e944e9d5208ff8a49dfd361de71fc347f12806ae4e0cad" + }, + { + "path": "docs/research/experiment-core/traceability-matrix-exp-701-705.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "57191fc0a863a8c50752ed94487e53359a586890d49262c143c7ee1b9e939e34" + }, + { + "path": "docs/research/formal-semantic-validation/analysis-v1.1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "91c83666ee9099d8c8484a1ce6e3ce30f858d050a066cfb2bb342375fcbb6642" + }, + { + "path": "docs/research/formal-semantic-validation/analysis-v1.2.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "dd2966c96e2daf0b97107e90557438f89b150d1976297d6a0f4216a28f2336d8" + }, + { + "path": "docs/research/formal-semantic-validation/analysis-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "0930f0369cdc574a4aba64559727c7a4ffb6513ce06136ae683d765bc71b93fa" + }, + { + "path": "docs/research/formal-semantic-validation/corpus/manifest-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "89019de409f23f20f6136e305da0cef618ec3add6a3372628fe610008ead9b31" + }, + { + "path": "docs/research/formal-semantic-validation/corpus/semantic-valid.sdl.yaml", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "a074d75b1b420a47a740703deaff45c20ec1c5d846f660412929bc69ab0efb19" + }, + { + "path": "docs/research/formal-semantic-validation/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": 19, + "content_sha256": "815ad32527478e1e9056c8e64419f83b0086ab1509c7df21cf5863b2d565cd46" + }, + { + "path": "docs/research/formal-semantic-validation/execution-snapshot-v1.2.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 19, + "content_sha256": "2a411d210a4d24916290b2e61a7b96acf36b63e648fa8188186d2ce7822c4c10" + }, + { + "path": "docs/research/formal-semantic-validation/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": 19, + "content_sha256": "e061530e05ed00a2ae68ce3c5760c1011223e48e7c2e8a1b8e2068718674e2bc" + }, + { + "path": "docs/research/formal-semantic-validation/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "3bcb125e5fac91f3827edfbf139ae1f53dcf21c686ea0a6ee790241853850369" + }, + { + "path": "docs/research/formal-semantic-validation/protocol-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "cda5d300dfb9ec553cbc86538eaaf4a8ef43dfa59a14c1dcc113a56cfac8d826" + }, + { + "path": "docs/research/formal-semantic-validation/satisfiability-analysis-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "194399bf85c05b2a6cd9aacc1590486f9ca051d2fa08b6b5bb56ddc85c8b33cf" + }, + { + "path": "docs/research/formal-semantic-validation/satisfiability-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": 8, + "content_sha256": "330c9aa41d3d7c388d82e5639bbeebfa98cb11156bea61ad8970297560d2e373" + }, + { + "path": "docs/research/initial-service-state-precedents-2026-07-24.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "3934b85e585250be7e5e6eafc7199b3e9c567ef858a68bba32c85479df9d13c5" + }, + { + "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": 23, + "content_sha256": "dd3e5fd6835192dbe142fc456f18203ebc07fe9ab8e5be0ad0586562cc641253" + }, + { + "path": "docs/research/participant-backend-contracts/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "a0c2c3b1f2980a53a607127c5ccdd8093e0074f1ba53bfeeacaa95a1ad02c72b" + }, + { + "path": "docs/research/participant-backend-contracts/preflight-guardrails.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "3c8151865af05542620256c34d28bb353c71b8c45a5713235689cec2cea53dc9" + }, + { + "path": "docs/research/participant-backend-contracts/prior-art-and-design-criteria.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "4ded03f3a8a26687e4043b1a6ebb9947e19d3611af5f8a369965b1a82501e3d4" + }, + { + "path": "docs/research/participant-interactive-access/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "299321015a07c9ad703f7ec5c023cf08787b72cf92091e149a2f08ee4a3fcf9a" + }, + { + "path": "docs/research/participant-io-control/adoption-design.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 2, + "content_sha256": "602649f2083f1337422d483c016ad2c59067751e56123f216dce98893620f191" + }, + { + "path": "docs/research/participant-io-control/adoption-program.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "fff200b23d171334e31ce9da00e921be7ad868b74bf5b4ba21c158277b5db56b" + }, + { + "path": "docs/research/participant-io-control/current-state-assessment.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 10, + "content_sha256": "53c30870282bd09b493abddf2f96d966137be04f8af880d1965ad90d655a048e" + }, + { + "path": "docs/research/participant-io-control/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "d12d66ff4b97231ac253700baa3e4b8754906e07cc0621e1c96d2e1987069b08" + }, + { + "path": "docs/research/participant-io-control/requirement-disposition.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "b6557a24280525c14a55a209e6c530e8272f255fd6c3e4155a4c37f29dd17d76" + }, + { + "path": "docs/research/primary/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "15cea19fee65485e2b03091c53620f676010186d8134361db39765b4f13fbeca" + }, + { + "path": "docs/research/primary/literature/cyber-range-scenario-survey.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "a00392d29fd7bad14d77109a0cb26adcc8c371eb839a65715e3922cbdd426e6d" + }, + { + "path": "docs/research/realization-envelope/prior-art-and-design-criteria.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 13, + "content_sha256": "b57c40323557bf759479176faecec6e4bb7bda074195df27a5feaf91922e87a9" + }, + { + "path": "docs/research/related-work-comparison/analysis-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 32, + "content_sha256": "88c5821702918bd43e0f4766a871d3215fdcc90a7c43a3df336278feaf8809d3" + }, + { + "path": "docs/research/related-work-comparison/extraction-snapshot-2026-07-13.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 86, + "content_sha256": "8c1cb84c36d05e47bf73995400c53018e3759178a431213328cebc306f2dc281" + }, + { + "path": "docs/research/related-work-comparison/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 5, + "content_sha256": "1c394b7bfc8d1b91cd8637f7b0c8f2f2ba90a0caa0e15da61ec09785e4bf041f" + }, + { + "path": "docs/research/related-work-comparison/protocol-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "75ad031c493aa7637daa8304291b7c4e8353c208a22fa257997df77438471a1d" + }, + { + "path": "docs/research/related-work-comparison/search-log.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 6, + "content_sha256": "6497be8d5cf8acbf7b76e3534823414e3a955e724fbb5032558a43c951c6a776" + }, + { + "path": "docs/research/scenario-variation-trial-realization/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "c4157960afaa820ef61f5e3005349d77a0048ff934a20bddecb6053a1b4cb50f" + }, + { + "path": "docs/research/scenario-variation-trial-realization/prior-art-and-design-criteria.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 19, + "content_sha256": "bddccb5c1651b76552601e6c22ebf6d2de118afed85685896a881b0389eff304" + }, + { + "path": "docs/research/scoring-scope/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 1, + "content_sha256": "1b6c07fd7c4594fdee584277166a0e63f878b3c1b5aef9c7e98abc89ffdbdbcb" + }, + { + "path": "docs/research/scoring-scope/prior-art-and-design-criteria.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 7, + "content_sha256": "df17af636d5f8c798c5df346b61adb92dc645f5635e56f9339a34381149de2c6" + }, + { + "path": "docs/research/scoring-scope/scoring-surface-inventory.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "1581f185528d6c9b995207120007c687d0f9ea92c9ca0f923fa12325cf8641d5" + }, + { + "path": "docs/research/specification-coverage/analysis-v1.1.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": "c3cd8dd7faeb7d7ea69164e6fb9e9069b1e7c3cc3e28989b70a9a1fe56b86114" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "path": "docs/research/specification-coverage/index.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 3, + "content_sha256": "d66d381d589ab3230d164d8e9a6b8b455fe56fb54ce605eb158360e27ec04264" + }, + { + "path": "docs/research/specification-coverage/protocol-v1.json", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 12, + "content_sha256": "e97a19e643e94c9e589dca823a63c6ce49d3329fe2a3cb888ab630838ed93125" + }, + { + "path": "docs/research/time-model/prior-art-and-design-criteria.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 11, + "content_sha256": "403db824d4359a88d1f19989fcc15a489fbcdf5d1e6d49572710884f721eb349" + }, + { + "path": "docs/research/validation-admission-profiles/traceability-matrix-asr-511-515.md", + "record_class": "research-record", + "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", + "occurrences": 4, + "content_sha256": "778711fccb5b5912a6fd106d080b4eac9dd354e4ccd4574204274fea653fecbc" + } + ] +} diff --git a/tools/policy/repo_policy.py b/tools/policy/repo_policy.py index cd6895a42..7ef5cc804 100644 --- a/tools/policy/repo_policy.py +++ b/tools/policy/repo_policy.py @@ -98,8 +98,6 @@ def structural_runner(input_document: dict) -> list[PolicyFailure]: } ) ) - failures.extend(_check_package_import_direction(repo_root, policy, changed)) - failures.extend(_check_retired_namespace(repo_root, policy, changed)) failures.extend(_check_layering_and_oversized(repo_root, policy, changed)) failures.extend(_check_module_boundaries(repo_root, policy, changed, check_set=check_set)) @@ -528,55 +526,6 @@ def _check_drain(allowlist: frozenset[str], allowlist_path: str) -> list[PolicyF ] -def _check_package_import_direction(repo_root: Path, policy: dict, changed: list[str]) -> list[PolicyFailure]: - failures: list[PolicyFailure] = [] - package_root = repo_root / policy["retired_namespace"]["owning_root"] - prefixes = tuple(policy["retired_namespace"]["forbidden_import_prefixes"]) - for rel_path in changed: - if not rel_path.endswith(".py") or not path_matches_prefix( - rel_path, package_root.relative_to(repo_root).as_posix() - ): - continue - path = repo_root / rel_path - tree = ast.parse(path.read_text(encoding="utf-8"), filename=rel_path) - for node in ast.walk(tree): - if isinstance(node, ast.Import): - for alias in node.names: - if alias.name.startswith(prefixes): - failures.append( - PolicyFailure( - "retired-namespace-import", - "owning packages must not import retired aces or aces_* modules", - rel_path, - ) - ) - elif isinstance(node, ast.ImportFrom) and node.module and node.module.startswith(prefixes): - failures.append( - PolicyFailure( - "retired-namespace-import", - "owning packages must not import retired aces or aces_* modules", - rel_path, - ) - ) - return failures - - -def _check_retired_namespace(repo_root: Path, policy: dict, changed: list[str]) -> list[PolicyFailure]: - failures: list[PolicyFailure] = [] - retired_root = policy["retired_namespace"]["root"] - for rel_path in changed: - if not path_matches_prefix(rel_path, retired_root) or not (repo_root / rel_path).exists(): - continue - failures.append( - PolicyFailure( - "retired-namespace-path", - "the retired implementations/python/src/aces namespace must not be reintroduced", - rel_path, - ) - ) - return failures - - def _validate_module_boundary_config(repo_root: Path, config: object) -> tuple[list[dict], list[PolicyFailure]]: """Validate the ADR-036 module-boundary policy block.""" diff --git a/tools/policy/requirement_governance.py b/tools/policy/requirement_governance.py index 00e9977b6..a2ac453a5 100644 --- a/tools/policy/requirement_governance.py +++ b/tools/policy/requirement_governance.py @@ -222,4 +222,4 @@ def _check_traceability( def requirement_uid_from_context(branch_name: str | None, explicit_uid: str | None) -> str | None: - return explicit_uid or os.environ.get("ACES_REQUIREMENT_UID") or detect_requirement_uid(branch_name) + return explicit_uid or os.environ.get("RAES_REQUIREMENT_UID") or detect_requirement_uid(branch_name) diff --git a/tools/policy/requirement_order.yaml b/tools/policy/requirement_order.yaml index 00197da5f..e94cfcf13 100644 --- a/tools/policy/requirement_order.yaml +++ b/tools/policy/requirement_order.yaml @@ -1,4 +1,4 @@ -project: aces-sdl +project: raes-sdl phases: - id: gov-concept-authority diff --git a/tools/real-daemon/README.md b/tools/real-daemon/README.md index 17fc591a1..fc244f14c 100644 --- a/tools/real-daemon/README.md +++ b/tools/real-daemon/README.md @@ -77,7 +77,7 @@ raes libvirt techvault guest-certify \ --project-dir . --run-id guest-proof-1 --yes ``` -It emits the `aces.libvirt.scenario-evidence-run/v1` artifact under +It emits the `raes.libvirt.scenario-evidence-run/v1` artifact under `runs//scenario-evidence/libvirt-scenario-evidence-run.json`. The artifact is validated (source separation, binding, redaction) **before** it is written, so it contains no host paths, connection URIs, raw domain UUIDs, XML, or secrets; the @@ -86,7 +86,7 @@ challenge, the selected envelope/configuration + appliance digests, and a `sha256:` native correlation. The equivalent gate also runs as an opt-in pytest: ```sh -ACES_REAL_LIBVIRT_URI=qemu:///system \ +RAES_REAL_LIBVIRT_URI=qemu:///system \ uv run pytest -m integration \ implementations/python/tests/test_libvirt_backend_guest_certified_real_libvirt.py ``` diff --git a/tools/real-daemon/evidence/guest-certified-asr519-20260712T031842Z.json b/tools/real-daemon/evidence/guest-certified-asr519-20260712T031842Z.json index c99d82e2a..5d683f290 100644 --- a/tools/real-daemon/evidence/guest-certified-asr519-20260712T031842Z.json +++ b/tools/real-daemon/evidence/guest-certified-asr519-20260712T031842Z.json @@ -95,7 +95,7 @@ }, "compatibility": { "processors": [ - "aces-reference-processor" + "raes-reference-processor" ] }, "concept_bindings": [ @@ -187,7 +187,7 @@ "participant_behaviors": [] }, "compiled_model_fingerprint": "sha256:b0ce6d650a49f579592b7746e84ab43e73ca3d31a7b3e2dd2a6620eb2fcee654", - "processor": "aces-reference-processor" + "processor": "raes-reference-processor" }, "defensive_evidence": { "captured_at": "2026-07-12T03:20:52.516205+00:00", @@ -246,7 +246,7 @@ "negative_boundary_checks", "evaluator_outcome" ], - "note": "Stable ACES addresses and evidence refs for the Brad-Edwards/aces#600 cross-backend invariant ledger; no libvirt domain UUIDs, host paths, or APTL-private identifiers.", + "note": "Stable RAES addresses and evidence refs for the Brad-Edwards/raes#600 cross-backend invariant ledger; no libvirt domain UUIDs, host paths, or APTL-private identifiers.", "observation_boundaries": [], "participant_behaviors": [], "scenario_content_sha256": "sha256:54cfbf2301510771eff148748e60dc0917efbbf695270d9a5b837ee80bcbb454", @@ -268,7 +268,7 @@ "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 Brad-Edwards/aces#600." + "No full semantic-equivalence claim beyond the invariant ledger in Brad-Edwards/raes#600." ], "participant_action_proof": { "admitted_action_addresses": [], @@ -308,7 +308,7 @@ "architecture": "x86_64", "image_policy": "generated-initramfs-appliance", "memory_mib": 128, - "name": "aces-evidence-guest-vm", + "name": "raes-evidence-guest-vm", "network_attachments": [ "provision.network.guest-net" ], @@ -323,7 +323,7 @@ "forward_mode": "none", "gateway": "192.0.2.1", "internal": true, - "name": "aces-evidence-guest-net", + "name": "raes-evidence-guest-net", "observation_source": "daemon-observed" } ], @@ -345,7 +345,7 @@ "address": "provision.node.guest-vm", "architecture": "x86_64", "content": [ - "/etc/aces/marker|aabb38db6cd478c6dcbf41b2973d99a8e72cafe9a026df5808d5015452b38a55|644" + "/etc/raes/marker|aabb38db6cd478c6dcbf41b2973d99a8e72cafe9a026df5808d5015452b38a55|644" ], "correlation": "sha256:e02c8435b23e310dbe541c17e94a776cf501b290b07e398dbb7a67c89c23d9bb", "memory_mib": 78, @@ -414,10 +414,10 @@ "disclosure": "Compiled topology remains planned; the native surface contains only independently daemon-observed substrate fields.", "native_surface": { "domains": [ - "aces-evidence-guest-vm" + "raes-evidence-guest-vm" ], "networks": [ - "aces-evidence-guest-net" + "raes-evidence-guest-net" ], "source": "daemon-observed", "substrate": "libvirt-qemu-initramfs" @@ -482,7 +482,7 @@ "relative_path": "examples/scenarios/techvault-guest-certified.sdl.yaml", "version": "*" }, - "schema": "aces.libvirt.scenario-evidence-run/v1", + "schema": "raes.libvirt.scenario-evidence-run/v1", "terminal_observation": { "behavior_history": {}, "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 the behavior-history equivalent.", diff --git a/tools/real-daemon/libvirt_smoke.py b/tools/real-daemon/libvirt_smoke.py index 58bc4c048..5751eb247 100644 --- a/tools/real-daemon/libvirt_smoke.py +++ b/tools/real-daemon/libvirt_smoke.py @@ -33,7 +33,7 @@ from raes_contracts.runtime_state import RuntimeSnapshot URI = "qemu:///system" -PREFIX = "acestest" +PREFIX = "raestest" CIRROS = "/var/lib/libvirt/images/cirros.img" RESULTS: list[tuple[str, bool, str]] = [] @@ -84,14 +84,14 @@ def new_driver() -> LibvirtDeploymentDriver: def purge(): - """Best-effort removal of any leftover acestest-* / acesprov-* objects.""" + """Best-effort removal of any leftover raestest-* / raesprov-* objects.""" conn = raw() for obj in [*conn.listAllDomains(), *conn.listAllNetworks()]: try: nm = obj.name() except libvirt.libvirtError: continue - if not nm.startswith((PREFIX, "acesprov")): + if not nm.startswith((PREFIX, "raesprov")): continue with contextlib.suppress(libvirt.libvirtError): if obj.isActive(): @@ -103,7 +103,7 @@ def purge(): nm = nf.name() except libvirt.libvirtError: continue - if nm.startswith((PREFIX, "acesprov")): + if nm.startswith((PREFIX, "raesprov")): with contextlib.suppress(libvirt.libvirtError): nf.undefine() conn.close() @@ -118,12 +118,12 @@ def t_abi_absence_behavior(): conn = raw() try: try: - conn.lookupByName("acestest-nope-xyz") + conn.lookupByName("raestest-nope-xyz") raise AssertionError("expected libvirtError for missing domain") except libvirt.libvirtError as e: assert e.get_error_code() == 42, e.get_error_code() try: - conn.networkLookupByName("acestest-nope-xyz") + conn.networkLookupByName("raestest-nope-xyz") raise AssertionError("expected libvirtError for missing network") except libvirt.libvirtError as e: assert e.get_error_code() == 43, e.get_error_code() @@ -153,10 +153,10 @@ def t_create_network_and_domain(): assert d.realized_addresses() == {"provision.network.lan", "provision.node.web"} conn = raw() try: - assert net_exists(conn, "acestest-lan"), "network not defined" - assert conn.networkLookupByName("acestest-lan").isActive() == 1, "network not active" - assert dom_exists(conn, "acestest-web"), "domain not defined" - assert dom_state_running(conn, "acestest-web"), "domain not running" + assert net_exists(conn, "raestest-lan"), "network not defined" + assert conn.networkLookupByName("raestest-lan").isActive() == 1, "network not active" + assert dom_exists(conn, "raestest-web"), "domain not defined" + assert dom_state_running(conn, "raestest-web"), "domain not running" finally: conn.close() return "real network active + real domain running under QEMU" @@ -179,11 +179,11 @@ def t_update_reconverges_no_duplicate(): assert not r2.diagnostics, [x.code for x in r2.diagnostics] conn = raw() try: - doms = [x.name() for x in conn.listAllDomains() if x.name() == "acestest-web2"] - nets = [x.name() for x in conn.listAllNetworks() if x.name() == "acestest-lan2"] + doms = [x.name() for x in conn.listAllDomains() if x.name() == "raestest-web2"] + nets = [x.name() for x in conn.listAllNetworks() if x.name() == "raestest-lan2"] assert len(doms) == 1, f"duplicate domains: {doms}" assert len(nets) == 1, f"duplicate networks: {nets}" - assert dom_state_running(conn, "acestest-web2") + assert dom_state_running(conn, "raestest-web2") finally: conn.close() # teardown @@ -199,8 +199,8 @@ def t_teardown_removes_everything(): assert all(not h.realized for h in (*res.networks, *res.domains)) conn = raw() try: - assert not dom_exists(conn, "acestest-web"), "domain orphaned after teardown" - assert not net_exists(conn, "acestest-lan"), "network orphaned after teardown" + assert not dom_exists(conn, "raestest-web"), "domain orphaned after teardown" + assert not net_exists(conn, "raestest-lan"), "network orphaned after teardown" finally: conn.close() return "fresh-driver teardown removed real domain + network (no orphans)" @@ -227,7 +227,7 @@ def t_teardown_inactive_domain(): assert not r.diagnostics, [x.code for x in r.diagnostics] conn = raw() try: - dom = conn.lookupByName("acestest-inact") + dom = conn.lookupByName("raestest-inact") dom.destroy() # stop it out of band -> defined but inactive assert dom.isActive() == 0 finally: @@ -236,7 +236,7 @@ def t_teardown_inactive_domain(): assert not res.diagnostics, f"inactive teardown should be clean: {[x.code for x in res.diagnostics]}" conn = raw() try: - assert not dom_exists(conn, "acestest-inact"), "inactive domain not undefined" + assert not dom_exists(conn, "raestest-inact"), "inactive domain not undefined" finally: conn.close() return "teardown of an already-inactive domain (VIR_ERR_OPERATION_INVALID on stop) succeeds" @@ -245,7 +245,7 @@ def t_teardown_inactive_domain(): def t_ownership_conflict_not_destroyed(): # Define a FOREIGN domain at our runtime name with a different UUID; driver # teardown must refuse and leave it intact. - name = "acestest-foreign" + name = "raestest-foreign" foreign_uuid = "11111111-2222-3333-4444-555555555555" xml = f"""{name}{foreign_uuid} 641 @@ -282,7 +282,7 @@ def t_nwfilter_lifecycle(): assert not r.diagnostics, [x.code for x in r.diagnostics] conn = raw() try: - nf = conn.nwfilterLookupByName("acestest-fw-acl") # raises if missing + nf = conn.nwfilterLookupByName("raestest-fw-acl") # raises if missing assert nf.UUIDString() == _filter_owner_uuid("provision.node.fw") finally: conn.close() @@ -291,7 +291,7 @@ def t_nwfilter_lifecycle(): try: gone = False try: - conn.nwfilterLookupByName("acestest-fw-acl") + conn.nwfilterLookupByName("raestest-fw-acl") except libvirt.libvirtError: gone = True assert gone, "nwfilter not undefined after teardown" @@ -315,7 +315,7 @@ def t_partial_create_rollback(): ] conn = raw() try: - assert not dom_exists(conn, "acestest-bad"), "partial-create domain orphaned (not rolled back)" + assert not dom_exists(conn, "raestest-bad"), "partial-create domain orphaned (not rolled back)" finally: conn.close() return "domain whose start failed was rolled back (undefined) - no orphan" @@ -355,7 +355,7 @@ def _plan(*resources, action=ChangeAction.CREATE): def t_provisioner_full_stack(): # LibvirtProvisioner -> real driver: CREATE then DELETE (teardown) then idempotent re-DELETE. - drv = LibvirtDeploymentDriver(connection_uri=URI, name_prefix="acesprov") + drv = LibvirtDeploymentDriver(connection_uri=URI, name_prefix="raesprov") prov = LibvirtProvisioner(drv) net = PlannedResource( address="provision.network.pnet", @@ -375,8 +375,8 @@ def t_provisioner_full_stack(): assert set(create.snapshot.entries) == {"provision.network.pnet", "provision.node.pweb"} conn = raw() try: - assert net_exists(conn, "acesprov-pnet") and conn.networkLookupByName("acesprov-pnet").isActive() == 1 - assert dom_exists(conn, "acesprov-pweb") and dom_state_running(conn, "acesprov-pweb") + assert net_exists(conn, "raesprov-pnet") and conn.networkLookupByName("raesprov-pnet").isActive() == 1 + assert dom_exists(conn, "raesprov-pweb") and dom_state_running(conn, "raesprov-pweb") finally: conn.close() @@ -399,8 +399,8 @@ def t_provisioner_full_stack(): assert teardown.snapshot.entries == {} conn = raw() try: - assert not dom_exists(conn, "acesprov-pweb"), "provisioner teardown orphaned domain" - assert not net_exists(conn, "acesprov-pnet"), "provisioner teardown orphaned network" + assert not dom_exists(conn, "raesprov-pweb"), "provisioner teardown orphaned domain" + assert not net_exists(conn, "raesprov-pnet"), "provisioner teardown orphaned network" finally: conn.close() @@ -413,7 +413,7 @@ def t_provisioner_full_stack(): def t_cirros_real_boot_and_teardown(): # Full realize path: cirros overlay disk + cloud-init seed (genisoimage) -> # a real guest OS boots, then is torn down. - overlay = os.path.join(tempfile.gettempdir(), "aces-cirros-overlay.qcow2") + overlay = os.path.join(tempfile.gettempdir(), "raes-cirros-overlay.qcow2") subprocess.run( ["qemu-img", "create", "-f", "qcow2", "-F", "qcow2", "-b", CIRROS, overlay], check=True, capture_output=True ) @@ -435,8 +435,8 @@ def t_cirros_real_boot_and_teardown(): assert not r.diagnostics, [x.code for x in r.diagnostics] conn = raw() try: - assert dom_state_running(conn, "acestest-cirros"), "cirros domain not running" - xml = conn.lookupByName("acestest-cirros").XMLDesc() + assert dom_state_running(conn, "raestest-cirros"), "cirros domain not running" + xml = conn.lookupByName("raestest-cirros").XMLDesc() assert "cdrom" in xml and ".iso" in xml, "cloud-init seed ISO not attached" finally: conn.close() @@ -444,7 +444,7 @@ def t_cirros_real_boot_and_teardown(): assert not res.diagnostics, [x.code for x in res.diagnostics] conn = raw() try: - assert not dom_exists(conn, "acestest-cirros"), "cirros domain orphaned" + assert not dom_exists(conn, "raestest-cirros"), "cirros domain orphaned" finally: conn.close() return "real cirros guest booted with cloud-init seed ISO, then torn down cleanly" @@ -453,13 +453,13 @@ def t_cirros_real_boot_and_teardown(): def t_no_orphans_at_end(): conn = raw() try: - doms = [x.name() for x in conn.listAllDomains() if x.name().startswith((PREFIX, "acesprov"))] - nets = [x.name() for x in conn.listAllNetworks() if x.name().startswith((PREFIX, "acesprov"))] - nfs = [x.name() for x in conn.listAllNWFilters() if x.name().startswith((PREFIX, "acesprov"))] + doms = [x.name() for x in conn.listAllDomains() if x.name().startswith((PREFIX, "raesprov"))] + nets = [x.name() for x in conn.listAllNetworks() if x.name().startswith((PREFIX, "raesprov"))] + nfs = [x.name() for x in conn.listAllNWFilters() if x.name().startswith((PREFIX, "raesprov"))] assert not doms and not nets and not nfs, f"orphans left: doms={doms} nets={nets} nfs={nfs}" finally: conn.close() - return "no acestest/acesprov domains, networks, or nwfilters remain" + return "no raestest/raesprov domains, networks, or nwfilters remain" def main() -> int: diff --git a/tools/real-daemon/run_aws_guest_certify.sh b/tools/real-daemon/run_aws_guest_certify.sh index ab0980050..1fdd69925 100755 --- a/tools/real-daemon/run_aws_guest_certify.sh +++ b/tools/real-daemon/run_aws_guest_certify.sh @@ -24,8 +24,8 @@ KEEP=0 REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" WORK="$(mktemp -d)" -KEY="$WORK/aces-guest-test.pem" -NAME="aces-guest-certify-test" +KEY="$WORK/raes-guest-test.pem" +NAME="raes-guest-certify-test" AWS=(aws --profile "$PROFILE" --region "$REGION") cleanup_aws() { @@ -50,7 +50,7 @@ SUBNET=$("${AWS[@]}" ec2 describe-subnets --filters Name=default-for-az,Values=t "${AWS[@]}" ec2 create-key-pair --key-name "$NAME" --query KeyMaterial --output text > "$KEY" chmod 600 "$KEY" -SG=$("${AWS[@]}" ec2 create-security-group --group-name "$NAME-sg" --description "aces guest-certify proof" --vpc-id "$VPC" --query GroupId --output text 2>/dev/null \ +SG=$("${AWS[@]}" ec2 create-security-group --group-name "$NAME-sg" --description "raes guest-certify proof" --vpc-id "$VPC" --query GroupId --output text 2>/dev/null \ || "${AWS[@]}" ec2 describe-security-groups --filters Name=group-name,Values="$NAME-sg" --query 'SecurityGroups[0].GroupId' --output text) "${AWS[@]}" ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr "$MYIP/32" >/dev/null 2>&1 || true @@ -89,30 +89,30 @@ echo "=== wait for ssh + userdata ===" for _ in $(seq 1 40); do ssh "${SSHOPT[@]}" ubuntu@"$IP" "test -f /var/lib/cloud/userdata-done" 2>/dev/null && break; sleep 10; done echo "=== deploy code ===" -ssh "${SSHOPT[@]}" ubuntu@"$IP" "mkdir -p /home/ubuntu/aces/implementations/python /home/ubuntu/aces/contracts /home/ubuntu/aces/examples" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "mkdir -p /home/ubuntu/raes/implementations/python /home/ubuntu/raes/contracts /home/ubuntu/raes/examples" rsync -az --delete --exclude '.venv' --exclude '__pycache__' --exclude '.git' --exclude '.pytest_cache' --exclude '.nox' --exclude '*.pyc' \ - -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/implementations/python/" ubuntu@"$IP":/home/ubuntu/aces/implementations/python/ -rsync -az --delete --exclude '.git' -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/contracts/" ubuntu@"$IP":/home/ubuntu/aces/contracts/ -rsync -az --delete --exclude '.git' -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/examples/" ubuntu@"$IP":/home/ubuntu/aces/examples/ -scp "${SSHOPT[@]}" "$REPO_ROOT/.ground-control.yaml" ubuntu@"$IP":/home/ubuntu/aces/.ground-control.yaml + -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/implementations/python/" ubuntu@"$IP":/home/ubuntu/raes/implementations/python/ +rsync -az --delete --exclude '.git' -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/contracts/" ubuntu@"$IP":/home/ubuntu/raes/contracts/ +rsync -az --delete --exclude '.git' -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/examples/" ubuntu@"$IP":/home/ubuntu/raes/examples/ +scp "${SSHOPT[@]}" "$REPO_ROOT/.ground-control.yaml" ubuntu@"$IP":/home/ubuntu/raes/.ground-control.yaml # The editable build (hatch_build.py) reads the repo-root README for packaging. -scp "${SSHOPT[@]}" "$REPO_ROOT/README.md" ubuntu@"$IP":/home/ubuntu/aces/README.md +scp "${SSHOPT[@]}" "$REPO_ROOT/README.md" ubuntu@"$IP":/home/ubuntu/raes/README.md echo "=== install venv + libvirt-python ===" -ssh "${SSHOPT[@]}" ubuntu@"$IP" "curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1; cd ~/aces/implementations/python && ~/.local/bin/uv sync --all-extras >/dev/null 2>&1 && ~/.local/bin/uv pip install libvirt-python >/dev/null 2>&1 && echo venv-ready" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1; cd ~/raes/implementations/python && ~/.local/bin/uv sync --all-extras >/dev/null 2>&1 && ~/.local/bin/uv pip install libvirt-python >/dev/null 2>&1 && echo venv-ready" echo "=== run guest-certified proof ===" -ssh "${SSHOPT[@]}" ubuntu@"$IP" "sudo bash -lc 'cd /home/ubuntu/aces && implementations/python/.venv/bin/python -c \" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "sudo bash -lc 'cd /home/ubuntu/raes && implementations/python/.venv/bin/python -c \" from pathlib import Path from raes_operations.libvirt_evidence_run import run_libvirt_evidence_run, LibvirtEvidenceRunConfig -r = run_libvirt_evidence_run(scenario_path=Path(\\\"examples/scenarios/techvault-guest-certified.sdl.yaml\\\").resolve(), project_dir=Path(\\\"/home/ubuntu/aces/gc-out\\\"), run_id=\\\"$RUN_ID\\\", config=LibvirtEvidenceRunConfig(evidence_source_mode=\\\"guest-certified\\\", connection_uri=\\\"qemu:///system\\\")) +r = run_libvirt_evidence_run(scenario_path=Path(\\\"examples/scenarios/techvault-guest-certified.sdl.yaml\\\").resolve(), project_dir=Path(\\\"/home/ubuntu/raes/gc-out\\\"), run_id=\\\"$RUN_ID\\\", config=LibvirtEvidenceRunConfig(evidence_source_mode=\\\"guest-certified\\\", connection_uri=\\\"qemu:///system\\\")) print(r.render()) import sys; sys.exit(0 if r.passed else 1) \"'" echo "=== pull evidence artifact ===" -ssh "${SSHOPT[@]}" ubuntu@"$IP" "sudo chown -R ubuntu /home/ubuntu/aces/gc-out 2>/dev/null || true" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "sudo chown -R ubuntu /home/ubuntu/raes/gc-out 2>/dev/null || true" mkdir -p "$REPO_ROOT/tools/real-daemon/evidence" -scp "${SSHOPT[@]}" ubuntu@"$IP":/home/ubuntu/aces/gc-out/runs/"$RUN_ID"/scenario-evidence/libvirt-scenario-evidence-run.json \ +scp "${SSHOPT[@]}" ubuntu@"$IP":/home/ubuntu/raes/gc-out/runs/"$RUN_ID"/scenario-evidence/libvirt-scenario-evidence-run.json \ "$REPO_ROOT/tools/real-daemon/evidence/guest-certified-$RUN_ID.json" echo "pulled: tools/real-daemon/evidence/guest-certified-$RUN_ID.json" diff --git a/tools/real-daemon/run_aws_smoke.sh b/tools/real-daemon/run_aws_smoke.sh index c3d8e77eb..75c47f110 100755 --- a/tools/real-daemon/run_aws_smoke.sh +++ b/tools/real-daemon/run_aws_smoke.sh @@ -24,8 +24,8 @@ KEEP=0 REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" WORK="$(mktemp -d)" -KEY="$WORK/aces-libvirt-test.pem" -NAME="aces-libvirt-test" +KEY="$WORK/raes-libvirt-test.pem" +NAME="raes-libvirt-test" AWS=(aws --profile "$PROFILE" --region "$REGION") cleanup_aws() { @@ -50,7 +50,7 @@ SUBNET=$("${AWS[@]}" ec2 describe-subnets --filters Name=default-for-az,Values=t "${AWS[@]}" ec2 create-key-pair --key-name "$NAME" --query KeyMaterial --output text > "$KEY" chmod 600 "$KEY" -SG=$("${AWS[@]}" ec2 create-security-group --group-name "$NAME-sg" --description "aces libvirt real-daemon smoke" --vpc-id "$VPC" --query GroupId --output text 2>/dev/null \ +SG=$("${AWS[@]}" ec2 create-security-group --group-name "$NAME-sg" --description "raes libvirt real-daemon smoke" --vpc-id "$VPC" --query GroupId --output text 2>/dev/null \ || "${AWS[@]}" ec2 describe-security-groups --filters Name=group-name,Values="$NAME-sg" --query 'SecurityGroups[0].GroupId' --output text) "${AWS[@]}" ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr "$MYIP/32" >/dev/null 2>&1 || true @@ -90,14 +90,14 @@ for _ in $(seq 1 30); do ssh "${SSHOPT[@]}" ubuntu@"$IP" "test -f /var/lib/cloud echo "=== deploy code ===" # rsync does not create multiple missing parent levels; pre-create the tree. -ssh "${SSHOPT[@]}" ubuntu@"$IP" "mkdir -p /home/ubuntu/aces/implementations/python /home/ubuntu/aces/contracts" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "mkdir -p /home/ubuntu/raes/implementations/python /home/ubuntu/raes/contracts" rsync -az --delete --exclude '.venv' --exclude '__pycache__' --exclude '.git' --exclude '.pytest_cache' --exclude '.nox' --exclude '*.pyc' \ - -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/implementations/python/" ubuntu@"$IP":/home/ubuntu/aces/implementations/python/ -rsync -az --delete --exclude '.git' -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/contracts/" ubuntu@"$IP":/home/ubuntu/aces/contracts/ -scp "${SSHOPT[@]}" "$REPO_ROOT/tools/real-daemon/libvirt_smoke.py" ubuntu@"$IP":/home/ubuntu/aces/implementations/python/real_daemon_smoke.py + -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/implementations/python/" ubuntu@"$IP":/home/ubuntu/raes/implementations/python/ +rsync -az --delete --exclude '.git' -e "ssh ${SSHOPT[*]}" "$REPO_ROOT/contracts/" ubuntu@"$IP":/home/ubuntu/raes/contracts/ +scp "${SSHOPT[@]}" "$REPO_ROOT/tools/real-daemon/libvirt_smoke.py" ubuntu@"$IP":/home/ubuntu/raes/implementations/python/real_daemon_smoke.py echo "=== install venv + libvirt-python ===" -ssh "${SSHOPT[@]}" ubuntu@"$IP" "curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1; cd ~/aces/implementations/python && ~/.local/bin/uv sync --all-extras >/dev/null 2>&1 && ~/.local/bin/uv pip install libvirt-python >/dev/null 2>&1 && echo venv-ready" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1; cd ~/raes/implementations/python && ~/.local/bin/uv sync --all-extras >/dev/null 2>&1 && ~/.local/bin/uv pip install libvirt-python >/dev/null 2>&1 && echo venv-ready" echo "=== run real-daemon smoke ===" -ssh "${SSHOPT[@]}" ubuntu@"$IP" "cd ~/aces/implementations/python && .venv/bin/python real_daemon_smoke.py" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "cd ~/raes/implementations/python && .venv/bin/python real_daemon_smoke.py" From 6dddb0e154aa5b7009b2f4ed8b68112ba8ac5ff6 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 23:01:19 +0200 Subject: [PATCH 20/55] Refactor contracts facade export manifest --- .../raes_contracts/contracts/__init__.py | 147 +------- .../raes_contracts/contracts/_exports.py | 315 ++++++++++++++++++ .../tests/test_contracts_facade_exports.py | 10 + 3 files changed, 329 insertions(+), 143 deletions(-) create mode 100644 implementations/python/packages/raes_contracts/contracts/_exports.py create mode 100644 implementations/python/tests/test_contracts_facade_exports.py diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index fd46389ab..9d0105a88 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -1,5 +1,8 @@ """Schema-first external contract models for RAES artifact boundaries.""" +# This module is an intentional re-export facade governed by PUBLIC_EXPORTS. +# ruff: noqa: F401, F403 + from ..versions import ( ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION, ATLAS_TACTICS_SOURCE_SCHEMA_VERSION, @@ -43,6 +46,7 @@ WorkflowFeature, WorkflowStatePredicateFeature, ) +from ._exports import PUBLIC_EXPORTS as __all__ from .associated_artifacts import AssociatedArtifactManifestModel, AssociatedArtifactSetDigestString from .base import ( BehavioralClaimBindingModel, @@ -356,146 +360,3 @@ NistCsfDefensiveCategorySourceModel, NistCsfDefensiveCategorySourceTermModel, ) - -# fmt: off -__all__ = [ - "AcesSemanticInvariantEntryModel", "AcesSemanticInvariantInputModel", "AcesSemanticInvariantProfileModel", - "AcesSemanticInvariantProfileReferenceModel", "ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION", - "ATLAS_TACTICS_SOURCE_SCHEMA_VERSION", "AttackEnterpriseTacticSourceTermModel", - "AttackEnterpriseTacticsSourceModel", "AtlasTacticSourceTermModel", "AtlasTacticsSourceModel", - "ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION", "AssociatedArtifactManifestModel", - "AssociatedArtifactParentReferenceModel", "AssociatedArtifactSetDigestString", - "BACKEND_MANIFEST_V2_SCHEMA_VERSION", "ApparatusIdentityModel", "BackendCompatibilityModel", - "BackendManifestV2Model", "BackendCapabilitiesV2Model", "BehavioralClaimBindingModel", "BehavioralRelationId", - "BehavioralTaxonomyRevision", "CONCEPT_FAMILIES_SCHEMA_VERSION", "ConceptBindingEntryModel", - "ConceptFamilyCatalogModel", "ConceptFamilyDefinitionModel", "ConceptFamilyId", "ConceptProvenanceCategory", - "CONTROLLED_VOCABULARIES_SCHEMA_VERSION", "ControlledVocabularyCatalogModel", - "ControlledVocabularyDefinitionModel", "ControlledVocabularySourceModel", "ControlledVocabularyTermId", - "ControlledVocabularyTermModel", "ContractModel", "ExperimentAnalysisPlanModel", - "NIST_CSF_DEFENSIVE_CATEGORIES_SOURCE_SCHEMA_VERSION", "NistCsfDefensiveCategorySourceModel", - "NistCsfDefensiveCategorySourceTermModel", - "ApparatusBindingTargetModel", "BindingOwnerModel", "BindingScalarType", - "ConfigurationTargetDeclarationModel", "ConfigurationTargetRegistryModel", - "ExperimentApparatusCompatibilityReferenceModel", "ExperimentApparatusComponentModel", - "ExperimentApparatusConstraintModel", "ExperimentApparatusContextModel", "ExperimentArtifactRefModel", - "ExperimentAugmentationDisclosureModel", "ExperimentBackendReferenceModel", - "ExperimentCaptureRequirementModel", "ExperimentCaptureSpecModel", "ExperimentCaptureSpecReferenceModel", - "ExperimentBindingDescriptorModel", "ExperimentBindingDescriptorSetModel", - "ExperimentCaptureWindowModel", "ExperimentChecksumModel", "ExperimentClockContextModel", - "ExperimentConditionAssignmentParameterModel", "ExperimentConditionAssignmentReferenceModel", - "ExperimentDerivedMeasureMethodModel", "ExperimentDerivedMeasureModel", - "ExperimentDerivedMeasureReferenceModel", "ExperimentEpisodeControlModel", "ExperimentEvidenceRecordModel", - "ExperimentEvidenceRecordReferenceModel", "ExperimentEvidenceReferenceModel", - "ExperimentEvaluationProtocolModel", "ExperimentInvalidationModel", "ExperimentManifestReferenceModel", - "ExperimentMeasurementChannelReferenceModel", "ExperimentMetricDefinitionModel", - "ExperimentMissingDataPolicyModel", "ExperimentMultipleComparisonPolicyModel", "ExperimentParameterModel", - "ExperimentProcessorReferenceModel", "ExperimentRealizedFormDisclosureModel", - "ExperimentRedVariantSelectionModel", "ExperimentReferenceModel", "ExperimentResultSummaryModel", - "ExperimentRunAllocationPlanModel", "ExperimentRunModel", "ExperimentRunPlanModel", - "ExperimentRunTraceabilityModel", "ExperimentScenarioReferenceModel", - "ExperimentScenarioSnapshotReferenceModel", "ExperimentSpecModel", "ExperimentSplitAndLeakageControlsModel", - "ExperimentStatisticalMethodModel", "ExperimentStochasticControlModel", "ExperimentStudyFactorModel", - "ExperimentStudyMembershipModel", "ExperimentStudyModel", "ExperimentTaskReferenceModel", - "ExperimentTaskModel", "ExperimentUncertaintyMethodModel", "ExperimentValidityNoteModel", - "LiteralBindingValueModel", "ParticipantConfigurationModel", "ParticipantConfigurationResultModel", - "ParticipantImplementationBindingTargetModel", - "RealizedBindingProvenanceModel", "ScenarioBindingTargetModel", "SecretReferenceBindingValueModel", - "EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION", "EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION", - "EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION", - "EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION", "EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION", - "EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION", "EXPERIMENT_RUN_SCHEMA_VERSION", - "EXPERIMENT_STUDY_SCHEMA_VERSION", "EXPERIMENT_TASK_SCHEMA_VERSION", "EvaluationHistoryEventModel", - "EvaluationPlanModel", "EvaluationResultStateModel", "PropositionAssertionPolarity", - "PropositionEvaluationBasis", "PropositionIndeterminacyReason", "PropositionLossDisclosureModel", - "PropositionLossKind", "PropositionProbeBindingModel", "PropositionTemporalContextModel", - "PropositionTruthOutcome", "PropositionTruthResultModel", "EVALUATION_STATE_SCHEMA_VERSION", - "EvaluatorCapabilitiesModel", "EventClassificationModel", "InstantiationRequestModel", - "OPERATION_SCHEMA_VERSION", "OperationReceiptModel", "OperationStatusModel", "ObservationCapabilitiesModel", - "OrchestrationPlanModel", "OrchestratorCapabilitiesModel", "PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION", - "PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION", - "PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION", - "PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION", "ParticipantActionEffectResultModel", - "ParticipantActionPreconditionResultModel", "ParticipantActionResultModel", - "ParticipantAttributionCandidateModel", "ParticipantAttributionEdgeModel", - "ParticipantAttributionEvidenceBasisModel", "ParticipantAttributionOrderingBasisModel", - "ParticipantActivityOccurrenceProvenanceModel", "ParticipantAutonomousExecutionStateModel", - "ParticipantBehaviorHistoryEventModel", - "ParticipantContextViewModel", "ParticipantControlDeclarationModel", - "ParticipantControlOccurrenceModel", "ParticipantCrossingOccurrenceModel", - "validate_participant_control_occurrence_context", - "validate_participant_crossing_occurrence_context", - "ParticipantDecisionSurfaceActionEntryModel", - "ParticipantDecisionSurfaceCandidateSetFormModel", - "ParticipantDecisionSurfaceConstrainedFormModel", - "ParticipantDecisionSurfaceExposureBindingModel", - "ParticipantDecisionSurfaceExposureRealizationModel", "ParticipantDecisionSurfaceModel", - "ParticipantDecisionSurfaceAssuranceV2Model", - "ParticipantDecisionSurfaceBehaviorAnchorV2Model", - "ParticipantDecisionSurfaceCausalCutModel", - "ParticipantDecisionSurfaceDeliveryV2Model", - "ParticipantDecisionSurfaceDerivationAnchorV2Model", - "ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model", - "ParticipantDecisionSurfaceExposureBindingV2Model", - "ParticipantDecisionSurfaceOpenEndedFormModel", - "ParticipantDecisionSurfaceSelectionModel", - "ParticipantDecisionSurfaceSelectionV2Model", - "ParticipantDecisionSurfaceSequenceCutModel", - "ParticipantDecisionSurfaceStateCutModel", - "ParticipantDecisionSurfaceStateCutOrderModel", - "ParticipantDecisionSurfaceV2Model", - "ParticipantDecisionSurfaceViewV2Model", - "validate_participant_decision_surface_context", "ParticipantEpisodeHistoryEventModel", - "ParticipantEpisodeStateModel", "ParticipantExposurePolicyModel", "ParticipantFeatureSupportLevel", - "ParticipantFeatureSupportModel", "ParticipantHistoryViewBehaviorEventModel", - "ParticipantHistoryViewEpisodeEventModel", "ParticipantHistoryViewModel", - "ParticipantImplementationCapabilitiesModel", "ParticipantImplementationCompatibilityModel", - "ParticipantImplementationManifestModel", "ParticipantImplementationProvenanceModel", - "ParticipantImplementationSelectionModel", "ParticipantJointActionAccessSetModel", - "ParticipantJointActionRecordModel", "ParticipantLifecycleEventModel", "ParticipantObservationEnvelopeModel", - "ParticipantObservationLossDescriptorModel", "ParticipantObservationStochasticContextModel", - "ParticipantOutcomeInterpretationRecordModel", "ParticipantOutcomeReportModel", - "ParticipantOutcomeReportSourceModel", "ParticipantOutcomeReportStateRelationshipModel", - "ParticipantOutcomeSourceRecordModel", "ParticipantOutcomeTargetRecordModel", - "ParticipantRuntimeBaseEnvelopeModel", "ParticipantRuntimeCapabilitiesModel", - "ParticipantSharedStateAccessModel", "ParticipantSharedStateRecordModel", - "ParticipantStatusViewEpisodeStateModel", "ParticipantStatusViewModel", - "ParticipantTemporalRuntimeContextModel", "ParticipantTimeManagementContextModel", - "VIEW_SCOPE_PROJECTED_FIELDS", "PlanOperationModel", "ProcessorFeature", - "PROCESSOR_MANIFEST_V2_SCHEMA_VERSION", "ProcessorManifestV2Model", "ProcessorCompatibilityModel", - "ProcessorCapabilitiesV2Model", "ProvisionerCapabilitiesModel", "ProvisioningPlanModel", - "RANDOM_STREAM_DRAW_PURPOSE_SCOPE", "RANDOM_STREAM_PROFILE_SCHEMA_VERSION", - "RANDOM_STREAM_VECTOR_SCHEMA_VERSION", "GovernedEntropyRefModel", "GovernedRandomOutcomeRefModel", - "PublicRandomOutcomeModel", "PublicSeedModel", "RandomDrawOutcomeModel", - "RandomStreamAddressEncodingSpecModel", "RandomStreamBlockEncodingSpecModel", - "RandomStreamBoundedIntegerVectorCaseModel", "RandomStreamControlBindingModel", - "RandomStreamDerivationSpecModel", "RandomStreamDrawRecordModel", "RandomStreamGeneratorModel", - "RandomStreamProfileModel", "RandomStreamProfileReferenceModel", "RandomStreamRootEntropySpecModel", - "RandomStreamTransformSpecModel", "RandomStreamVectorModel", "RootEntropyModel", "StreamAddressModel", - "TrialCoordinateModel", - "RealizationEnvelopeIdentityModel", "RawDataIntegrityModel", "RealizationProvenanceEntryModel", - "RealizationSupportDeclarationModel", "RealizationSupportMode", "ReferenceModelCatalogModel", - "ReferenceModelDefinitionModel", "ReferenceModelSchemaBindingModel", "REFERENCE_MODELS_SCHEMA_VERSION", - "REUSABLE_ASSET_EVIDENCE_CLASSES", "REUSABLE_ASSET_FAMILIES", "REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION", - "RUNTIME_SNAPSHOT_SCHEMA_VERSION", "ReusableAssetAuthenticityPolicyModel", - "ReusableAssetEvidenceRequirementModel", "ReusableAssetFamilyTrustPolicyModel", - "ReusableAssetTrustPolicyModel", "RUNTIME_FACT_BINDING_PLANE_V1_SCHEMA_VERSION", - "RuntimeFactAbsenceDisposition", "RuntimeFactAudience", "RuntimeFactBindingDisposition", - "RuntimeFactBindingEventModel", "RuntimeFactBindingPlaneModel", "RuntimeFactBindingRequestModel", - "RuntimeFactBindingSelectionModel", "RuntimeFactDeclarationModel", "RuntimeFactProjectionModel", - "RuntimeFactScopeKind", "RuntimeFactScopeModel", "RuntimeFactSensitivity", "RuntimeFactSinkModel", - "RuntimeFactSourceKind", "RuntimeFactValueType", "RuntimeFactVersionModel", "RuntimeFactVisibilityModel", - "RuntimeSnapshotEnvelopeModel", - "SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION", "SEMANTIC_PROFILE_SCHEMA_VERSION", "schema_bundle", - "SemanticBehaviorAssumptionModel", "SemanticProfileModel", "SemanticProfilePhaseModel", "SnapshotEntryModel", - "SourcePipelineModel", "SourceStatusModel", "UcoAlignmentCatalogModel", "UcoAlignmentTypeModel", - "UcoFamilyAlignmentModel", "UCO_ALIGNMENT_SCHEMA_VERSION", "WorkflowCancellationRequestModel", - "WORKFLOW_CANCELLATION_REQUEST_SCHEMA_VERSION", "WorkflowExecutionStateModel", "WorkflowFeature", - "WorkflowHistoryEventModel", "WorkflowStatePredicateFeature", "WorkflowStepStateModel", - "WORKFLOW_STATE_SCHEMA_VERSION", "validate_aces_semantic_invariant_annotations", - "validate_experiment_apparatus_context_against_manifests", - "validate_experiment_apparatus_context_archival_datetimes", "validate_experiment_run_against_task", - "validate_experiment_run_archival_datetimes", "validate_experiment_study_against_tasks_and_runs", - "validate_experiment_study_archival_datetimes", "validate_experiment_task_archival_datetimes", - "ValidationBasisDisclosureDocumentModel", "validate_experiment_run_time_model", "TimeCapabilitiesModel", -] -# fmt: on diff --git a/implementations/python/packages/raes_contracts/contracts/_exports.py b/implementations/python/packages/raes_contracts/contracts/_exports.py new file mode 100644 index 000000000..df23c07da --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/_exports.py @@ -0,0 +1,315 @@ +"""Canonical public export manifest for the contracts facade.""" + +PUBLIC_EXPORTS = [ + "AcesSemanticInvariantEntryModel", + "AcesSemanticInvariantInputModel", + "AcesSemanticInvariantProfileModel", + "AcesSemanticInvariantProfileReferenceModel", + "ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION", + "ATLAS_TACTICS_SOURCE_SCHEMA_VERSION", + "AttackEnterpriseTacticSourceTermModel", + "AttackEnterpriseTacticsSourceModel", + "AtlasTacticSourceTermModel", + "AtlasTacticsSourceModel", + "ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION", + "AssociatedArtifactManifestModel", + "AssociatedArtifactParentReferenceModel", + "AssociatedArtifactSetDigestString", + "BACKEND_MANIFEST_V2_SCHEMA_VERSION", + "ApparatusIdentityModel", + "BackendCompatibilityModel", + "BackendManifestV2Model", + "BackendCapabilitiesV2Model", + "BehavioralClaimBindingModel", + "BehavioralRelationId", + "BehavioralTaxonomyRevision", + "CONCEPT_FAMILIES_SCHEMA_VERSION", + "ConceptBindingEntryModel", + "ConceptFamilyCatalogModel", + "ConceptFamilyDefinitionModel", + "ConceptFamilyId", + "ConceptProvenanceCategory", + "CONTROLLED_VOCABULARIES_SCHEMA_VERSION", + "ControlledVocabularyCatalogModel", + "ControlledVocabularyDefinitionModel", + "ControlledVocabularySourceModel", + "ControlledVocabularyTermId", + "ControlledVocabularyTermModel", + "ContractModel", + "ExperimentAnalysisPlanModel", + "NIST_CSF_DEFENSIVE_CATEGORIES_SOURCE_SCHEMA_VERSION", + "NistCsfDefensiveCategorySourceModel", + "NistCsfDefensiveCategorySourceTermModel", + "ApparatusBindingTargetModel", + "BindingOwnerModel", + "BindingScalarType", + "ConfigurationTargetDeclarationModel", + "ConfigurationTargetRegistryModel", + "ExperimentApparatusCompatibilityReferenceModel", + "ExperimentApparatusComponentModel", + "ExperimentApparatusConstraintModel", + "ExperimentApparatusContextModel", + "ExperimentArtifactRefModel", + "ExperimentAugmentationDisclosureModel", + "ExperimentBackendReferenceModel", + "ExperimentCaptureRequirementModel", + "ExperimentCaptureSpecModel", + "ExperimentCaptureSpecReferenceModel", + "ExperimentBindingDescriptorModel", + "ExperimentBindingDescriptorSetModel", + "ExperimentCaptureWindowModel", + "ExperimentChecksumModel", + "ExperimentClockContextModel", + "ExperimentConditionAssignmentParameterModel", + "ExperimentConditionAssignmentReferenceModel", + "ExperimentDerivedMeasureMethodModel", + "ExperimentDerivedMeasureModel", + "ExperimentDerivedMeasureReferenceModel", + "ExperimentEpisodeControlModel", + "ExperimentEvidenceRecordModel", + "ExperimentEvidenceRecordReferenceModel", + "ExperimentEvidenceReferenceModel", + "ExperimentEvaluationProtocolModel", + "ExperimentInvalidationModel", + "ExperimentManifestReferenceModel", + "ExperimentMeasurementChannelReferenceModel", + "ExperimentMetricDefinitionModel", + "ExperimentMissingDataPolicyModel", + "ExperimentMultipleComparisonPolicyModel", + "ExperimentParameterModel", + "ExperimentProcessorReferenceModel", + "ExperimentRealizedFormDisclosureModel", + "ExperimentRedVariantSelectionModel", + "ExperimentReferenceModel", + "ExperimentResultSummaryModel", + "ExperimentRunAllocationPlanModel", + "ExperimentRunModel", + "ExperimentRunPlanModel", + "ExperimentRunTraceabilityModel", + "ExperimentScenarioReferenceModel", + "ExperimentScenarioSnapshotReferenceModel", + "ExperimentSpecModel", + "ExperimentSplitAndLeakageControlsModel", + "ExperimentStatisticalMethodModel", + "ExperimentStochasticControlModel", + "ExperimentStudyFactorModel", + "ExperimentStudyMembershipModel", + "ExperimentStudyModel", + "ExperimentTaskReferenceModel", + "ExperimentTaskModel", + "ExperimentUncertaintyMethodModel", + "ExperimentValidityNoteModel", + "LiteralBindingValueModel", + "ParticipantConfigurationModel", + "ParticipantConfigurationResultModel", + "ParticipantImplementationBindingTargetModel", + "RealizedBindingProvenanceModel", + "ScenarioBindingTargetModel", + "SecretReferenceBindingValueModel", + "EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION", + "EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION", + "EXPERIMENT_BINDING_DESCRIPTORS_V1_SCHEMA_VERSION", + "EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION", + "EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION", + "EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION", + "EXPERIMENT_RUN_SCHEMA_VERSION", + "EXPERIMENT_STUDY_SCHEMA_VERSION", + "EXPERIMENT_TASK_SCHEMA_VERSION", + "EvaluationHistoryEventModel", + "EvaluationPlanModel", + "EvaluationResultStateModel", + "PropositionAssertionPolarity", + "PropositionEvaluationBasis", + "PropositionIndeterminacyReason", + "PropositionLossDisclosureModel", + "PropositionLossKind", + "PropositionProbeBindingModel", + "PropositionTemporalContextModel", + "PropositionTruthOutcome", + "PropositionTruthResultModel", + "EVALUATION_STATE_SCHEMA_VERSION", + "EvaluatorCapabilitiesModel", + "EventClassificationModel", + "InstantiationRequestModel", + "OPERATION_SCHEMA_VERSION", + "OperationReceiptModel", + "OperationStatusModel", + "ObservationCapabilitiesModel", + "OrchestrationPlanModel", + "OrchestratorCapabilitiesModel", + "PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION", + "PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION", + "PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION", + "PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION", + "ParticipantActionEffectResultModel", + "ParticipantActionPreconditionResultModel", + "ParticipantActionResultModel", + "ParticipantAttributionCandidateModel", + "ParticipantAttributionEdgeModel", + "ParticipantAttributionEvidenceBasisModel", + "ParticipantAttributionOrderingBasisModel", + "ParticipantActivityOccurrenceProvenanceModel", + "ParticipantAutonomousExecutionStateModel", + "ParticipantBehaviorHistoryEventModel", + "ParticipantContextViewModel", + "ParticipantControlDeclarationModel", + "ParticipantControlOccurrenceModel", + "ParticipantCrossingOccurrenceModel", + "validate_participant_control_occurrence_context", + "validate_participant_crossing_occurrence_context", + "ParticipantDecisionSurfaceActionEntryModel", + "ParticipantDecisionSurfaceCandidateSetFormModel", + "ParticipantDecisionSurfaceConstrainedFormModel", + "ParticipantDecisionSurfaceExposureBindingModel", + "ParticipantDecisionSurfaceExposureRealizationModel", + "ParticipantDecisionSurfaceModel", + "ParticipantDecisionSurfaceAssuranceV2Model", + "ParticipantDecisionSurfaceBehaviorAnchorV2Model", + "ParticipantDecisionSurfaceCausalCutModel", + "ParticipantDecisionSurfaceDeliveryV2Model", + "ParticipantDecisionSurfaceDerivationAnchorV2Model", + "ParticipantDecisionSurfaceEpisodeReadinessAnchorV2Model", + "ParticipantDecisionSurfaceExposureBindingV2Model", + "ParticipantDecisionSurfaceOpenEndedFormModel", + "ParticipantDecisionSurfaceSelectionModel", + "ParticipantDecisionSurfaceSelectionV2Model", + "ParticipantDecisionSurfaceSequenceCutModel", + "ParticipantDecisionSurfaceStateCutModel", + "ParticipantDecisionSurfaceStateCutOrderModel", + "ParticipantDecisionSurfaceV2Model", + "ParticipantDecisionSurfaceViewV2Model", + "validate_participant_decision_surface_context", + "ParticipantEpisodeHistoryEventModel", + "ParticipantEpisodeStateModel", + "ParticipantExposurePolicyModel", + "ParticipantFeatureSupportLevel", + "ParticipantFeatureSupportModel", + "ParticipantHistoryViewBehaviorEventModel", + "ParticipantHistoryViewEpisodeEventModel", + "ParticipantHistoryViewModel", + "ParticipantImplementationCapabilitiesModel", + "ParticipantImplementationCompatibilityModel", + "ParticipantImplementationManifestModel", + "ParticipantImplementationProvenanceModel", + "ParticipantImplementationSelectionModel", + "ParticipantJointActionAccessSetModel", + "ParticipantJointActionRecordModel", + "ParticipantLifecycleEventModel", + "ParticipantObservationEnvelopeModel", + "ParticipantObservationLossDescriptorModel", + "ParticipantObservationStochasticContextModel", + "ParticipantOutcomeInterpretationRecordModel", + "ParticipantOutcomeReportModel", + "ParticipantOutcomeReportSourceModel", + "ParticipantOutcomeReportStateRelationshipModel", + "ParticipantOutcomeSourceRecordModel", + "ParticipantOutcomeTargetRecordModel", + "ParticipantRuntimeBaseEnvelopeModel", + "ParticipantRuntimeCapabilitiesModel", + "ParticipantSharedStateAccessModel", + "ParticipantSharedStateRecordModel", + "ParticipantStatusViewEpisodeStateModel", + "ParticipantStatusViewModel", + "ParticipantTemporalRuntimeContextModel", + "ParticipantTimeManagementContextModel", + "VIEW_SCOPE_PROJECTED_FIELDS", + "PlanOperationModel", + "ProcessorFeature", + "PROCESSOR_MANIFEST_V2_SCHEMA_VERSION", + "ProcessorManifestV2Model", + "ProcessorCompatibilityModel", + "ProcessorCapabilitiesV2Model", + "ProvisionerCapabilitiesModel", + "ProvisioningPlanModel", + "RANDOM_STREAM_DRAW_PURPOSE_SCOPE", + "RANDOM_STREAM_PROFILE_SCHEMA_VERSION", + "RANDOM_STREAM_VECTOR_SCHEMA_VERSION", + "GovernedEntropyRefModel", + "GovernedRandomOutcomeRefModel", + "PublicRandomOutcomeModel", + "PublicSeedModel", + "RandomDrawOutcomeModel", + "RandomStreamAddressEncodingSpecModel", + "RandomStreamBlockEncodingSpecModel", + "RandomStreamBoundedIntegerVectorCaseModel", + "RandomStreamControlBindingModel", + "RandomStreamDerivationSpecModel", + "RandomStreamDrawRecordModel", + "RandomStreamGeneratorModel", + "RandomStreamProfileModel", + "RandomStreamProfileReferenceModel", + "RandomStreamRootEntropySpecModel", + "RandomStreamTransformSpecModel", + "RandomStreamVectorModel", + "RootEntropyModel", + "StreamAddressModel", + "TrialCoordinateModel", + "RealizationEnvelopeIdentityModel", + "RawDataIntegrityModel", + "RealizationProvenanceEntryModel", + "RealizationSupportDeclarationModel", + "RealizationSupportMode", + "ReferenceModelCatalogModel", + "ReferenceModelDefinitionModel", + "ReferenceModelSchemaBindingModel", + "REFERENCE_MODELS_SCHEMA_VERSION", + "REUSABLE_ASSET_EVIDENCE_CLASSES", + "REUSABLE_ASSET_FAMILIES", + "REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION", + "RUNTIME_SNAPSHOT_SCHEMA_VERSION", + "ReusableAssetAuthenticityPolicyModel", + "ReusableAssetEvidenceRequirementModel", + "ReusableAssetFamilyTrustPolicyModel", + "ReusableAssetTrustPolicyModel", + "RUNTIME_FACT_BINDING_PLANE_V1_SCHEMA_VERSION", + "RuntimeFactAbsenceDisposition", + "RuntimeFactAudience", + "RuntimeFactBindingDisposition", + "RuntimeFactBindingEventModel", + "RuntimeFactBindingPlaneModel", + "RuntimeFactBindingRequestModel", + "RuntimeFactBindingSelectionModel", + "RuntimeFactDeclarationModel", + "RuntimeFactProjectionModel", + "RuntimeFactScopeKind", + "RuntimeFactScopeModel", + "RuntimeFactSensitivity", + "RuntimeFactSinkModel", + "RuntimeFactSourceKind", + "RuntimeFactValueType", + "RuntimeFactVersionModel", + "RuntimeFactVisibilityModel", + "RuntimeSnapshotEnvelopeModel", + "SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION", + "SEMANTIC_PROFILE_SCHEMA_VERSION", + "schema_bundle", + "SemanticBehaviorAssumptionModel", + "SemanticProfileModel", + "SemanticProfilePhaseModel", + "SnapshotEntryModel", + "SourcePipelineModel", + "SourceStatusModel", + "UcoAlignmentCatalogModel", + "UcoAlignmentTypeModel", + "UcoFamilyAlignmentModel", + "UCO_ALIGNMENT_SCHEMA_VERSION", + "WorkflowCancellationRequestModel", + "WORKFLOW_CANCELLATION_REQUEST_SCHEMA_VERSION", + "WorkflowExecutionStateModel", + "WorkflowFeature", + "WorkflowHistoryEventModel", + "WorkflowStatePredicateFeature", + "WorkflowStepStateModel", + "WORKFLOW_STATE_SCHEMA_VERSION", + "validate_aces_semantic_invariant_annotations", + "validate_experiment_apparatus_context_against_manifests", + "validate_experiment_apparatus_context_archival_datetimes", + "validate_experiment_run_against_task", + "validate_experiment_run_archival_datetimes", + "validate_experiment_study_against_tasks_and_runs", + "validate_experiment_study_archival_datetimes", + "validate_experiment_task_archival_datetimes", + "ValidationBasisDisclosureDocumentModel", + "validate_experiment_run_time_model", + "TimeCapabilitiesModel", +] diff --git a/implementations/python/tests/test_contracts_facade_exports.py b/implementations/python/tests/test_contracts_facade_exports.py new file mode 100644 index 000000000..8df3d369a --- /dev/null +++ b/implementations/python/tests/test_contracts_facade_exports.py @@ -0,0 +1,10 @@ +"""Public-surface checks for the contracts package facade.""" + +import raes_contracts.contracts as contracts_facade +from raes_contracts.contracts._exports import PUBLIC_EXPORTS + + +def test_contracts_facade_matches_export_manifest() -> None: + assert contracts_facade.__all__ == PUBLIC_EXPORTS + assert len(PUBLIC_EXPORTS) == len(set(PUBLIC_EXPORTS)) + assert [name for name in PUBLIC_EXPORTS if not hasattr(contracts_facade, name)] == [] From 4c4b2604b471448f95a732db7587b577e7bf79de Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 23:10:08 +0200 Subject: [PATCH 21/55] Include autonomous execution migration in docs --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 1ba00d739..9dd51c4fb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -160,6 +160,7 @@ specs/formal lessons/README migration/README +migration/autonomous-execution-v2 migration/raes-rename research/experiment-core/index research/realization-envelope/index From 3b570ddddc7cf4a2fd91c5b019cecdd0b3178754 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 23:34:59 +0200 Subject: [PATCH 22/55] Retain existing SonarCloud project key --- ...-cutover-and-historical-record-boundary.md | 6 + docs/decisions/adrs/adr-index.yaml | 4 + ...sue-908-raes-identity-cutover-preflight.md | 10 +- .../tests/test_identity_cutover_policy.py | 34 ++++- sonar-project.properties | 4 +- tools/check_identity_cutover.py | 128 ++++++++++++++++-- tools/policy/historical_identity_records.json | 11 +- 7 files changed, 175 insertions(+), 22 deletions(-) diff --git a/docs/decisions/adrs/adr-096-identity-cutover-and-historical-record-boundary.md b/docs/decisions/adrs/adr-096-identity-cutover-and-historical-record-boundary.md index 87eec8cde..b65e08de1 100644 --- a/docs/decisions/adrs/adr-096-identity-cutover-and-historical-record-boundary.md +++ b/docs/decisions/adrs/adr-096-identity-cutover-and-historical-record-boundary.md @@ -129,3 +129,9 @@ validation, or evidence semantics beyond their identity-bearing values. - [ADR-061](adr-061-published-schema-evolution-policy.md) - [ADR-075](adr-075-ecosystem-versioning-deprecation-and-migration-governance.md) - [ADR-093](adr-093-raes-rename-and-compatibility-boundaries.md) + +## Amendments + +| Date | Commit/PR | Summary | +|---|---|---| +| 2026-07-26 | #908 | Retained the existing SonarCloud project key as an exact, content-bound external-service designation; it is not a current RAES product identity or a general compatibility allowance. | diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index a9b7707c0..da1978a8a 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -442,3 +442,7 @@ adrs: - id: ADR-096 path: docs/decisions/adrs/adr-096-identity-cutover-and-historical-record-boundary.md pin: 4d3ccdf376fd6eee58da0831d1215cf0a55ae25d2666d6bf3452978a3d0b3997 + amendments: + - date: 2026-07-26 + ref: "#908" + summary: "Retained the existing SonarCloud project key as an exact, content-bound external-service designation rather than a current RAES product identity." diff --git a/docs/decisions/issue-908-raes-identity-cutover-preflight.md b/docs/decisions/issue-908-raes-identity-cutover-preflight.md index 3565c1d2f..203211248 100644 --- a/docs/decisions/issue-908-raes-identity-cutover-preflight.md +++ b/docs/decisions/issue-908-raes-identity-cutover-preflight.md @@ -125,12 +125,14 @@ change invalidates the entry. The gate does not consume The repository targets these final identities: - schema namespace: `https://raes.dev/schemas/` -- Sonar project: `Brad-Edwards_raes` +- Sonar project: the existing pre-cutover service-owned key, retained as an + exact operational designation - Ground Control project: `raes-sdl` -Their remote resources must exist before the corresponding repository -configuration is considered verified. OBL-908-REMOTE-IDENTITIES-1 records that -provisioning dependency. +Replacement remote resources must exist before the corresponding repository +configuration points to them. OBL-908-REMOTE-IDENTITIES-1 records that +provisioning dependency; the retained Sonar project does not require a +replacement resource. ## Non-Goals diff --git a/implementations/python/tests/test_identity_cutover_policy.py b/implementations/python/tests/test_identity_cutover_policy.py index dce0016bd..ff3cfd373 100644 --- a/implementations/python/tests/test_identity_cutover_policy.py +++ b/implementations/python/tests/test_identity_cutover_policy.py @@ -41,17 +41,29 @@ def _record(path: str, content: bytes, *, occurrences: int = 1) -> dict[str, obj } +def _binding(path: str, content: bytes, *, occurrences: int = 1) -> dict[str, object]: + return { + "path": path, + "binding_class": "external-service-project-key", + "rationale": "Retains an exact service-owned project key.", + "occurrences": occurrences, + "content_sha256": hashlib.sha256(content).hexdigest(), + } + + def _seed_repo( repo_root: Path, *, files: dict[str, str | bytes], records: list[dict[str, object]] | None = None, + bindings: list[dict[str, object]] | None = None, ) -> None: for relative_path, content in files.items(): _write(repo_root / relative_path, content) manifest = { - "schema_version": "historical-identity-records/v1", + "schema_version": "historical-identity-records/v2", "hash_algorithm": "sha256", + "operational_bindings": bindings or [], "records": records or [], } _write( @@ -116,6 +128,26 @@ def test_changed_historical_record_fails_closed(tmp_path: Path) -> None: ) +def test_exact_content_bound_operational_binding_passes_and_fails_closed(tmp_path: Path) -> None: + content = f"projectKey=Brad-Edwards_{RETIRED_LOWER}\n".encode() + path = "sonar-project.properties" + _seed_repo( + tmp_path, + files={path: content}, + bindings=[_binding(path, content)], + ) + + assert evaluate_identity_cutover(tmp_path) == [] + + _write(tmp_path / path, content + b"changed=true\n") + failures = evaluate_identity_cutover(tmp_path) + + assert any( + failure.rule_id == "identity-cutover-operational-content" and failure.path == path + for failure in failures + ) + + def test_historical_occurrence_count_is_verified(tmp_path: Path) -> None: content = f"{RETIRED_UPPER} and {RETIRED_LOWER}.runtime\n".encode() path = "docs/research/snapshot.md" diff --git a/sonar-project.properties b/sonar-project.properties index e295e3b97..9d28a939d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ # SonarCloud Configuration -# https://sonarcloud.io/project/overview?id=Brad-Edwards_raes +# https://sonarcloud.io/project/overview?id=Brad-Edwards_aces -sonar.projectKey=Brad-Edwards_raes +sonar.projectKey=Brad-Edwards_aces sonar.organization=brad-edwards # Quality gate (issue #527): make the scanner wait for the SonarCloud quality diff --git a/tools/check_identity_cutover.py b/tools/check_identity_cutover.py index 93a45337e..9280582fc 100644 --- a/tools/check_identity_cutover.py +++ b/tools/check_identity_cutover.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Reject retired project naming outside exact historical records.""" +"""Reject retired project naming outside exact content-bound records.""" from __future__ import annotations @@ -23,7 +23,7 @@ ) MANIFEST_PATH = "tools/policy/historical_identity_records.json" -MANIFEST_SCHEMA = "historical-identity-records/v1" +MANIFEST_SCHEMA = "historical-identity-records/v2" MAX_MANIFEST_BYTES = 1_000_000 SHA256_RE = re.compile(r"^[0-9a-f]{64}$") IDENTITY_PATTERN = re.compile( @@ -41,12 +41,15 @@ "release-history", "research-record", } +OPERATIONAL_BINDING_CLASSES = {"external-service-project-key"} RULE_LIVE = "identity-cutover-live-token" RULE_MANIFEST = "identity-cutover-manifest" RULE_MANIFEST_PATH = "identity-cutover-manifest-path" RULE_HISTORICAL_CONTENT = "identity-cutover-historical-content" RULE_HISTORICAL_COUNT = "identity-cutover-historical-count" +RULE_OPERATIONAL_CONTENT = "identity-cutover-operational-content" +RULE_OPERATIONAL_COUNT = "identity-cutover-operational-count" RULE_TRACKED_TREE = "identity-cutover-tracked-tree" @@ -59,13 +62,22 @@ class HistoricalRecord: content_sha256: str +@dataclass(frozen=True) +class OperationalBinding: + path: str + binding_class: str + rationale: str + occurrences: int + content_sha256: str + + def _manifest_failure(message: str) -> PolicyFailure: return PolicyFailure(RULE_MANIFEST, message, MANIFEST_PATH) def _load_historical_records( repo_root: Path, -) -> tuple[dict[str, HistoricalRecord], list[PolicyFailure]]: +) -> tuple[dict[str, HistoricalRecord], dict[str, OperationalBinding], list[PolicyFailure]]: try: payload = load_bounded_json_object( repo_root, @@ -73,7 +85,7 @@ def _load_historical_records( max_bytes=MAX_MANIFEST_BYTES, ) except (OSError, UnicodeDecodeError, ValueError) as exc: - return {}, [_manifest_failure(str(exc))] + return {}, {}, [_manifest_failure(str(exc))] failures: list[PolicyFailure] = [] if payload.get("schema_version") != MANIFEST_SCHEMA: @@ -83,7 +95,11 @@ def _load_historical_records( raw_records = payload.get("records") if not isinstance(raw_records, list): failures.append(_manifest_failure("records must be a list")) - return {}, failures + return {}, {}, failures + raw_bindings = payload.get("operational_bindings") + if not isinstance(raw_bindings, list): + failures.append(_manifest_failure("operational_bindings must be a list")) + return {}, {}, failures records: dict[str, HistoricalRecord] = {} expected_keys = { @@ -135,7 +151,71 @@ def _load_historical_records( occurrences=occurrences, content_sha256=content_sha256, ) - return records, failures + + bindings: dict[str, OperationalBinding] = {} + binding_keys = { + "path", + "binding_class", + "rationale", + "occurrences", + "content_sha256", + } + for index, raw_binding in enumerate(raw_bindings): + if not isinstance(raw_binding, dict): + failures.append(_manifest_failure(f"operational_bindings[{index}] must be an object")) + continue + if set(raw_binding) != binding_keys: + failures.append( + _manifest_failure( + f"operational_bindings[{index}] must contain exactly {sorted(binding_keys)!r}" + ) + ) + continue + path = raw_binding.get("path") + binding_class = raw_binding.get("binding_class") + rationale = raw_binding.get("rationale") + occurrences = raw_binding.get("occurrences") + content_sha256 = raw_binding.get("content_sha256") + if not isinstance(path, str) or not path: + failures.append(_manifest_failure(f"operational_bindings[{index}].path must be a non-empty string")) + continue + if path in records or path in bindings: + failures.append(_manifest_failure(f"duplicate content-bound path {path!r}")) + continue + if safe_repo_path(repo_root, path) is None: + failures.append(PolicyFailure(RULE_MANIFEST_PATH, "operational binding path is unsafe", path)) + continue + if binding_class not in OPERATIONAL_BINDING_CLASSES: + failures.append( + _manifest_failure( + f"operational_bindings[{index}].binding_class must be one of " + f"{sorted(OPERATIONAL_BINDING_CLASSES)!r}" + ) + ) + continue + if not isinstance(rationale, str) or not rationale.strip(): + failures.append(_manifest_failure(f"operational_bindings[{index}].rationale must be non-empty")) + continue + if isinstance(occurrences, bool) or not isinstance(occurrences, int) or occurrences < 1: + failures.append( + _manifest_failure(f"operational_bindings[{index}].occurrences must be a positive integer") + ) + continue + if not isinstance(content_sha256, str) or not SHA256_RE.fullmatch(content_sha256): + failures.append( + _manifest_failure( + f"operational_bindings[{index}].content_sha256 must be a lowercase sha256 digest" + ) + ) + continue + bindings[path] = OperationalBinding( + path=path, + binding_class=binding_class, + rationale=rationale, + occurrences=occurrences, + content_sha256=content_sha256, + ) + return records, bindings, failures def _tracked_paths(repo_root: Path) -> tuple[list[str], list[PolicyFailure]]: @@ -157,7 +237,7 @@ def _match_lines(content: bytes, matches: list[re.Match[bytes]]) -> list[int]: def evaluate_identity_cutover(repo_root: Path = REPO_ROOT) -> list[PolicyFailure]: - records, failures = _load_historical_records(repo_root) + records, bindings, failures = _load_historical_records(repo_root) tracked_paths, tracked_failures = _tracked_paths(repo_root) failures.extend(tracked_failures) if failures: @@ -172,6 +252,14 @@ def evaluate_identity_cutover(repo_root: Path = REPO_ROOT) -> list[PolicyFailure record_path, ) ) + for binding_path in sorted(bindings.keys() - tracked): + failures.append( + PolicyFailure( + RULE_MANIFEST_PATH, + "operational binding is not a tracked file", + binding_path, + ) + ) for relative_path in tracked_paths: path = safe_repo_path(repo_root, relative_path) @@ -198,21 +286,33 @@ def evaluate_identity_cutover(repo_root: Path = REPO_ROOT) -> list[PolicyFailure matches = list(IDENTITY_PATTERN.finditer(content)) record = records.get(relative_path) - if record is not None: + binding = bindings.get(relative_path) + content_bound = record or binding + if content_bound is not None: digest = hashlib.sha256(content).hexdigest() - if digest != record.content_sha256: + if digest != content_bound.content_sha256: failures.append( PolicyFailure( - RULE_HISTORICAL_CONTENT, - "historical record content no longer matches its classified digest", + RULE_HISTORICAL_CONTENT if record is not None else RULE_OPERATIONAL_CONTENT, + ( + "historical record content no longer matches its classified digest" + if record is not None + else "operational binding content no longer matches its classified digest" + ), relative_path, ) ) - if len(matches) != record.occurrences: + if len(matches) != content_bound.occurrences: failures.append( PolicyFailure( - RULE_HISTORICAL_COUNT, - f"historical record declares {record.occurrences} occurrences but contains {len(matches)}", + RULE_HISTORICAL_COUNT if record is not None else RULE_OPERATIONAL_COUNT, + ( + f"historical record declares {content_bound.occurrences} occurrences " + f"but contains {len(matches)}" + if record is not None + else f"operational binding declares {content_bound.occurrences} occurrences " + f"but contains {len(matches)}" + ), relative_path, ) ) diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index 44fb84174..363e95492 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -1,6 +1,15 @@ { - "schema_version": "historical-identity-records/v1", + "schema_version": "historical-identity-records/v2", "hash_algorithm": "sha256", + "operational_bindings": [ + { + "path": "sonar-project.properties", + "binding_class": "external-service-project-key", + "rationale": "Retains the existing service-owned SonarCloud project designation without treating it as current RAES product identity.", + "occurrences": 2, + "content_sha256": "df85536b996b633de8f1400941ce9a8b47e72c22bfe1cb5736e520a02d0fe361" + } + ], "records": [ { "path": "CHANGELOG.md", From bcbf740773c143c2cecee6bee53237be29217527 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 23:35:07 +0200 Subject: [PATCH 23/55] Fix migration documentation toctree --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 1ba00d739..9dd51c4fb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -160,6 +160,7 @@ specs/formal lessons/README migration/README +migration/autonomous-execution-v2 migration/raes-rename research/experiment-core/index research/realization-envelope/index From c55f93e6b18e1e82065852ba12a7ae4cbce83e8c Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 26 Jul 2026 23:48:49 +0200 Subject: [PATCH 24/55] Refresh ADR index identity record --- tools/policy/historical_identity_records.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index 363e95492..b44595733 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -492,7 +492,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", "occurrences": 4, - "content_sha256": "4d8077e5dced72d2980fa7846b8908b400941757a332cd7ae502c42fe829e626" + "content_sha256": "fbbaa8aee300174af664fea63031221f93bd45d734566ddf1f255b3c05e98e25" }, { "path": "docs/decisions/cage-2-replication-design.md", From 4fcc205c0e910e43c6ca728e7e32fa5751613036 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 00:29:45 +0200 Subject: [PATCH 25/55] feat(participant): add portable execution lifecycle control --- .../backend-manifest-v2/valid/stub.json | 11 +- .../entries/backend-manifest-v2.json | 6 +- .../entries/backend-profile-v1.json | 6 +- .../participant-execution-binding-v1.json | 10 + .../participant-execution-control-v1.json | 10 + ...articipant-execution-service-state-v1.json | 10 + .../entries/runtime-snapshot-v1.json | 6 +- contracts/schemas/README.md | 22 + .../backend-manifest/backend-manifest-v2.json | 150 ++++ .../participant-execution-binding-v1.json | 90 +++ .../participant-execution-control-v1.json | 56 ++ ...articipant-execution-service-state-v1.json | 192 +++++ .../schemas/profiles/backend-profile-v1.json | 3 + .../snapshots/runtime-snapshot-v1.json | 197 +++++ ...s-benign-participants-under-shared-time.md | 43 ++ docs/decisions/adrs/adr-index.yaml | 5 +- ...participant-execution-control-preflight.md | 380 ++++++++++ docs/explain/reference/backend-conformance.md | 17 +- .../reference/shared-semantic-integrity.md | 2 +- docs/explain/sdl/lineage.md | 11 + docs/explain/sdl/runtime-architecture.md | 9 + docs/migration/README.md | 4 + .../participant-execution-control.md | 37 + .../lineage/source-audit-2026-07-12.md | 36 + .../raes_backend_protocols/capabilities.py | 1 + .../capability_admission.py | 38 + .../raes_backend_protocols/manifest.py | 6 + .../participant_action_commit.py | 3 + .../participant_capabilities.py | 104 +++ .../participant_execution_manifest.py | 67 ++ .../participant_execution_runtime.py | 89 +++ .../participant_execution_service.py | 21 + .../participant_runtime_base.py | 9 +- .../raes_backend_protocols/protocols.py | 41 +- .../participant_execution_probes.py | 295 ++++++++ .../conformance/snapshot_semantics.py | 3 + .../conformance/target_probes.py | 18 +- .../conformance/validators.py | 8 + .../raes_contracts/contracts/__init__.py | 5 +- .../raes_contracts/contracts/bundle.py | 8 + .../raes_contracts/contracts/manifests.py | 40 ++ .../contracts/participant_execution.py | 150 ++++ .../contracts/realization_plans.py | 5 + .../raes_contracts/manifest_authority.py | 15 +- .../raes_contracts/participant_binding.py | 34 +- .../participant_native_execution.py | 24 + .../packages/raes_contracts/runtime_state.py | 7 + .../participant_autonomous_execution.py | 55 +- .../raes_processor/models/__init__.py | 2 + .../models/behavior_resources.py | 12 + .../packages/raes_runtime/backend_calls.py | 3 + .../raes_runtime/control_plane_api.py | 68 ++ .../raes_runtime/control_plane_api_models.py | 10 +- .../raes_runtime/control_plane_execution.py | 30 + .../raes_runtime/control_plane_store.py | 2 + .../raes_runtime/participant_clock_driver.py | 39 +- .../raes_runtime/participant_control.py | 44 ++ .../participant_execution_control.py | 7 + .../participant_execution_control_boundary.py | 144 ++++ .../participant_execution_scheduler_state.py | 158 +++++ .../raes_runtime/participant_scheduler.py | 170 ++--- .../participant_scheduler_concurrency.py | 354 +++++++++ .../participant_scheduler_operations.py | 63 +- .../participant_scheduler_policy.py | 70 ++ .../participant_scheduler_types.py | 41 ++ .../python/packages/raes_runtime/registry.py | 35 + .../participant_execution_test_backend.py | 203 ++++++ ...st_dsl_437_benign_participant_execution.py | 115 ++- ...issue_898_participant_execution_control.py | 670 ++++++++++++++++++ .../autonomous-execution.md | 67 ++ .../participant-backend-contracts.md | 23 + tools/generate_contract_schemas.py | 3 + 72 files changed, 4495 insertions(+), 197 deletions(-) create mode 100644 contracts/schema-publication/entries/participant-execution-binding-v1.json create mode 100644 contracts/schema-publication/entries/participant-execution-control-v1.json create mode 100644 contracts/schema-publication/entries/participant-execution-service-state-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-execution-binding-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-execution-control-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-execution-service-state-v1.json create mode 100644 docs/decisions/issue-898-portable-participant-execution-control-preflight.md create mode 100644 docs/migration/participant-execution-control.md create mode 100644 implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py create mode 100644 implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py create mode 100644 implementations/python/packages/raes_backend_protocols/participant_execution_service.py create mode 100644 implementations/python/packages/raes_conformance/conformance/participant_execution_probes.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_execution.py create mode 100644 implementations/python/packages/raes_contracts/participant_native_execution.py create mode 100644 implementations/python/packages/raes_runtime/participant_execution_control_boundary.py create mode 100644 implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_policy.py create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_types.py create mode 100644 implementations/python/tests/participant_execution_test_backend.py create mode 100644 implementations/python/tests/test_issue_898_participant_execution_control.py 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 c0884ba2d..0c802ef69 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -125,6 +125,7 @@ }, "participant_runtime": { "constraints": {}, + "execution_bindings": [], "feature_support": [ { "constraint_refs": [], @@ -205,6 +206,8 @@ "max_autonomous_occurrences": null, "max_autonomous_participants": null, "max_autonomous_retries_per_occurrence": null, + "max_concurrent_actions": null, + "max_execution_services": null, "name": "stub-participant-runtime", "supported_autonomous_action_contracts": [], "supported_autonomous_activity_features": [], @@ -237,7 +240,10 @@ "red", "white" ], - "supports_autonomous_execution": false + "supported_execution_control_actions": [], + "supports_autonomous_execution": false, + "supports_bounded_concurrency": false, + "supports_execution_control": false }, "provisioner": { "constraints": {}, @@ -386,6 +392,9 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-execution-binding-v1", + "participant-execution-control-v1", + "participant-execution-service-state-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index 72bc774c2..86d5c77ed 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": "1b770e2150f7a36f51fa4da1a7fa267e256302da4b687c4438065ee0cbebf3ae", + "content_hash": "222e608578080336f461faee05fc37e2a28acf99f65276403b2d8d98511d9957", "last_change": { - "summary": "Combined evidence-backed participant policy support declarations with exact autonomous activity profile, feature, random-stream, occurrence, retry, and burst capabilities.", - "content_hash": "1b770e2150f7a36f51fa4da1a7fa267e256302da4b687c4438065ee0cbebf3ae" + "summary": "Added exact action-to-target execution bindings, lifecycle control, and bounded concurrent participant-service capability declarations for issue #898.", + "content_hash": "222e608578080336f461faee05fc37e2a28acf99f65276403b2d8d98511d9957" } } diff --git a/contracts/schema-publication/entries/backend-profile-v1.json b/contracts/schema-publication/entries/backend-profile-v1.json index ac7261a1c..6d5676299 100644 --- a/contracts/schema-publication/entries/backend-profile-v1.json +++ b/contracts/schema-publication/entries/backend-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "63c54137176f37d1253095c1fb93b3090084f604972e41588d0f71de14855e53", + "content_hash": "475bb6cc6cf7be8bda19fd0e3cb0e6d27321fabf81c31c7417f332846f1cf516", "last_change": { - "summary": "Added participant control and crossing occurrence contracts to the governed backend profile vocabulary.", - "content_hash": "63c54137176f37d1253095c1fb93b3090084f604972e41588d0f71de14855e53" + "summary": "Added participant execution binding, lifecycle control, and service-state contracts to the governed backend profile vocabulary for issue #898.", + "content_hash": "475bb6cc6cf7be8bda19fd0e3cb0e6d27321fabf81c31c7417f332846f1cf516" } } diff --git a/contracts/schema-publication/entries/participant-execution-binding-v1.json b/contracts/schema-publication/entries/participant-execution-binding-v1.json new file mode 100644 index 000000000..3e2a80659 --- /dev/null +++ b/contracts/schema-publication/entries/participant-execution-binding-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-execution-binding-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-execution-binding-v1.json", + "stability": "draft", + "content_hash": "3b80c5ca6cf152de84426f2e22c1d66bdba8157b18b9ed85c2ca051b323df818", + "last_change": { + "summary": "Published the exact native participant action-to-target execution relation and finite execution bounds for issue #898.", + "content_hash": "3b80c5ca6cf152de84426f2e22c1d66bdba8157b18b9ed85c2ca051b323df818" + } +} diff --git a/contracts/schema-publication/entries/participant-execution-control-v1.json b/contracts/schema-publication/entries/participant-execution-control-v1.json new file mode 100644 index 000000000..022dd3d3d --- /dev/null +++ b/contracts/schema-publication/entries/participant-execution-control-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-execution-control-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-execution-control-v1.json", + "stability": "draft", + "content_hash": "efe00dae0e16258eb63f6c1ca2c833027ee1a659f5e7f41053d0d81a1ec69e50", + "last_change": { + "summary": "Published generation-fenced start, pause, resume, bounded drain, reset, and teardown requests for issue #898.", + "content_hash": "efe00dae0e16258eb63f6c1ca2c833027ee1a659f5e7f41053d0d81a1ec69e50" + } +} diff --git a/contracts/schema-publication/entries/participant-execution-service-state-v1.json b/contracts/schema-publication/entries/participant-execution-service-state-v1.json new file mode 100644 index 000000000..d2a596d99 --- /dev/null +++ b/contracts/schema-publication/entries/participant-execution-service-state-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-execution-service-state-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-execution-service-state-v1.json", + "stability": "draft", + "content_hash": "59bb66a44d86b6466bb2511abb9f62148291d762ab0a4a93ca912c2fb8c970bc", + "last_change": { + "summary": "Published participant execution lifecycle, generation, health, readiness, concurrency, shared-time provenance, and evidence readback for issue #898.", + "content_hash": "59bb66a44d86b6466bb2511abb9f62148291d762ab0a4a93ca912c2fb8c970bc" + } +} diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index b237cabd4..d77571193 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": "d0b31b768af1ebd963e590429e0d38f4b16e816f25c59b95703d3166d463985b", + "content_hash": "c5e118021ab5337ca2bb7a79db7a71fce276413463ffe9fe8dee46989d123e6b", "last_change": { - "summary": "Added typed autonomous activity v2 continuation and safe occurrence provenance to runtime snapshots for issue #897.", - "content_hash": "d0b31b768af1ebd963e590429e0d38f4b16e816f25c59b95703d3166d463985b" + "summary": "Added generation-fenced participant execution service health, readiness, capacity, shared-time provenance, and lifecycle evidence for issue #898.", + "content_hash": "c5e118021ab5337ca2bb7a79db7a71fce276413463ffe9fe8dee46989d123e6b" } } diff --git a/contracts/schemas/README.md b/contracts/schemas/README.md index 9ad9e8067..1eff5c5b2 100644 --- a/contracts/schemas/README.md +++ b/contracts/schemas/README.md @@ -23,6 +23,8 @@ Current published schemas cover: - evaluation result envelopes - evaluation history streams - operation receipts and statuses +- exact participant action-to-target execution bindings, generation-fenced + execution lifecycle control, and typed execution-service readback - control-plane participant status/history/context views, including SEM-214 context-view meaning and comparability semantics - experiment-core task, run, apparatus-context, study/collection, capture @@ -54,6 +56,26 @@ provenance relations, reference resolution, and graph semantics remain the normative model/semantic-admission layer; schema validity alone does not assert that provenance is truthful or that an artifact is compiler-admitted. +## Participant Execution Control + +The participant-runtime family publishes three issue #898 contracts: + +- `participant-execution-binding-v1` declares one exact native action, + target-service set, participant implementation, evidence/constraints, and + finite timeout, retry, attempt, and in-flight bounds. +- `participant-execution-control-v1` carries generation-fenced `start`, + `pause`, `resume`, bounded `drain`, `reset`, and `teardown` requests. +- `participant-execution-service-state-v1` separates lifecycle, generation, + health, readiness, work admission, capacity, quiescence/resource release, + policy/binding/shared-time digests, pacing deviations, and evidence. + +The service state is embedded under +`runtime-snapshot-v1.participant_execution_services`; control mutations use +the existing operation receipt/status contracts. Schema validity does not +prove native execution. Autonomous capability conformance additionally +requires executable bounded actions, typed outcomes, operation accounting, +and lifecycle evidence. + Current filenames still use `runtime` for some live-execution artifacts. That naming is preserved for compatibility while the repository migrates toward the processor/runtime boundary described in diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index b21910166..82bc41dba 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -709,6 +709,94 @@ "title": "OrchestratorCapabilitiesModel", "type": "object" }, + "ParticipantExecutionBindingModel": { + "additionalProperties": false, + "description": "Exact executable relation between one action and its native targets.", + "properties": { + "action_contract_address": { + "minLength": 1, + "title": "Action Contract Address", + "type": "string" + }, + "binding_id": { + "minLength": 1, + "title": "Binding Id", + "type": "string" + }, + "constraint_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Constraint Refs", + "type": "array" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "max_action_attempts": { + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_in_flight": { + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_retries": { + "minimum": 0, + "title": "Max Retries", + "type": "integer" + }, + "participant_implementation_ref": { + "minLength": 1, + "title": "Participant Implementation Ref", + "type": "string" + }, + "schema_version": { + "const": "participant-execution-binding/v1", + "default": "participant-execution-binding/v1", + "title": "Schema Version", + "type": "string" + }, + "target_addresses": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Target Addresses", + "type": "array" + }, + "timeout_seconds": { + "minimum": 1, + "title": "Timeout Seconds", + "type": "integer" + } + }, + "required": [ + "binding_id", + "action_contract_address", + "target_addresses", + "participant_implementation_ref", + "constraint_refs", + "evidence_refs", + "max_action_attempts", + "max_in_flight", + "timeout_seconds", + "max_retries" + ], + "title": "ParticipantExecutionBindingModel", + "type": "object" + }, "ParticipantFeatureSupportLevel": { "description": "ADR-054 guarantee-strength scale for per-feature participant runtime support.", "enum": [ @@ -918,6 +1006,13 @@ "title": "Constraints", "type": "object" }, + "execution_bindings": { + "items": { + "$ref": "#/$defs/ParticipantExecutionBindingModel" + }, + "title": "Execution Bindings", + "type": "array" + }, "feature_support": { "items": { "$ref": "#/$defs/ParticipantFeatureSupportModel" @@ -1003,6 +1098,32 @@ "default": null, "title": "Max Autonomous Retries Per Occurrence" }, + "max_concurrent_actions": { + "anyOf": [ + { + "minimum": 2, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Concurrent Actions" + }, + "max_execution_services": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Execution Services" + }, "name": { "minLength": 1, "title": "Name", @@ -1096,6 +1217,22 @@ "type": "array", "uniqueItems": true }, + "supported_execution_control_actions": { + "items": { + "enum": [ + "start", + "pause", + "resume", + "drain", + "reset", + "teardown" + ], + "type": "string" + }, + "title": "Supported Execution Control Actions", + "type": "array", + "uniqueItems": true + }, "supported_interaction_features": { "items": { "minLength": 1, @@ -1120,6 +1257,16 @@ "default": false, "title": "Supports Autonomous Execution", "type": "boolean" + }, + "supports_bounded_concurrency": { + "default": false, + "title": "Supports Bounded Concurrency", + "type": "boolean" + }, + "supports_execution_control": { + "default": false, + "title": "Supports Execution Control", + "type": "boolean" } }, "required": [ @@ -1817,6 +1964,9 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-execution-binding-v1", + "participant-execution-control-v1", + "participant-execution-service-state-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", diff --git a/contracts/schemas/participant-runtime/participant-execution-binding-v1.json b/contracts/schemas/participant-runtime/participant-execution-binding-v1.json new file mode 100644 index 000000000..2b10f2fc4 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-execution-binding-v1.json @@ -0,0 +1,90 @@ +{ + "$id": "https://aces.dev/schemas/participant-execution-binding-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Exact executable relation between one action and its native targets.", + "properties": { + "action_contract_address": { + "minLength": 1, + "title": "Action Contract Address", + "type": "string" + }, + "binding_id": { + "minLength": 1, + "title": "Binding Id", + "type": "string" + }, + "constraint_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Constraint Refs", + "type": "array" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "max_action_attempts": { + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_in_flight": { + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_retries": { + "minimum": 0, + "title": "Max Retries", + "type": "integer" + }, + "participant_implementation_ref": { + "minLength": 1, + "title": "Participant Implementation Ref", + "type": "string" + }, + "schema_version": { + "const": "participant-execution-binding/v1", + "default": "participant-execution-binding/v1", + "title": "Schema Version", + "type": "string" + }, + "target_addresses": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Target Addresses", + "type": "array" + }, + "timeout_seconds": { + "minimum": 1, + "title": "Timeout Seconds", + "type": "integer" + } + }, + "required": [ + "binding_id", + "action_contract_address", + "target_addresses", + "participant_implementation_ref", + "constraint_refs", + "evidence_refs", + "max_action_attempts", + "max_in_flight", + "timeout_seconds", + "max_retries" + ], + "title": "ParticipantExecutionBindingModel", + "type": "object" +} diff --git a/contracts/schemas/participant-runtime/participant-execution-control-v1.json b/contracts/schemas/participant-runtime/participant-execution-control-v1.json new file mode 100644 index 000000000..a6ab92b30 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-execution-control-v1.json @@ -0,0 +1,56 @@ +{ + "$id": "https://aces.dev/schemas/participant-execution-control-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Generation-fenced lifecycle mutation for one admitted execution scope.", + "properties": { + "action": { + "enum": [ + "start", + "pause", + "resume", + "drain", + "reset", + "teardown" + ], + "title": "Action", + "type": "string" + }, + "execution_scope_ref": { + "minLength": 1, + "title": "Execution Scope Ref", + "type": "string" + }, + "expected_generation": { + "minimum": 0, + "title": "Expected Generation", + "type": "integer" + }, + "schema_version": { + "const": "participant-execution-control/v1", + "default": "participant-execution-control/v1", + "title": "Schema Version", + "type": "string" + }, + "timeout_seconds": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout Seconds" + } + }, + "required": [ + "execution_scope_ref", + "action", + "expected_generation" + ], + "title": "ParticipantExecutionControlRequestModel", + "type": "object" +} diff --git a/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json b/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json new file mode 100644 index 000000000..df5a7bebe --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json @@ -0,0 +1,192 @@ +{ + "$id": "https://aces.dev/schemas/participant-execution-service-state-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Typed health, readiness, lifecycle, capacity, and evidence readback.", + "properties": { + "accepting_new_work": { + "title": "Accepting New Work", + "type": "boolean" + }, + "binding_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": "Binding Digest", + "type": "string" + }, + "capacity": { + "minimum": 1, + "title": "Capacity", + "type": "integer" + }, + "desired_lifecycle": { + "enum": [ + "stopped", + "starting", + "running", + "pausing", + "paused", + "draining", + "quiescent", + "resetting", + "tearing_down", + "terminated", + "failed" + ], + "title": "Desired Lifecycle", + "type": "string" + }, + "draining": { + "title": "Draining", + "type": "boolean" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "execution_scope_ref": { + "minLength": 1, + "title": "Execution Scope Ref", + "type": "string" + }, + "generation": { + "minimum": 0, + "title": "Generation", + "type": "integer" + }, + "health": { + "enum": [ + "healthy", + "degraded", + "unhealthy", + "unknown" + ], + "title": "Health", + "type": "string" + }, + "in_flight": { + "minimum": 0, + "title": "In Flight", + "type": "integer" + }, + "last_transition_ref": { + "minLength": 1, + "title": "Last Transition Ref", + "type": "string" + }, + "observed_generation": { + "minimum": 0, + "title": "Observed Generation", + "type": "integer" + }, + "observed_lifecycle": { + "enum": [ + "stopped", + "starting", + "running", + "pausing", + "paused", + "draining", + "quiescent", + "resetting", + "tearing_down", + "terminated", + "failed" + ], + "title": "Observed Lifecycle", + "type": "string" + }, + "pacing_deviation_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Pacing Deviation Refs", + "type": "array" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "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" + }, + "quiescent": { + "title": "Quiescent", + "type": "boolean" + }, + "readiness": { + "enum": [ + "ready", + "not_ready", + "unknown" + ], + "title": "Readiness", + "type": "string" + }, + "reserved": { + "minimum": 0, + "title": "Reserved", + "type": "integer" + }, + "resources_released": { + "title": "Resources Released", + "type": "boolean" + }, + "scheduler_state_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Scheduler State Refs", + "type": "array" + }, + "schema_version": { + "const": "participant-execution-service-state/v1", + "default": "participant-execution-service-state/v1", + "title": "Schema Version", + "type": "string" + }, + "time_declaration_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": "Time Declaration Digest", + "type": "string" + } + }, + "required": [ + "execution_scope_ref", + "policy_address", + "desired_lifecycle", + "observed_lifecycle", + "generation", + "observed_generation", + "health", + "readiness", + "accepting_new_work", + "draining", + "quiescent", + "resources_released", + "policy_digest", + "binding_digest", + "time_declaration_digest", + "capacity", + "reserved", + "in_flight", + "last_transition_ref", + "evidence_refs" + ], + "title": "ParticipantExecutionServiceStateModel", + "type": "object" +} diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index 079d471ca..37a5aca6d 100644 --- a/contracts/schemas/profiles/backend-profile-v1.json +++ b/contracts/schemas/profiles/backend-profile-v1.json @@ -29,6 +29,9 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-execution-binding-v1", + "participant-execution-control-v1", + "participant-execution-service-state-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index 0760a31fb..ebb27486e 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -2547,6 +2547,196 @@ "title": "ParticipantEpisodeStateModel", "type": "object" }, + "ParticipantExecutionServiceStateModel": { + "additionalProperties": false, + "description": "Typed health, readiness, lifecycle, capacity, and evidence readback.", + "properties": { + "accepting_new_work": { + "title": "Accepting New Work", + "type": "boolean" + }, + "binding_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": "Binding Digest", + "type": "string" + }, + "capacity": { + "minimum": 1, + "title": "Capacity", + "type": "integer" + }, + "desired_lifecycle": { + "enum": [ + "stopped", + "starting", + "running", + "pausing", + "paused", + "draining", + "quiescent", + "resetting", + "tearing_down", + "terminated", + "failed" + ], + "title": "Desired Lifecycle", + "type": "string" + }, + "draining": { + "title": "Draining", + "type": "boolean" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "execution_scope_ref": { + "minLength": 1, + "title": "Execution Scope Ref", + "type": "string" + }, + "generation": { + "minimum": 0, + "title": "Generation", + "type": "integer" + }, + "health": { + "enum": [ + "healthy", + "degraded", + "unhealthy", + "unknown" + ], + "title": "Health", + "type": "string" + }, + "in_flight": { + "minimum": 0, + "title": "In Flight", + "type": "integer" + }, + "last_transition_ref": { + "minLength": 1, + "title": "Last Transition Ref", + "type": "string" + }, + "observed_generation": { + "minimum": 0, + "title": "Observed Generation", + "type": "integer" + }, + "observed_lifecycle": { + "enum": [ + "stopped", + "starting", + "running", + "pausing", + "paused", + "draining", + "quiescent", + "resetting", + "tearing_down", + "terminated", + "failed" + ], + "title": "Observed Lifecycle", + "type": "string" + }, + "pacing_deviation_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Pacing Deviation Refs", + "type": "array" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "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" + }, + "quiescent": { + "title": "Quiescent", + "type": "boolean" + }, + "readiness": { + "enum": [ + "ready", + "not_ready", + "unknown" + ], + "title": "Readiness", + "type": "string" + }, + "reserved": { + "minimum": 0, + "title": "Reserved", + "type": "integer" + }, + "resources_released": { + "title": "Resources Released", + "type": "boolean" + }, + "scheduler_state_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Scheduler State Refs", + "type": "array" + }, + "schema_version": { + "const": "participant-execution-service-state/v1", + "default": "participant-execution-service-state/v1", + "title": "Schema Version", + "type": "string" + }, + "time_declaration_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": "Time Declaration Digest", + "type": "string" + } + }, + "required": [ + "execution_scope_ref", + "policy_address", + "desired_lifecycle", + "observed_lifecycle", + "generation", + "observed_generation", + "health", + "readiness", + "accepting_new_work", + "draining", + "quiescent", + "resources_released", + "policy_digest", + "binding_digest", + "time_declaration_digest", + "capacity", + "reserved", + "in_flight", + "last_transition_ref", + "evidence_refs" + ], + "title": "ParticipantExecutionServiceStateModel", + "type": "object" + }, "ParticipantExternalDirectionOccurrenceModel": { "additionalProperties": false, "description": "A scoped direction that does not bypass proposal validation or admission.", @@ -7124,6 +7314,13 @@ "title": "Participant Episode Results", "type": "object" }, + "participant_execution_services": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantExecutionServiceStateModel" + }, + "title": "Participant Execution Services", + "type": "object" + }, "proposition_truth_results": { "additionalProperties": { "$ref": "#/$defs/PropositionTruthResultModel" 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 f1ca7b2a2..5b26cd746 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 @@ -165,6 +165,48 @@ or causality from timestamps. Backend admission is exact for profile, policy features, selection strategy, random-stream profile/transform support, time constraint kinds, and finite limits. +### 8. Make native execution services portable and generation fenced + +An autonomous backend claim is relational. Each +`participant-execution-binding/v1` record binds exactly one participant action +contract to its native target-service addresses, selected participant +implementation, constraints, evidence, timeout/retry policy, and finite +attempt/in-flight bounds. Separate action and target sets are discovery +indexes; they do not authorize their Cartesian product. + +The participant runtime exposes `participant-execution-control/v1` lifecycle +requests for `start`, `pause`, `resume`, bounded `drain`, `reset`, and +`teardown`. Requests carry the expected execution generation. Reset increments +that generation; work admitted under a stale generation is rejected before +native execution, and a native completion whose generation changed is +discarded without committing its snapshot or portable history. +The shared participant base supplies typed readback but does not synthesize +control success. A backend-owned operation must perform scheduler/shared-time +coordination and resource work; RAES accepts success only when changed +readback and new evidence prove the requested observation. + +`participant-execution-service-state/v1` is the typed readback for desired and +observed lifecycle, generation, health, readiness, admission state, finite +capacity, reserved/in-flight work, resource release, policy/binding/shared-time +digests, scheduler references, pacing deviations, and evidence. Health, +readiness, lifecycle, participant episode state, and scheduler continuation +remain distinct concepts. + +Multiple due participants may execute natively in parallel only within the +admitted finite bound. Every worker receives one immutable predecessor; +portable state and history commit serially with revision checks. Conflicting +shared-state or history writes fail closed rather than selecting a last writer. +Shared-clock pause/resume controls execution admission, shared-clock reset +advances the generation, and loss of wall pacing degrades and pauses the +execution service with an explicit deviation/evidence reference. + +Manifest support requires the complete lifecycle action set, exact execution +bindings, and a concurrent capacity of at least two. Runtime-target +registration requires executable binding, lifecycle/readback, and bounded +batch methods. Conditional live conformance drives two native actions and the +full lifecycle; a declaration with methods but no typed outcome or lifecycle +evidence does not pass. + ## Consequences - Human, AI, scripted, and benign simulated participants share one semantic and @@ -201,3 +243,4 @@ constraint kinds, and finite limits. |------|-----------|---------| | 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. | diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 5cb24c962..30ba720eb 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -425,7 +425,7 @@ adrs: pin: c2a0e6ac9fb87aa10fbb571b0f70efe0b99520b806f6ffe706f1556b0ec7e84b - id: ADR-092 path: docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md - pin: a4398471e58b052d6b7c05cf9014aa4a76adf3e39c7dddd6b1db8a8301047577 + pin: adf8a99f99a941a2a24b781df017f03e155b601ded2ecd170dd605549456521b amendments: - date: 2026-07-24 ref: "#861" @@ -433,6 +433,9 @@ adrs: - date: 2026-07-26 ref: "#897" summary: "Kept v1 stable and governed richer within-run timing, weighted selection, lifecycle state, and provenance as a versioned autonomous-execution profile." + - 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." - id: ADR-093 path: docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md pin: 72725b8f09063b0b1d8ec65c4fda58e5648113035588ee4effbece5e0b1c3436 diff --git a/docs/decisions/issue-898-portable-participant-execution-control-preflight.md b/docs/decisions/issue-898-portable-participant-execution-control-preflight.md new file mode 100644 index 000000000..c717e38d8 --- /dev/null +++ b/docs/decisions/issue-898-portable-participant-execution-control-preflight.md @@ -0,0 +1,380 @@ +# Issue 898 Portable Participant Execution Control Preflight + +Issue: #898 + +Requirement: none; the issue is the authoritative contract. + +Date: 2026-07-26 + +This note fixes architecture guardrails before implementation. It is not an +implementation plan and adds no schema, runtime, backend, control-plane, +conformance, or release behavior. + +## Authority Decision + +Issue #898 extends the existing autonomous-participant path; it does not create +a second scheduler, actor, action, lifecycle, time, backend, or control-plane +root. + +The portable execution controller is a capability-specific extension of the +existing `RuntimeTarget.participant_runtime` component, following the incumbent +`AutonomousParticipantRuntime`, `CoordinatedParticipantResetRuntime`, and +`CoordinatedParticipantTimeRuntime` protocol pattern. Do not add a sibling +`participant_executor`, worker-service registry, or pack-owned controller to +`RuntimeTarget`. + +The implementation changes the authority boundary recorded by ADR-092: it +makes the autonomous scheduler/executor remotely controllable and admits +backend-native bounded concurrency. The normative change therefore requires an +in-band ADR-092 amendment and pin update under ADR-059 in the same change that +publishes the contracts. A new ADR would split one autonomous-execution +decision across two authorities. ADR-091 already owns portable time +control/readback and need not be amended unless its time semantics change. + +## Concept Boundaries + +Keep these state machines distinct and join them by typed references: + +| Concern | Canonical owner | Issue #898 boundary | +| --- | --- | --- | +| Authored autonomous behavior | ADR-092, `ParticipantBehaviorSpecification.autonomous_execution` | Unchanged; no new SDL root or pack-local scheduler syntax. | +| Participant episode | ADR-013, RUN-311, `ParticipantRuntime.initialize/reset/restart/terminate` | Episode identity and terminal state; not executor-service health, readiness, drain, or teardown. | +| Per-participant scheduler continuation | `ParticipantAutonomousExecutionStateModel` | Remains the policy/participant cursor and accounting state. It is not service health or an operation receipt. | +| Execution-service lifecycle | New capability-specific protocol on the incumbent participant runtime | Start, pause, resume, drain, reset-generation coordination, health/readiness, and teardown for one admitted execution scope. | +| Shared time | ADR-090/091, `TimeRuntime`, `time-runtime-state-v1` | Clock control, coordinate, segment, transition history, and pacing provenance. Executor pause cannot mint a private clock. | +| Native action execution | `ParticipantActionAdmissionRequest`, `ParticipantNativeActionExecution`, `ParticipantActionResultModel` | Exact action-to-target binding, native outcome, and safe provenance before portable history commit. | +| Control-plane operation | `OperationReceipt`, `OperationStatus`, `RuntimeDomain.PARTICIPANT` | Asynchronous acknowledgement/status for lifecycle mutations. It does not report participant action success. | +| Supervisory participant control | ADR-085/RUN-310, `ParticipantControlIntent` and control occurrences | Human/controller authority over participant decisions; not operational executor lifecycle. | +| Teardown | Execution-service resource release after drain | Does not terminate an episode, reset shared time, delete scenario services, or erase evidence. | + +The control target is an admitted execution scope identified by run/target, +compiled autonomous policy address, and execution generation. It is not an +individual action name, backend process id, participant episode id, or URL. + +## Portable Contract And Protocol Guardrails + +### Exact action-to-target support + +The current manifest has independent +`supported_autonomous_action_contracts` and +`supported_autonomous_target_addresses` lists. Their cross product is not an +execution claim. The compiled policy likewise carries action contracts and an +aggregate target set without preserving which action refers to which targets. + +Issue #898 must extend `capabilities.participant_runtime` with one closed, +relational execution-binding entry that preserves at least: + +- compiled participant action-contract address; +- every compiled target address required by that action's preconditions and + effects, including the named service address when the target is a service; +- the selected participant-implementation/adapter binding or its safe + apparatus reference; +- exact support/constraint and evidence references; and +- finite action, timeout, retry, and concurrency limits that apply to the + binding. + +Compiler output must preserve the same action-to-target relation. Admission +matches complete binding entries, never membership in two independent lists, +action-name conventions, resource-type guesses, native adapter discovery, or a +free-form manifest `constraints` string. + +The binding extends `ParticipantActionAdmissionRequest` and the existing +participant implementation manifest/provenance join. It must not create a +second action request DTO, copy target-service configuration into SDL, or make +the backend manifest the participant implementation identity. + +### Execution control and readback + +Publish the minimum closed participant-runtime contract needed to carry an +execution control request and typed execution-service readback. Reuse +`OperationReceiptModel` and `OperationStatusModel` for mutation acknowledgement +and polling, and reuse `runtime-snapshot-v1` for the durable joined state. Do +not publish participant-specific copies of those envelopes. + +The execution readback must distinguish: + +- desired and observed execution lifecycle; +- execution generation and the request generation most recently observed; +- liveness/health from readiness to accept governed work; +- accepting-new-work, draining, quiescent, and terminal/resource-released + conditions; +- admitted policy/binding/time declaration digests and execution-scope refs; +- bounded capacity, reserved/in-flight counts, and safe last-transition + identity; +- per-policy scheduler-state references rather than copied scheduler cursors; + and +- pacing-loss/deviation and evidence references rather than backend logs. + +Health means the controller can make an observation. Readiness means the +admitted bindings, time authority, workers, and native adapters can accept +governed work for the observed generation. Neither is participant episode +`running`, generic `SnapshotEntry.status`, HTTP reachability alone, or proof +that an action executed. + +Every mutating request carries the expected execution generation. The control +plane and backend reject a stale generation before reserving native work. +Completions from an older generation may be retained as bounded late/stale +evidence, but they cannot mutate current portable state or be silently retried. +Execution generation must not be overloaded onto `episode_id`, clock segment, +policy digest, operation id, snapshot schema version, or control-history head. + +### Lifecycle semantics + +- **Start** binds the admitted execution scope and generation, proves + readiness, and then permits due-action reservation. A receipt acknowledging + the request is not readiness. +- **Pause** stops new reservations and preserves continuation. It must + coordinate with the bound shared-time authority or record explicit pacing + loss; allowing the clock to skip a due coordinate remains invalid. +- **Resume** revalidates generation, bindings, readiness, shared-time state, + and missed-coordinate rules before accepting work. +- **Drain** rejects new reservations, waits only within a declared finite + bound for in-flight native calls, and ends quiescent or failed/degraded with + explicit unresolved-operation evidence. +- **Reset** uses the existing atomic shared-time/participant reset contract + when scenario semantics require reset, then advances execution generation. + Replacing a snapshot or scheduler cursor is not native rollback. +- **Teardown** is idempotent after drain, releases executor/adapter resources, + and forbids future work for that generation while preserving operation, + snapshot, behavior-history, and realized-time evidence. + +Reset, teardown, timeout, cancellation, and transport failure do not prove that +an indeterminate native side effect did not occur. + +### Bounded concurrency and commit safety + +The current `ParticipantScheduler.run_due()` loops policies and participants +serially under the participant execution lock. `max_in_flight` is currently an +admission/accounting field, not proof of concurrent execution. Conformance must +reject a backend that claims bounded concurrency but only changes the counter +or runs the existing serialized loop. + +Concurrency is bounded by the minimum of the policy, binding, backend-manifest, +and execution-service limits. Reservation increments in-flight accounting +atomically before native dispatch. Drain/reset/teardown close reservation +before waiting for or rejecting completions. + +Native calls may run concurrently, but portable snapshot/history commit has one +revision/generation-checked owner. Workers must not share a mutable +`RuntimeSnapshot`, merge whole snapshots returned from the same predecessor, or +hold the global control-plane lock across unbounded native I/O. Build on +`ParticipantNativeActionExecution` and the `BaseParticipantRuntime` commit +path: execute against an immutable reservation, then validate and commit the +typed terminal result, behavior history, scheduler accounting, joint-action +record, and shared-state revisions against current generation. + +Actual overlap and realized ordering use the existing RUN-308 +`ParticipantJointActionRecordModel`, +`ParticipantTimeManagementContextModel`, shared-state revision/conflict +contracts, ordering basis, isolation guarantee, and interaction classes. Equal +ticks, multiple threads, a worker-pool size, or `in_flight > 1` alone do not +prove simultaneity, isolation, or conflict handling. + +## Relevant Precedent + +Internal precedent is controlling: + +- ADR-013, ADR-022, ADR-041, ADR-054, ADR-060, ADR-066, ADR-072, ADR-085, + ADR-090, ADR-091, and ADR-092; +- RUN-308 concurrency/joint-action/time-management carriers and RUN-311 + episode lifecycle; +- the issue #599 participant implementation-binding preflight, issue #861 + native autonomous-execution guardrails, and issue #897 activity-policy + extension; and +- the issue #197 target/control-plane and issue #604 drain/teardown, + idempotency, ownership, redaction, and snapshot-commit precedents. + +External precedent supplies design criteria, not wire or semantic authority: + +- [FMI 3.0.2 Scheduled Execution](https://fmi-standard.org/docs/3.0.2/) + separates importer-controlled activation, state-machine legality, and + termination quiescence; it explicitly does not make parallel computation + part of the FMI API. +- [gRPC health checking](https://grpc.io/docs/guides/health-checking/) uses a + distinct service health readback whose status is maintained by the + implementation; endpoint presence is not health truth. +- [Google AIP-151](https://google.aip.dev/151) uses one reusable long-running + operation resource and explicit parallel-operation behavior rather than a + bespoke status type per lifecycle call. +- [Kubernetes Pod conditions](https://kubernetes.io/docs/concepts/workloads/pods/pod-condition/) + bind a reported condition to the generation it observed, providing precedent + for rejecting stale readiness. +- [OASIS TOSCA 2.0](https://docs.oasis-open.org/tosca/TOSCA/v2.0/cs01/TOSCA-v2.0-cs01.html) + separates lifecycle interface operations from their implementation + artifacts, supporting the RAES portable-operation/backend-owned-adapter + boundary. + +RAES adopts none of those APIs, schemas, lifecycle tokens, transports, or +compatibility claims. + +## Canonical Cross-Cutting Incumbents + +| Layer | Incumbent to extend | +| --- | --- | +| SDL ingress | `parse_sdl`, `load_sdl_yaml`, `SDLParserLimits`, safe YAML/duplicate/merge-key checks, and closed `SDLModel` shapes | +| Semantic validation | `raes.semantics.participant_behavior`, `raes.validator._participant_execution_renderers`, `raes.validator._time_model`, and existing action/target/service reference resolution | +| Compilation | `_compile_autonomous_execution`, `ParticipantAutonomousExecutionRuntime`, `ParticipantActionContractRuntime`, canonical address helpers, and canonical contract digests | +| Apparatus binding | `ParticipantImplementationManifestModel`, `ParticipantImplementationSelectionModel`, participant implementation provenance/configuration contracts, and `ParticipantActionAdmissionRequest` | +| Manifest/admission | `ParticipantRuntimeCapabilitiesModel`, `ParticipantRuntimeCapabilities`, manifest round-trip adapters, `PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS`, `participant_runtime_capability_contract_gaps()`, `participant_autonomous_execution_capability_gaps()`, and `_participant_execution_diagnostics()` | +| Runtime target | `RuntimeTarget`, `RuntimeTargetComponents`, `BackendRegistry`, `_validate_runtime_target_shape()`, `_require_invokable_method()`, and capability-specific protocols on `participant_runtime` | +| Native execution | `AutonomousParticipantRuntime`, `BaseParticipantRuntime`, `ParticipantNativeActionExecution`, `ParticipantActionApplyResult`, `autonomous_action_result_violation()`, and backend-owned adapter leaves | +| Scheduling/time | `ParticipantScheduler`, scheduler operation helpers, `RuntimeParticipantExecutionMixin`, `ParticipantClockDriver`, `RuntimeTimeControlMixin`, `TimeRuntime`, and coordinated reset protocols | +| Concurrency | RUN-308 joint-action, time-management, shared-state revision/conflict, ordering, isolation, and interaction-class contracts/validators | +| Control plane | `RuntimeControlPlane`, `execute_participant_action()`, `create_control_plane_app()`, `OperationReceipt`, `OperationStatus`, request fingerprints, and idempotency records | +| Persistence | `RuntimeSnapshot.participant_autonomous_execution_states`, first-class participant/time/history maps, `ControlPlaneStore`, `InMemoryControlPlaneStore`, and `LocalControlPlaneStore` | +| Result validation | `_call_backend_apply()`, `participant_runtime_state_contract_diagnostics()`, `participant_runtime_history_transition_diagnostics()`, and `require_participant_autonomous_runtime_snapshot()` | +| Observability | `Diagnostic`, `AuditEvent`, operation records, typed runtime snapshots/history, realized-time provenance, and `operational_apparatus_summary()` | +| Conformance | `profile_for_manifest()`, `_target_adapter_cases()`, target live probes, snapshot semantics, `time_model_conformance_diagnostics()`, backend fixtures/profiles, and bounded conformance reports | +| Publication/workflow | `ContractModel`, `schema_bundle()`, authored schemas and fixtures, schema-publication entries/manifest, canonical nox `verify`, Ground Control traceability, and release-please | + +Do not duplicate these as pack DTOs, backend-local public schemas, API-only +models, worker-state repositories, or adapter-specific exception/log streams. + +## Whole-Path Security And Validation Gates + +1. **SDL/parser gate.** Action, target, participant, policy, time, and + implementation refs still pass parser limits, closed authoring models, + semantic validation, canonical compilation, and address resolution. No + endpoint, URI, command, adapter name, credential, or worker configuration is + accepted from an untyped action field. +2. **Configuration and secret-reference gate.** Participant implementation + configuration continues through + `ConfigurationTargetRegistryModel` and the existing literal versus + `secret-reference` validators. Manifests, bindings, control requests, + receipts, snapshots, history, and provenance carry safe refs/digests, never + resolved credentials. Issue #898 adds no environment-variable binding or + secret resolver. +3. **Manifest/admission gate.** Closed manifest models, controlled + vocabularies, exact relational binding entries, finite limits, required + contract ids, and term-level evidence all agree across internal and wire + models. Independent action/target membership, method presence, installed + adapter libraries, or schema publication cannot satisfy admission. +4. **Runtime-target gate.** Component presence still matches the manifest. + Claimed lifecycle, concurrency, native binding, coordinated reset, and time + behavior require invokable capability-specific methods, but signature probes + remain structural only; live conformance proves behavior. +5. **Control-plane authentication gate.** New HTTP mutations stay inside + `create_control_plane_app()` with `ControlPlaneSecurityConfig.strict_defaults()`, + bearer or verified-proxy identity, operator/backend mutating roles, + `ControlPlaneIdentity.target_name` binding, request-size limits, request + fingerprints, idempotency keys, and `AuditEvent`. Executor operations are + target operations, not RUN-310 participant-subject authority and not + auditor mutations. +6. **Remote-target/SSRF gate.** A request names only admitted canonical + execution/action/target addresses. Backend registration resolves those to + preconfigured adapter leaves. Caller-supplied URLs, sockets, hostnames, + connection strings, filesystem paths, commands, and arbitrary headers never + cross the portable control boundary. +7. **Backend apply/commit gate.** `_call_backend_apply()` retains baseline + copying, exception wrapping, result-shape validation, snapshot semantic + validation, append-only history checks, and invalid-result rejection. + Generation/revision checks run both before native reservation and before + completion commit. No stale or failed result replaces durable current state. +8. **Persistence gate.** Service readback gets a first-class closed snapshot + field/carrier; scheduler continuation stays in + `participant_autonomous_execution_states`; action evidence stays in behavior + history; time stays in `time_model_state` and realized-time provenance. + `RuntimeSnapshot.metadata`, `ApplyResult.details`, audit reason text, and a + new participant database are not state stores. +9. **OS/process gate.** Backend adapters use in-process APIs or fixed argv with + no shell, bounded input/output/time, controlled working directory and + environment, and no credentials or action payloads in argv. Worker, + process, thread, host, pid, socket, and backend-native object ids stay + private unless a safe evidence reference explicitly governs them. +10. **Error-envelope gate.** Expected failures are bounded `Diagnostic` + values in existing operation status and conformance reports. Conflict + responses disclose only safe lifecycle/generation codes. The generic + redacted HTTP 500 handler remains the unexpected-error boundary; raw + Pydantic inputs, adapter errors, tracebacks, paths, command output, + credentials, and backend reprs do not cross it. +11. **Observability gate.** Typed receipts, statuses, health/readiness, + snapshots, behavior histories, joint-action/time-management records, + realized-time deviations, and audit events are the evidence surface. Logs + may carry safe ids, generations, counts, durations, and stable low-cardinality + codes only. Do not create a second audit log, evidence stream, logger, or + metric schema as contract authority. + +## Extensibility, Compatibility, And Traceability + +The primary extension seam is a versioned execution-binding entry plus a +versioned execution-control/readback contract on the incumbent participant +runtime component. A new product adapter adds a binding implementation and +evidence; a new lifecycle operation or state-machine meaning requires a new +contract version. Neither requires new SDL participant semantics, a new +control plane, or edits to pack-local schedulers. + +Concurrency policy remains parameterized by finite capacity, per-binding +limits, isolation/conflict guarantee, drain timeout, and execution generation. +The next reasonable variation—another service adapter or a backend-native +scheduler—must fit behind that seam while preserving the same request, +readback, history, time, and conformance contracts. + +Existing backends that do not claim portable autonomous execution remain +valid. A backend that already claims `supports_autonomous_execution` must add +the new contract ids, relational bindings, lifecycle/readback support, and live +evidence before the released capability can remain truthful; silent inference +or compatibility fallback is forbidden. Follow ADR-061/075 for additive versus +breaking schema/profile changes and migration notice. + +Do not create a new backend profile merely to name this optional capability. +Extend the existing conditional participant-runtime capability/evidence checks +and the `FULL_REMOTE_CONTROL_PLANE` live-probe path when the manifest claims +autonomous execution. A profile or schema file is not execution evidence. + +There is no Ground Control requirement UID to invent. Traceability must join +issue #898, the ADR-092 amendment, normative contracts/specification, manifest +claim, runtime and backend implementation, focused/live/negative conformance +cases, PR, released package, migration guidance, and the external consumer +`autarchy-ai/penumbra-scenarios#556`. Release-please owns `CHANGELOG.md` and +package versioning. + +## Gotchas And Anti-Patterns + +Avoid: + +- a `participant_executor` target component, pack-owned scheduler/control API, + second action request, second time service, or second operation store; +- treating episode initialize/reset/restart/terminate as executor + start/reset/teardown; +- treating RUN-310 supervisory control intents as operational scheduler + lifecycle; +- inferring action-to-target support from separate lists or treating every + supported action as executable against every supported target; +- discovering adapters from action names, ATT&CK/tool labels, URLs, environment + variables, imports, reflection, or native resource types; +- claiming concurrency from `max_in_flight`, thread count, same-tick execution, + or overlapping log timestamps without RUN-308 evidence; +- concurrent mutation or last-writer-wins merging of whole runtime snapshots; +- holding a global lock across unbounded native service I/O; +- accepting stale completions after reset/teardown, or replaying indeterminate + native actions after a timeout or transport error; +- allowing executor pause to advance past due shared-time coordinates without + a governed disposition and pacing-deviation evidence; +- treating health, readiness, receipt acceptance, action outcome, episode + status, and control-plane operation status as synonyms; +- treating drain as cancellation, teardown as scenario-resource deletion, or + snapshot replacement as native rollback; +- placing credentials, adapter endpoints, raw service state, native ids, + commands, argv, stdout/stderr, tracebacks, or environment dumps in portable + artifacts; and +- adding duplicate schemas, validators, exception hierarchies, repositories, + audit logs, fixture runners, conformance engines, or release workflows. + +## Non-Goals And Implementation Boundaries + +- No Penumbra, KeplerOps, product, service, deployment, pack, historical-data, + or private scheduler semantics enter RAES. +- No new SDL actor, action, target-service, clock, calendar, workflow, or + evaluation authority is introduced. +- RAES specifies portable bindings, lifecycle meaning, control/readback, and + evidence; each backend owns adapter implementation, worker topology, + transport, deployment, credential resolution, and native resource cleanup. +- The reference runtime need not prove production throughput, human realism, + exactly-once native side effects, native rollback, or universal concurrency + equivalence. +- This issue does not redesign participant episodes, supervisory control, + shared-time semantics, participant implementation identity, scoring, + objectives, experiment allocation, or scenario-service lifecycle. +- Default verification remains hermetic and unprivileged. Real service/daemon + proofs are explicit opt-in evidence and cannot replace portable negative and + state-machine conformance tests. diff --git a/docs/explain/reference/backend-conformance.md b/docs/explain/reference/backend-conformance.md index 2db47958e..7908d6be7 100644 --- a/docs/explain/reference/backend-conformance.md +++ b/docs/explain/reference/backend-conformance.md @@ -142,9 +142,14 @@ capability. A backend that includes `autonomous_execution` in its supported selection strategies, exact action contracts, observation boundaries, target addresses, policy profiles, and positive finite limits for participants, attempts, in-flight actions, occurrences, retries per occurrence, -and burst size. The planner also compares the parent behavior +and burst size. It must also publish relational execution bindings, all six +generation-fenced lifecycle controls, bounded-concurrency support, and finite +execution-service and concurrent-action limits. The planner compares each +compiled action-to-target relation rather than accepting the Cartesian product +of separate action and target lists. The planner also compares the parent behavior specification's required feature set with the runtime capability. Runtime -target registration requires the autonomous native-binding method. This is +target registration requires the autonomous native-binding, +execution-control/readback, and bounded-batch methods. This is admission evidence only: conformance also requires the backend participant runtime to invoke its native service adapter, return a typed terminal action outcome at the bound temporal coordinate distinct from control-operation @@ -154,7 +159,13 @@ must be reachable. The portable runtime drives real-time and dilated participant cadence; externally paced autonomous execution is not admissible until a portable transition-notification contract is governed. Durable readback must agree across scheduler policy identity, clock segment/lifecycle, and live -participant episode. +participant episode. Conditional live conformance drives two bounded native +actions plus start, pause, resume, bounded drain, reset, stale-generation +fencing, and teardown; inert method implementations or missing transition +evidence fail. The shared participant base exposes readback only; it cannot +make a backend lifecycle claim pass. Each successful control operation must +come from the backend handler and produce changed action-specific readback and +new evidence. The explicit `participant-autonomous-execution/v2` profile additionally requires exact support for all governed activity features, `weighted` diff --git a/docs/explain/reference/shared-semantic-integrity.md b/docs/explain/reference/shared-semantic-integrity.md index 34535f35c..0afa7b79b 100644 --- a/docs/explain/reference/shared-semantic-integrity.md +++ b/docs/explain/reference/shared-semantic-integrity.md @@ -260,4 +260,4 @@ so they are tracked by their own requirements, not here. | 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 | | 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`, `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_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_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.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/lineage.md b/docs/explain/sdl/lineage.md index 3b3b0a563..3529c9017 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -908,6 +908,17 @@ which dynamic queue/log/config details remain evidence or bounded settings. Historical files remain ordinary initial service state; injects remain exercise orchestration; stochastic participant implementations remain governed run apparatus. +- Issue #898 extends that same DSL-437 lineage across a remote backend control + boundary. It keeps authored behavior, participant episodes, scheduler + continuation, execution-service lifecycle, shared time, native action + outcome, and control-plane operation status distinct. FMI Scheduled + Execution, gRPC health checking, Google long-running operations, Kubernetes + generation-bound conditions, and TOSCA lifecycle interfaces are operational + design precedents only. RAES retains its own contracts and requires exact + action-to-target bindings, generation-checked lifecycle/readback, bounded + RUN-308 concurrency evidence, existing operation receipts/statuses, and + existing time-state/provenance. It claims no wire, API, lifecycle-token, or + behavioral compatibility with those sources. - Issues #810 through #813 own the opacity/supervisor-visibility, proof-bearing bisimulation, adversarial threat-model, and simulation/federation extensions. SEM-230 preserves their participant, diff --git a/docs/explain/sdl/runtime-architecture.md b/docs/explain/sdl/runtime-architecture.md index 39fa471fb..fdf20a9b3 100644 --- a/docs/explain/sdl/runtime-architecture.md +++ b/docs/explain/sdl/runtime-architecture.md @@ -35,6 +35,15 @@ honor. Those choices are not just incidental implementation details: they shape experiment validity and must be declared and preserved as part of run provenance when different realizations are compared. +Autonomous participant execution follows the same separation. Compilation +preserves exact action-to-target-service bindings; planning admits those +relations against the backend manifest; and the runtime owns a typed, +generation-fenced execution-service scope. Native calls may overlap within a +finite admitted bound, but portable snapshot/history commits are serialized +with revision checks. Shared-clock pause, resume, and reset coordinate service +readiness and generation, while pacing loss is published as degraded, +not-ready state with an explicit deviation reference. + Under the repository's [coding standards](../reference/coding-standards.md), this layer is where `FM2` and `FM3` work becomes most relevant. The formalization target here is not raw YAML, but the typed runtime model, diff --git a/docs/migration/README.md b/docs/migration/README.md index e30385409..d4cd38c6c 100644 --- a/docs/migration/README.md +++ b/docs/migration/README.md @@ -14,6 +14,10 @@ The explicit opt-in from fixed-cadence autonomous participant execution to the governed activity profile is documented in [Autonomous Execution V2 Migration](autonomous-execution-v2.md). +Backends that declare autonomous participant execution must adopt the portable +execution-service surface described in +[Participant Execution Control Migration](participant-execution-control.md). + The reorganization moved existing material into the current long-term buckets: - root `schemas/` -> `contracts/schemas/` diff --git a/docs/migration/participant-execution-control.md b/docs/migration/participant-execution-control.md new file mode 100644 index 000000000..f9bbfa6fc --- /dev/null +++ b/docs/migration/participant-execution-control.md @@ -0,0 +1,37 @@ +# Participant Execution Control Migration + +Issue #898 strengthens `capabilities.participant_runtime` for every backend +that declares `autonomous_execution`. Backends that do not make that claim are +unchanged. + +An autonomous backend must now: + +- replace independent action/target support as its admission authority with + exact `execution_bindings`; +- declare all `start`, `pause`, `resume`, `drain`, `reset`, and `teardown` + controls; +- declare bounded concurrency with positive service capacity and at least two + concurrent actions; +- implement native binding, lifecycle mutation/readback, and bounded batch + methods; and +- publish the three participant execution contract ids plus operation + receipt/status and runtime snapshot support. + +Do not inherit or emulate lifecycle success by editing the portable snapshot. +The backend control method must perform and observe scheduler/shared-time +coordination, bounded drain, reset, and resource release. RAES rejects a +successful return whose readback did not change to the action-specific state +or did not add operation and evidence references. + +Action requests are generation bound. Reset and shared-clock reset increment +the generation; stale queued work and stale native completions are rejected. +Pause stops new admission, bounded drain requires zero reserved/in-flight work, +and teardown releases resources. A wall-pacing failure is visible as degraded, +not-ready, paused service state with a pacing-deviation evidence reference. + +Existing scenario SDL and both autonomous policy profiles remain valid. The +change is backend-facing and fail closed: a previous autonomous manifest +without execution bindings and lifecycle/concurrency declarations is rejected +during manifest validation or planning. Conditional target conformance also +executes two native actions and the lifecycle, so adding placeholder methods is +not sufficient. diff --git a/docs/research/lineage/source-audit-2026-07-12.md b/docs/research/lineage/source-audit-2026-07-12.md index 9613fd360..822b56967 100644 --- a/docs/research/lineage/source-audit-2026-07-12.md +++ b/docs/research/lineage/source-audit-2026-07-12.md @@ -273,6 +273,42 @@ Accordingly, NumPy, Random123, and stream-splitting precedents remain design criteria rather than source compatibility or copied-code claims. No additional license notice is required by this extension. +### Issue 898 portable execution-control review + +The DSL-437 participant and simulation-time sources above were re-reviewed on +2026-07-26 for issue #898. The following operational-control precedents add +design criteria without changing the existing semantic derivation: + +- Modelica Association Project FMI, *Functional Mock-up Interface + Specification* 3.0.2 (2024), , + separates importer-controlled activation, legal lifecycle states, and + quiescent termination, while explicitly leaving parallel computation outside + the FMI API. +- gRPC, *Health Checking*, + , separates + implementation-maintained service health from ordinary application calls. +- Google, *AIP-151: Long-running operations*, + , uses a shared operation resource and makes + parallel-operation behavior explicit. +- Kubernetes, *Pod Conditions*, + , binds + observations to an `observedGeneration`, so a consumer can detect stale + readiness. +- OASIS, *Topology and Orchestration Specification for Cloud Applications + Version 2.0* (2024), + , + separates portable lifecycle interface operations from the implementation + artifacts that realize them. + +RAES adapts only the separation of scheduler activation, lifecycle legality, +generation-bound readback, reusable operation status, health/readiness, and +backend-owned implementations. It does not adopt an FMI importer/FMU API, gRPC +service, Google operation schema, Kubernetes resource model, TOSCA topology or +workflow language, or any source lifecycle vocabulary or wire compatibility. +Bounded concurrent native participant execution, coordinated drain/reset, and +action-to-target evidence remain RAES-defined obligations under ADR-092, +ADR-054/RUN-308, ADR-091, and issue #898. + ## CRACK Publications Two related works by Russo, Costa, and Armando are distinct and must not share diff --git a/implementations/python/packages/raes_backend_protocols/capabilities.py b/implementations/python/packages/raes_backend_protocols/capabilities.py index 98551b6da..34ab01d8b 100644 --- a/implementations/python/packages/raes_backend_protocols/capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/capabilities.py @@ -18,6 +18,7 @@ PARTICIPANT_RUNTIME_POLICY_FEATURES = _participant_capabilities.PARTICIPANT_RUNTIME_POLICY_FEATURES PARTICIPANT_RUNTIME_ROLE_SCOPE = _participant_capabilities.PARTICIPANT_RUNTIME_ROLE_SCOPE ParticipantFeatureSupport = _participant_capabilities.ParticipantFeatureSupport +ParticipantExecutionBinding = _participant_capabilities.ParticipantExecutionBinding ParticipantRuntimeCapabilities = _participant_capabilities.ParticipantRuntimeCapabilities PROVISIONER_DOMAIN_PROFILE_SCOPE = _provisioner_capabilities.PROVISIONER_DOMAIN_PROFILE_SCOPE ProvisionerCapabilities = _provisioner_capabilities.ProvisionerCapabilities diff --git a/implementations/python/packages/raes_backend_protocols/capability_admission.py b/implementations/python/packages/raes_backend_protocols/capability_admission.py index fd64480ad..32e11c867 100644 --- a/implementations/python/packages/raes_backend_protocols/capability_admission.py +++ b/implementations/python/packages/raes_backend_protocols/capability_admission.py @@ -37,6 +37,7 @@ class AutonomousExecutionPolicy(Protocol): action_candidate_max_retries: tuple[int, ...] max_occurrences: int max_burst_size: int + execution_bindings: tuple[object, ...] _V2_ACTIVITY_FEATURES = frozenset( @@ -164,6 +165,37 @@ def _unsupported_autonomous_value_gaps( return gaps +def _autonomous_execution_binding_gaps( + capability: ParticipantRuntimeCapabilities, + policies: tuple[AutonomousExecutionPolicy, ...], +) -> list[str]: + gaps: list[str] = [] + declared = tuple(capability.execution_bindings) + for policy in policies: + for required in policy.execution_bindings: + matching = [ + binding + for binding in declared + if binding.action_contract_address == required.action_contract_address + and binding.participant_implementation_ref == required.participant_implementation_ref + ] + exact = [ + binding + for binding in matching + if set(binding.target_addresses) == set(required.target_addresses) + and binding.max_action_attempts >= required.max_action_attempts + and binding.max_in_flight >= required.max_in_flight + ] + if exact: + continue + required_targets = ", ".join(required.target_addresses) + gaps.append( + "unsupported autonomous execution binding for " + f"{required.action_contract_address} targets: {required_targets}" + ) + return gaps + + def _requires_coordinated_reset( policies: tuple[AutonomousExecutionPolicy, ...], time_model: object, @@ -207,6 +239,12 @@ def participant_autonomous_execution_capability_gaps( elif normalized_policies and capability is not None: gaps.extend(_autonomous_limit_gaps(capability, normalized_policies)) gaps.extend(_unsupported_autonomous_value_gaps(capability, normalized_policies)) + gaps.extend( + _autonomous_execution_binding_gaps( + capability, + normalized_policies, + ) + ) gaps.extend(_autonomous_reset_gaps(manifest, normalized_policies, time_model)) return tuple(gaps) diff --git a/implementations/python/packages/raes_backend_protocols/manifest.py b/implementations/python/packages/raes_backend_protocols/manifest.py index 22785d93e..e2da840b4 100644 --- a/implementations/python/packages/raes_backend_protocols/manifest.py +++ b/implementations/python/packages/raes_backend_protocols/manifest.py @@ -36,6 +36,10 @@ ProvisionerCapabilities, TimeCapabilities, ) +from .participant_execution_manifest import ( + participant_execution_capability_kwargs, + participant_execution_capability_payload, +) class BackendManifestEnvelopeUnsupportedError(ValueError): @@ -195,6 +199,7 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod manifest.participant_runtime.max_autonomous_retries_per_occurrence ), "max_autonomous_burst_size": manifest.participant_runtime.max_autonomous_burst_size, + **participant_execution_capability_payload(manifest.participant_runtime), "constraints": dict(manifest.participant_runtime.constraints), } if manifest.participant_runtime is not None @@ -366,6 +371,7 @@ def _participant_runtime_from_model( max_autonomous_occurrences=model.max_autonomous_occurrences, max_autonomous_retries_per_occurrence=model.max_autonomous_retries_per_occurrence, max_autonomous_burst_size=model.max_autonomous_burst_size, + **participant_execution_capability_kwargs(model), constraints=dict(model.constraints), ) diff --git a/implementations/python/packages/raes_backend_protocols/participant_action_commit.py b/implementations/python/packages/raes_backend_protocols/participant_action_commit.py index 0a6182edf..10ae3e245 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_action_commit.py +++ b/implementations/python/packages/raes_backend_protocols/participant_action_commit.py @@ -19,6 +19,9 @@ def participant_binding_post_state_digest(request: ParticipantActionAdmissionReq request.action_contract_address, request.observation_boundary_address, request.action_instance_id, + *request.target_addresses, + request.execution_scope_ref or "", + (str(request.execution_generation) if request.execution_generation is not None else ""), ) ) return "sha256:" + sha256(digest_input.encode("utf-8")).hexdigest() diff --git a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py index aa7da1ff6..6078c15b0 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -13,6 +13,8 @@ ) from raes_contracts.vocabulary import ParticipantFeatureSupportLevel +PARTICIPANT_EXECUTION_CONTROL_ACTIONS = frozenset({"start", "pause", "resume", "drain", "reset", "teardown"}) + def _validate_unique_non_empty_strings(field_name: str, values: tuple[str, ...]) -> None: if any(not value.strip() for value in values): @@ -92,6 +94,58 @@ def __post_init__(self) -> None: object.__setattr__(self, "evidence_refs", evidence_refs) +@dataclass(frozen=True) +class ParticipantExecutionBinding: + """Manifest claim for one exact action-to-target native binding.""" + + binding_id: str + action_contract_address: str + target_addresses: tuple[str, ...] + participant_implementation_ref: str + constraint_refs: tuple[str, ...] + evidence_refs: tuple[str, ...] + max_action_attempts: int + max_in_flight: int + timeout_seconds: int + max_retries: int + + def __post_init__(self) -> None: + for field_name in ( + "binding_id", + "action_contract_address", + "participant_implementation_ref", + ): + if not str(getattr(self, field_name)).strip(): + raise ValueError(f"ParticipantExecutionBinding.{field_name} must be non-empty") + for field_name in ("target_addresses", "constraint_refs", "evidence_refs"): + values = tuple(getattr(self, field_name)) + if not values: + raise ValueError(f"ParticipantExecutionBinding.{field_name} must not be empty") + _validate_unique_non_empty_strings( + f"ParticipantExecutionBinding.{field_name}", + values, + ) + object.__setattr__(self, field_name, values) + require_compiled_address( + self.action_contract_address, + field_name="action_contract_address", + ) + for target_address in self.target_addresses: + require_compiled_address( + target_address, + field_name="target_addresses", + ) + for field_name in ( + "max_action_attempts", + "max_in_flight", + "timeout_seconds", + ): + if getattr(self, field_name) < 1: + raise ValueError(f"ParticipantExecutionBinding.{field_name} must be positive") + if self.max_retries < 0: + raise ValueError("ParticipantExecutionBinding.max_retries must be non-negative") + + @dataclass(frozen=True) class ParticipantRuntimeCapabilities: """Backend participant lifecycle, behavior, and execution support.""" @@ -115,6 +169,12 @@ class ParticipantRuntimeCapabilities: max_autonomous_occurrences: int | None = None max_autonomous_retries_per_occurrence: int | None = None max_autonomous_burst_size: int | None = None + execution_bindings: tuple[ParticipantExecutionBinding, ...] = () + supports_execution_control: bool = False + supported_execution_control_actions: frozenset[str] = frozenset() + supports_bounded_concurrency: bool = False + max_execution_services: int | None = None + max_concurrent_actions: int | None = None constraints: dict[str, str] = field(default_factory=dict) def __post_init__(self) -> None: @@ -123,6 +183,11 @@ def __post_init__(self) -> None: self._validate_required_vocabularies() feature_support = self._normalize_feature_support() self._validate_feature_support(feature_support) + execution_bindings = tuple( + binding if isinstance(binding, ParticipantExecutionBinding) else ParticipantExecutionBinding(**binding) + for binding in self.execution_bindings + ) + object.__setattr__(self, "execution_bindings", execution_bindings) self._validate_autonomous_execution() object.__setattr__(self, "feature_support", feature_support) @@ -212,10 +277,41 @@ def _validate_enabled_autonomous_execution(self) -> None: if not self.supported_autonomous_random_stream_profiles: raise ValueError("autonomous execution v2 requires exact supported random-stream profiles") self._validate_autonomous_addresses() + self._validate_execution_control() for label, value in self._autonomous_limits(): if value is None or value < 1: raise ValueError(f"autonomous execution requires positive {label}") + def _validate_execution_control(self) -> None: + if not self.supports_execution_control: + raise ValueError("autonomous execution requires execution control support") + missing_actions = PARTICIPANT_EXECUTION_CONTROL_ACTIONS - self.supported_execution_control_actions + if missing_actions: + raise ValueError("execution control is missing required actions: " + ", ".join(sorted(missing_actions))) + unknown_actions = self.supported_execution_control_actions - PARTICIPANT_EXECUTION_CONTROL_ACTIONS + if unknown_actions: + raise ValueError("unsupported execution control actions: " + ", ".join(sorted(unknown_actions))) + if not self.supports_bounded_concurrency: + raise ValueError("autonomous execution requires bounded concurrency support") + if self.max_execution_services is None or self.max_execution_services < 1: + raise ValueError("autonomous execution requires positive max_execution_services") + if self.max_concurrent_actions is None or self.max_concurrent_actions < 2: + raise ValueError("bounded concurrency requires max_concurrent_actions of at least 2") + if not self.execution_bindings: + raise ValueError("autonomous execution requires relational execution_bindings") + binding_ids = tuple(binding.binding_id for binding in self.execution_bindings) + _validate_unique_non_empty_strings( + "ParticipantRuntimeCapabilities.execution_bindings", + binding_ids, + ) + for binding in self.execution_bindings: + if binding.action_contract_address not in self.supported_autonomous_action_contracts: + raise ValueError("execution binding action is not declared supported") + if not set(binding.target_addresses).issubset(self.supported_autonomous_target_addresses): + raise ValueError("execution binding target is not declared supported") + if binding.max_in_flight > self.max_concurrent_actions: + raise ValueError("execution binding exceeds max_concurrent_actions") + def _validate_autonomous_addresses(self) -> None: for field_name, addresses in ( ("supported_autonomous_action_contracts", self.supported_autonomous_action_contracts), @@ -244,16 +340,24 @@ def _has_autonomous_configuration(self) -> bool: or self.supported_autonomous_policy_profiles or self.supported_autonomous_activity_features or self.supported_autonomous_random_stream_profiles + or self.execution_bindings + or self.supports_execution_control + or self.supported_execution_control_actions + or self.supports_bounded_concurrency + or self.max_execution_services is not None + or self.max_concurrent_actions is not None or any(value is not None for _, value in self._autonomous_limits()) ) __all__ = [ + "PARTICIPANT_EXECUTION_CONTROL_ACTIONS", "PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE", "PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS", "PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE", "PARTICIPANT_RUNTIME_POLICY_FEATURES", "PARTICIPANT_RUNTIME_ROLE_SCOPE", "ParticipantFeatureSupport", + "ParticipantExecutionBinding", "ParticipantRuntimeCapabilities", ] diff --git a/implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py b/implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py new file mode 100644 index 000000000..0420a3ac6 --- /dev/null +++ b/implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py @@ -0,0 +1,67 @@ +"""Wire projection helpers for participant execution capabilities.""" + +from raes_contracts.contracts.participant_execution import ParticipantExecutionBindingModel + +from .participant_capabilities import ParticipantExecutionBinding, ParticipantRuntimeCapabilities + + +def participant_execution_capability_payload( + capability: ParticipantRuntimeCapabilities, +) -> dict[str, object]: + """Project portable execution claims into a manifest payload.""" + + return { + "execution_bindings": [ + ParticipantExecutionBindingModel( + binding_id=binding.binding_id, + action_contract_address=binding.action_contract_address, + target_addresses=binding.target_addresses, + participant_implementation_ref=binding.participant_implementation_ref, + constraint_refs=binding.constraint_refs, + evidence_refs=binding.evidence_refs, + max_action_attempts=binding.max_action_attempts, + max_in_flight=binding.max_in_flight, + timeout_seconds=binding.timeout_seconds, + max_retries=binding.max_retries, + ) + for binding in capability.execution_bindings + ], + "supports_execution_control": capability.supports_execution_control, + "supported_execution_control_actions": sorted(capability.supported_execution_control_actions), + "supports_bounded_concurrency": capability.supports_bounded_concurrency, + "max_execution_services": capability.max_execution_services, + "max_concurrent_actions": capability.max_concurrent_actions, + } + + +def participant_execution_capability_kwargs(model: object) -> dict[str, object]: + """Restore portable execution claims from a validated manifest model.""" + + return { + "execution_bindings": tuple( + ParticipantExecutionBinding( + binding_id=binding.binding_id, + action_contract_address=binding.action_contract_address, + target_addresses=tuple(binding.target_addresses), + participant_implementation_ref=binding.participant_implementation_ref, + constraint_refs=tuple(binding.constraint_refs), + evidence_refs=tuple(binding.evidence_refs), + max_action_attempts=binding.max_action_attempts, + max_in_flight=binding.max_in_flight, + timeout_seconds=binding.timeout_seconds, + max_retries=binding.max_retries, + ) + for binding in model.execution_bindings + ), + "supports_execution_control": model.supports_execution_control, + "supported_execution_control_actions": frozenset(model.supported_execution_control_actions), + "supports_bounded_concurrency": model.supports_bounded_concurrency, + "max_execution_services": model.max_execution_services, + "max_concurrent_actions": model.max_concurrent_actions, + } + + +__all__ = [ + "participant_execution_capability_kwargs", + "participant_execution_capability_payload", +] diff --git a/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py new file mode 100644 index 000000000..029586d00 --- /dev/null +++ b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py @@ -0,0 +1,89 @@ +"""Portable execution-service behavior shared by participant runtimes.""" + +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor + +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionServiceStateModel, +) +from raes_contracts.participant_binding import ( + ParticipantActionAdmissionRequest, + ParticipantActionApplyResult, +) +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot + +from .participant_action_commit import reject_participant_action_outcome +from .participant_execution_service import participant_execution_state + + +class ParticipantExecutionRuntimeMixin: + """Generation fencing, lifecycle readback, and bounded native dispatch.""" + + def execution_state( + self, + execution_scope_ref: str, + snapshot: RuntimeSnapshot, + ) -> ParticipantExecutionServiceStateModel: + """Return typed health/readiness and lifecycle readback.""" + + return participant_execution_state(execution_scope_ref, snapshot) + + @staticmethod + def _execution_generation_failure( + request: ParticipantActionAdmissionRequest, + snapshot: RuntimeSnapshot, + *, + completion: bool, + predecessor: RuntimeSnapshot | None = None, + ) -> ParticipantActionApplyResult | None: + scope = request.execution_scope_ref + if scope is None: + return None + payload = snapshot.participant_execution_services.get(scope) + expected = request.execution_generation + reason = None + if payload is None: + reason = "execution-service state is missing" + else: + state = ParticipantExecutionServiceStateModel.model_validate(payload) + if state.generation != expected or state.observed_generation != expected: + reason = "execution generation changed" + elif not completion and ( + state.observed_lifecycle != "running" or not state.accepting_new_work or state.readiness != "ready" + ): + reason = "execution service is not accepting work" + if reason is None: + return None + phase = "completion" if completion else "work" + original = predecessor or snapshot + return reject_participant_action_outcome( + original, + ApplyResult(success=False, snapshot=snapshot), + request.participant_address, + f"runtime.participant-execution-stale-{phase}", + (f"Participant action {phase} was rejected because its generation-bound {reason}."), + ) + + def admit_actions_concurrently( + self, + requests: tuple[ParticipantActionAdmissionRequest, ...], + snapshot: RuntimeSnapshot, + max_workers: int, + ) -> tuple[ParticipantActionApplyResult, ...]: + """Execute independent native actions with a finite worker bound. + + Each call receives the same immutable predecessor. The scheduler owns + revision-checked serialized merge and scheduler-state commit. + """ + + if max_workers < 2: + raise ValueError("concurrent participant execution requires at least two workers") + if len(requests) > max_workers: + raise ValueError("participant action batch exceeds its worker bound") + with ThreadPoolExecutor( + max_workers=max_workers, + thread_name_prefix="raes-participant", + ) as executor: + futures = [executor.submit(self.admit_action, request, snapshot) for request in requests] + return tuple(future.result() for future in futures) diff --git a/implementations/python/packages/raes_backend_protocols/participant_execution_service.py b/implementations/python/packages/raes_backend_protocols/participant_execution_service.py new file mode 100644 index 000000000..20edf6e47 --- /dev/null +++ b/implementations/python/packages/raes_backend_protocols/participant_execution_service.py @@ -0,0 +1,21 @@ +"""Typed participant execution-service readback.""" + +from __future__ import annotations + +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel +from raes_contracts.runtime_state import RuntimeSnapshot + + +def participant_execution_state( + execution_scope_ref: str, + snapshot: RuntimeSnapshot, +) -> ParticipantExecutionServiceStateModel: + """Read backend-observed state without synthesizing lifecycle success.""" + + payload = snapshot.participant_execution_services.get(execution_scope_ref) + if payload is None: + raise ValueError("participant execution scope is not configured") + return ParticipantExecutionServiceStateModel.model_validate(payload) + + +__all__ = ["participant_execution_state"] diff --git a/implementations/python/packages/raes_backend_protocols/participant_runtime_base.py b/implementations/python/packages/raes_backend_protocols/participant_runtime_base.py index 9d6b83216..d7d2de57c 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_runtime_base.py +++ b/implementations/python/packages/raes_backend_protocols/participant_runtime_base.py @@ -38,6 +38,7 @@ participant_binding_post_state_digest, reject_participant_action_outcome, ) +from .participant_execution_runtime import ParticipantExecutionRuntimeMixin from .participant_reset import reset_many_atomically _EMPTY_ADDRESS_MSG = "participant_address must be non-empty" @@ -54,7 +55,7 @@ def _now_iso() -> str: return datetime.now(UTC).isoformat().replace("+00:00", "Z") -class BaseParticipantRuntime: +class BaseParticipantRuntime(ParticipantExecutionRuntimeMixin): """Shared RUN-311 episode lifecycle base for RAES participant runtimes. Implements the full episode state machine (initialize, reset, restart, @@ -266,6 +267,12 @@ def admit_action( "cannot admit participant action for {address!r}: no live episode", ) failure = self._action_precondition_failure(predecessor, snapshot, address) + if failure is None: + failure = self._execution_generation_failure( + request, + snapshot, + completion=False, + ) native = None if failure is None: current_state = predecessor diff --git a/implementations/python/packages/raes_backend_protocols/protocols.py b/implementations/python/packages/raes_backend_protocols/protocols.py index e02da65fc..06e5bc017 100644 --- a/implementations/python/packages/raes_backend_protocols/protocols.py +++ b/implementations/python/packages/raes_backend_protocols/protocols.py @@ -4,7 +4,13 @@ from typing import TYPE_CHECKING, Protocol -from raes_contracts.contracts import ParticipantTemporalRuntimeContextModel +from raes_contracts.contracts import ( + ParticipantTemporalRuntimeContextModel, +) +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_binding import ParticipantActionAdmissionRequest, ParticipantActionApplyResult from raes_contracts.participant_episode import ( @@ -193,6 +199,39 @@ def bind_autonomous_action( ... +class ControlledAutonomousParticipantRuntime(AutonomousParticipantRuntime, Protocol): + """Autonomous runtime with portable lifecycle, readback, and bounded overlap.""" + + def control_execution( + self, + request: ParticipantExecutionControlRequestModel, + snapshot: RuntimeSnapshot, + ) -> ApplyResult: + """Perform and observe one generation-fenced native lifecycle operation. + + Implementations own scheduler and shared-time coordination for the + scope. Merely editing portable readback fields is non-conformant. + """ + ... + + def execution_state( + self, + execution_scope_ref: str, + snapshot: RuntimeSnapshot, + ) -> ParticipantExecutionServiceStateModel: + """Return typed execution-service health and readiness.""" + ... + + def admit_actions_concurrently( + self, + requests: tuple[ParticipantActionAdmissionRequest, ...], + snapshot: RuntimeSnapshot, + max_workers: int, + ) -> tuple[ParticipantActionApplyResult, ...]: + """Execute native actions with a finite overlap bound.""" + ... + + class TimeRuntime(Protocol): """Materializes and controls one admitted portable shared-time model.""" diff --git a/implementations/python/packages/raes_conformance/conformance/participant_execution_probes.py b/implementations/python/packages/raes_conformance/conformance/participant_execution_probes.py new file mode 100644 index 000000000..756b299e7 --- /dev/null +++ b/implementations/python/packages/raes_conformance/conformance/participant_execution_probes.py @@ -0,0 +1,295 @@ +"""Conditional autonomous participant execution conformance probes.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import replace + +from raes_contracts.contracts import ( + ParticipantTemporalRuntimeContextModel, +) +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import RuntimeSnapshot +from raes_runtime.control_plane import RuntimeControlPlane +from raes_runtime.registry import RuntimeTarget + +from raes_conformance.conformance.diagnostics import _diagnostic +from raes_conformance.conformance.report import ConformanceCaseResult + + +def _probe_digest(payload: object) -> str: + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) + return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def _participant_execution_probe_snapshot( + target: RuntimeTarget, +) -> RuntimeSnapshot: + capability = target.manifest.participant_runtime + if capability is None or not capability.supports_autonomous_execution: + return RuntimeSnapshot() + scope = "participant.autonomous-execution.conformance" + state = ParticipantExecutionServiceStateModel( + execution_scope_ref=scope, + policy_address=scope, + desired_lifecycle="stopped", + observed_lifecycle="stopped", + generation=0, + observed_generation=0, + health="healthy", + readiness="not_ready", + accepting_new_work=False, + draining=False, + quiescent=True, + resources_released=False, + policy_digest=_probe_digest({"scope": scope}), + binding_digest=_probe_digest( + [ + { + "action": binding.action_contract_address, + "targets": binding.target_addresses, + } + for binding in capability.execution_bindings + ] + ), + time_declaration_digest=_probe_digest({"clock": "time.clock.conformance"}), + scheduler_state_refs=( + f"{scope}.state.participant.conformance", + f"{scope}.state.participant.conformance-2", + ), + capacity=capability.max_concurrent_actions or 2, + reserved=0, + in_flight=0, + last_transition_ref="operation:participant-execution:configure:0", + evidence_refs=("conformance:participant-execution:configured",), + ) + return RuntimeSnapshot(participant_execution_services={scope: state.model_dump(mode="json")}) + + +def _participant_execution_operation_case( + control_plane: RuntimeControlPlane, + *, + scope: str, + action: str, + generation: int, + timeout_seconds: int | None = None, +) -> ConformanceCaseResult: + request = ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action=action, + expected_generation=generation, + timeout_seconds=timeout_seconds, + ) + receipt = control_plane.control_participant_execution(request) + status = control_plane.get_operation(receipt.operation_id) + diagnostics: list[Diagnostic] = [] + if status is None or status.state.value != "succeeded": + diagnostics.append( + _diagnostic( + "conformance.participant-execution-lifecycle-failed", + f"runtime.control-plane.participant-execution.{scope}", + f"Participant execution {action!r} did not complete successfully.", + ) + ) + else: + try: + state = control_plane.participant_execution_state(scope) + except (TypeError, ValueError) as exc: + diagnostics.append( + _diagnostic( + "conformance.participant-execution-readback-missing", + f"runtime.snapshot.participant-execution-services.{scope}", + f"Participant execution readback failed: {exc}", + ) + ) + else: + if not state.evidence_refs or not state.last_transition_ref: + diagnostics.append( + _diagnostic( + "conformance.participant-execution-evidence-missing", + f"runtime.snapshot.participant-execution-services.{scope}", + "Participant execution lifecycle readback lacks transition evidence.", + ) + ) + return ConformanceCaseResult( + name=f"participant-execution-{action}", + contract_name="participant-execution-service-state-v1", + valid=True, + passed=not diagnostics, + diagnostics=tuple(diagnostics), + expected_operations=(action,), + accounted_operations=(action,) if not diagnostics else (), + ) + + +def _participant_execution_action_case( + target: RuntimeTarget, + control_plane: RuntimeControlPlane, + *, + scope: str, +) -> ConformanceCaseResult: + runtime = target.participant_runtime + capability = target.manifest.participant_runtime + diagnostics: list[Diagnostic] = [] + evidence_refs: tuple[str, ...] = () + if runtime is None or capability is None or not capability.execution_bindings: + diagnostics.append( + _diagnostic( + "conformance.participant-execution-binding-missing", + "capabilities.participant_runtime.execution_bindings", + "Autonomous execution requires at least one executable binding.", + ) + ) + else: + binding = capability.execution_bindings[0] + evidence_refs = binding.evidence_refs + participants = ( + "participant.conformance", + "participant.conformance-2", + ) + temporal_contexts = ( + ParticipantTemporalRuntimeContextModel( + temporal_contract_id="time.constraint.conformance", + time_domain="scenario_time", + clock_authority="time.clock.conformance", + event_points=["submit", "start", "end", "observed"], + observation_point="time.clock.conformance@segment=0,tick=0", + reset_boundary="time.clock.conformance:segment=0", + ), + ) + try: + control_plane.initialize_participant_episode(participants[1]) + requests = [] + for ordinal, participant_address in enumerate(participants): + request = runtime.bind_autonomous_action( + participant_address, + binding.action_contract_address, + next(iter(capability.supported_autonomous_observation_boundaries)), + binding.participant_implementation_ref, + f"participant-execution-conformance-{ordinal}", + temporal_contexts, + control_plane.snapshot, + ) + requests.append( + replace( + request, + participant_address=participant_address, + action_contract_address=binding.action_contract_address, + action_instance_id=(f"participant-execution-conformance-{ordinal}"), + requires_terminal_outcome=True, + target_addresses=binding.target_addresses, + execution_scope_ref=scope, + execution_generation=0, + ) + ) + results = runtime.admit_actions_concurrently( + tuple(requests), + control_plane.snapshot, + 2, + ) + except Exception as exc: + diagnostics.append( + _diagnostic( + "conformance.participant-execution-action-failed", + f"runtime.participant-execution.{scope}", + (f"Bounded native action execution raised {type(exc).__name__}: {exc}"), + ) + ) + else: + if len(results) != 2 or any( + not result.success + or result.action_result is None + or not result.snapshot.participant_behavior_history.get(request.participant_address) + for request, result in zip(requests, results, strict=True) + ): + diagnostics.append( + _diagnostic( + "conformance.participant-execution-action-inert", + f"runtime.participant-execution.{scope}", + ( + "Declared bounded execution did not produce two " + "typed native outcomes with behavior evidence." + ), + ) + ) + return ConformanceCaseResult( + name="participant-execution-bounded-native-actions", + contract_name="participant-execution-binding-v1", + valid=True, + passed=not diagnostics, + diagnostics=tuple(diagnostics), + expected_operations=("admit-action", "admit-action"), + accounted_operations=(("admit-action", "admit-action") if not diagnostics else ()), + evidence_refs=evidence_refs, + ) + + +def _drive_participant_execution_probe( + target: RuntimeTarget, + control_plane: RuntimeControlPlane, +) -> list[ConformanceCaseResult]: + """Conditionally prove autonomous action execution and lifecycle behavior.""" + + scope = "participant.autonomous-execution.conformance" + cases = [ + _participant_execution_operation_case( + control_plane, + scope=scope, + action="start", + generation=0, + ), + _participant_execution_action_case( + target, + control_plane, + scope=scope, + ), + ] + for action in ("pause", "resume"): + cases.append( + _participant_execution_operation_case( + control_plane, + scope=scope, + action=action, + generation=0, + ) + ) + cases.append( + _participant_execution_operation_case( + control_plane, + scope=scope, + action="drain", + generation=0, + timeout_seconds=1, + ) + ) + cases.append( + _participant_execution_operation_case( + control_plane, + scope=scope, + action="reset", + generation=0, + ) + ) + cases.append( + _participant_execution_operation_case( + control_plane, + scope=scope, + action="drain", + generation=1, + timeout_seconds=1, + ) + ) + cases.append( + _participant_execution_operation_case( + control_plane, + scope=scope, + action="teardown", + generation=1, + ) + ) + return cases diff --git a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py index 0702276ef..9c3defc8a 100644 --- a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py +++ b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py @@ -77,6 +77,9 @@ def _snapshot_from_envelope(payload: dict[str, Any]) -> RuntimeSnapshot: state_address: state.model_dump(mode="json") for state_address, state in validated.participant_autonomous_execution_states.items() }, + participant_execution_services={ + scope: state.model_dump(mode="json") for scope, state in validated.participant_execution_services.items() + }, shared_state_records={ state_address: record.model_dump(mode="json") for state_address, record in validated.shared_state_records.items() diff --git a/implementations/python/packages/raes_conformance/conformance/target_probes.py b/implementations/python/packages/raes_conformance/conformance/target_probes.py index b2d3b7e1f..570fb03d2 100644 --- a/implementations/python/packages/raes_conformance/conformance/target_probes.py +++ b/implementations/python/packages/raes_conformance/conformance/target_probes.py @@ -18,6 +18,10 @@ from raes_runtime.registry import RuntimeTarget from raes_conformance.conformance.diagnostics import _SEMANTIC_INVALID_DIAGNOSTIC_CODE, _diagnostic +from raes_conformance.conformance.participant_execution_probes import ( + _drive_participant_execution_probe, + _participant_execution_probe_snapshot, +) from raes_conformance.conformance.profiles import ( BackendCapabilityProfile, BackendProfileSelector, @@ -311,6 +315,12 @@ def _hermetic_snapshot_payload(control_plane: RuntimeControlPlane) -> dict[str, participant_address: list(events) for participant_address, events in control_plane.snapshot.participant_behavior_history.items() }, + "participant_control_history": { + participant_address: list(events) + for participant_address, events in control_plane.snapshot.participant_control_history.items() + }, + "participant_autonomous_execution_states": dict(control_plane.snapshot.participant_autonomous_execution_states), + "participant_execution_services": dict(control_plane.snapshot.participant_execution_services), "shared_state_records": dict(control_plane.snapshot.shared_state_records), "shared_state_history": { state_address: list(records) @@ -407,7 +417,10 @@ def _target_adapter_cases( scenario = _DEFAULT_CONFORMANCE_SCENARIO if reference_scenario is None else reference_scenario execution_plan = run_reference_processor(scenario, target.manifest).execution_plan - control_plane = RuntimeControlPlane(target) + control_plane = RuntimeControlPlane( + target, + initial_snapshot=_participant_execution_probe_snapshot(target), + ) cases.append(_provisioning_probe_case(control_plane, execution_plan.provisioning)) if known != BackendCapabilityProfile.PROVISIONING_ONLY: if target.orchestrator is not None: @@ -421,5 +434,8 @@ def _target_adapter_cases( participant_address="participant.conformance", ) ) + capability = target.manifest.participant_runtime + if capability is not None and capability.supports_autonomous_execution: + cases.extend(_drive_participant_execution_probe(target, control_plane)) cases.append(_hermetic_snapshot_case(control_plane)) return tuple(cases) diff --git a/implementations/python/packages/raes_conformance/conformance/validators.py b/implementations/python/packages/raes_conformance/conformance/validators.py index 0a5a9b8a8..d128f8f6e 100644 --- a/implementations/python/packages/raes_conformance/conformance/validators.py +++ b/implementations/python/packages/raes_conformance/conformance/validators.py @@ -38,6 +38,11 @@ WorkflowExecutionStateModel, WorkflowHistoryEventModel, ) +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionBindingModel, + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) from raes_contracts.diagnostics import Diagnostic from raes_contracts.realization_envelope import BackendRealizationEnvelopeModel from raes_contracts.scientific_completeness import ( @@ -64,6 +69,9 @@ "workflow-result-envelope-v1": WorkflowExecutionStateModel.model_validate, "evaluation-result-envelope-v1": EvaluationResultStateModel.model_validate, "participant-episode-state-envelope-v1": ParticipantEpisodeStateModel.model_validate, + "participant-execution-binding-v1": ParticipantExecutionBindingModel.model_validate, + "participant-execution-control-v1": ParticipantExecutionControlRequestModel.model_validate, + "participant-execution-service-state-v1": ParticipantExecutionServiceStateModel.model_validate, "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_validate, "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_validate, "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_validate, diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index fd46389ab..f1448b409 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -415,9 +415,8 @@ "PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION", "PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION", "PARTICIPANT_CONFIGURATION_RESULT_V1_SCHEMA_VERSION", "ParticipantActionEffectResultModel", - "ParticipantActionPreconditionResultModel", "ParticipantActionResultModel", - "ParticipantAttributionCandidateModel", "ParticipantAttributionEdgeModel", - "ParticipantAttributionEvidenceBasisModel", "ParticipantAttributionOrderingBasisModel", + "ParticipantActionPreconditionResultModel", "ParticipantActionResultModel", "ParticipantAttributionCandidateModel", + "ParticipantAttributionEdgeModel", "ParticipantAttributionEvidenceBasisModel", "ParticipantAttributionOrderingBasisModel", "ParticipantActivityOccurrenceProvenanceModel", "ParticipantAutonomousExecutionStateModel", "ParticipantBehaviorHistoryEventModel", "ParticipantContextViewModel", "ParticipantControlDeclarationModel", diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index b1b55d107..7f953d0ae 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -46,6 +46,11 @@ ParticipantSharedStateRecordModel, ParticipantTimeManagementContextModel, ) +from .participant_execution import ( + ParticipantExecutionBindingModel, + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) from .participant_manifests import ( BackendManifestV2Model, ParticipantImplementationManifestModel, @@ -187,6 +192,9 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "ParticipantBehaviorHistoryEventStream", ParticipantBehaviorHistoryEventModel.model_json_schema(), ), + "participant-execution-binding-v1": ParticipantExecutionBindingModel.model_json_schema(), + "participant-execution-control-v1": ParticipantExecutionControlRequestModel.model_json_schema(), + "participant-execution-service-state-v1": ParticipantExecutionServiceStateModel.model_json_schema(), "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_json_schema(), "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_json_schema(), "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_json_schema(), diff --git a/implementations/python/packages/raes_contracts/contracts/manifests.py b/implementations/python/packages/raes_contracts/contracts/manifests.py index d6cc606a4..7bb12a0c4 100644 --- a/implementations/python/packages/raes_contracts/contracts/manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/manifests.py @@ -29,6 +29,7 @@ ) from .experiment_bindings import ConfigurationTargetRegistryModel from .feature_support import ParticipantFeatureSupportModel +from .participant_execution import ParticipantExecutionBindingModel from .time_manifest_capabilities import TimeCapabilitiesModel from .trial_cleanup import CleanupActionKind from .validators import ( @@ -140,6 +141,14 @@ class ParticipantRuntimeCapabilitiesModel(ContractModel): max_autonomous_occurrences: int | None = Field(default=None, ge=1) max_autonomous_retries_per_occurrence: int | None = Field(default=None, ge=1) max_autonomous_burst_size: int | None = Field(default=None, ge=1) + execution_bindings: list[ParticipantExecutionBindingModel] = Field(default_factory=list) + supports_execution_control: bool = False + supported_execution_control_actions: list[Literal["start", "pause", "resume", "drain", "reset", "teardown"]] = ( + Field(default_factory=list, json_schema_extra={"uniqueItems": True}) + ) + supports_bounded_concurrency: bool = False + max_execution_services: int | None = Field(default=None, ge=1) + max_concurrent_actions: int | None = Field(default=None, ge=2) constraints: dict[str, str] = Field(default_factory=dict) @model_validator(mode="after") @@ -167,6 +176,7 @@ def _validate_api_407_feature_support(self) -> ParticipantRuntimeCapabilitiesMod self._validate_supported_feature_levels() self._validate_autonomous_configuration() self._validate_autonomous_addresses() + self._validate_execution_control() return self def _validate_supported_feature_levels(self) -> None: @@ -226,6 +236,11 @@ def _has_complete_autonomous_configuration(self) -> bool: and self.supported_autonomous_observation_boundaries and self.supported_autonomous_policy_profiles and all(value is not None for value in self._autonomous_limits()) + and self.execution_bindings + and self.supports_execution_control + and self.supports_bounded_concurrency + and self.max_execution_services is not None + and self.max_concurrent_actions is not None ) def _has_any_autonomous_configuration(self) -> bool: @@ -238,8 +253,33 @@ def _has_any_autonomous_configuration(self) -> bool: or self.supported_autonomous_activity_features or self.supported_autonomous_random_stream_profiles or any(value is not None for value in self._autonomous_limits()) + or self.execution_bindings + or self.supports_execution_control + or self.supported_execution_control_actions + or self.supports_bounded_concurrency + or self.max_execution_services is not None + or self.max_concurrent_actions is not None ) + def _validate_execution_control(self) -> None: + if not self.supports_autonomous_execution: + return + required_actions = {"start", "pause", "resume", "drain", "reset", "teardown"} + missing = required_actions - set(self.supported_execution_control_actions) + if missing: + raise ValueError("execution control is missing required actions: " + ", ".join(sorted(missing))) + binding_ids = [binding.binding_id for binding in self.execution_bindings] + _validate_unique_string_values("execution_bindings", binding_ids) + supported_actions = set(self.supported_autonomous_action_contracts) + supported_targets = set(self.supported_autonomous_target_addresses) + for binding in self.execution_bindings: + if binding.action_contract_address not in supported_actions: + raise ValueError("execution binding action is not declared supported") + if not set(binding.target_addresses).issubset(supported_targets): + raise ValueError("execution binding target is not declared supported") + if self.max_concurrent_actions is not None and binding.max_in_flight > self.max_concurrent_actions: + raise ValueError("execution binding exceeds max_concurrent_actions") + def _autonomous_limits(self) -> tuple[int | None, ...]: return ( self.max_autonomous_participants, diff --git a/implementations/python/packages/raes_contracts/contracts/participant_execution.py b/implementations/python/packages/raes_contracts/contracts/participant_execution.py new file mode 100644 index 000000000..87e1be5ba --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_execution.py @@ -0,0 +1,150 @@ +"""Portable participant execution binding and lifecycle contracts.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import Field, model_validator + +from .base import ContractModel, NonEmptyString, PrefixedDigestString + +PARTICIPANT_EXECUTION_BINDING_SCHEMA_VERSION = "participant-execution-binding/v1" +PARTICIPANT_EXECUTION_CONTROL_SCHEMA_VERSION = "participant-execution-control/v1" +PARTICIPANT_EXECUTION_SERVICE_STATE_SCHEMA_VERSION = "participant-execution-service-state/v1" + +ParticipantExecutionControlAction = Literal[ + "start", + "pause", + "resume", + "drain", + "reset", + "teardown", +] +ParticipantExecutionLifecycle = Literal[ + "stopped", + "starting", + "running", + "pausing", + "paused", + "draining", + "quiescent", + "resetting", + "tearing_down", + "terminated", + "failed", +] +ParticipantExecutionHealth = Literal["healthy", "degraded", "unhealthy", "unknown"] +ParticipantExecutionReadiness = Literal["ready", "not_ready", "unknown"] + + +class ParticipantExecutionBindingModel(ContractModel): + """Exact executable relation between one action and its native targets.""" + + schema_version: Literal[PARTICIPANT_EXECUTION_BINDING_SCHEMA_VERSION] = PARTICIPANT_EXECUTION_BINDING_SCHEMA_VERSION + binding_id: NonEmptyString + action_contract_address: NonEmptyString + target_addresses: tuple[NonEmptyString, ...] = Field(min_length=1) + participant_implementation_ref: NonEmptyString + constraint_refs: tuple[NonEmptyString, ...] = Field(min_length=1) + evidence_refs: tuple[NonEmptyString, ...] = Field(min_length=1) + max_action_attempts: int = Field(ge=1) + max_in_flight: int = Field(ge=1) + timeout_seconds: int = Field(ge=1) + max_retries: int = Field(ge=0) + + @model_validator(mode="after") + def _validate_unique_refs(self) -> ParticipantExecutionBindingModel: + for field_name in ("target_addresses", "constraint_refs", "evidence_refs"): + values = getattr(self, field_name) + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must contain unique values") + return self + + +class ParticipantExecutionControlRequestModel(ContractModel): + """Generation-fenced lifecycle mutation for one admitted execution scope.""" + + schema_version: Literal[PARTICIPANT_EXECUTION_CONTROL_SCHEMA_VERSION] = PARTICIPANT_EXECUTION_CONTROL_SCHEMA_VERSION + execution_scope_ref: NonEmptyString + action: ParticipantExecutionControlAction + expected_generation: int = Field(ge=0) + timeout_seconds: int | None = Field(default=None, ge=1) + + @model_validator(mode="after") + def _validate_timeout(self) -> ParticipantExecutionControlRequestModel: + if self.action == "drain" and self.timeout_seconds is None: + raise ValueError("timeout_seconds is required for drain") + if self.action != "drain" and self.timeout_seconds is not None: + raise ValueError("timeout_seconds is only valid for drain") + return self + + +class ParticipantExecutionServiceStateModel(ContractModel): + """Typed health, readiness, lifecycle, capacity, and evidence readback.""" + + schema_version: Literal[PARTICIPANT_EXECUTION_SERVICE_STATE_SCHEMA_VERSION] = ( + PARTICIPANT_EXECUTION_SERVICE_STATE_SCHEMA_VERSION + ) + execution_scope_ref: NonEmptyString + policy_address: NonEmptyString + desired_lifecycle: ParticipantExecutionLifecycle + observed_lifecycle: ParticipantExecutionLifecycle + generation: int = Field(ge=0) + observed_generation: int = Field(ge=0) + health: ParticipantExecutionHealth + readiness: ParticipantExecutionReadiness + accepting_new_work: bool + draining: bool + quiescent: bool + resources_released: bool + policy_digest: PrefixedDigestString + binding_digest: PrefixedDigestString + time_declaration_digest: PrefixedDigestString + scheduler_state_refs: tuple[NonEmptyString, ...] = () + capacity: int = Field(ge=1) + reserved: int = Field(ge=0) + in_flight: int = Field(ge=0) + last_transition_ref: NonEmptyString + pacing_deviation_refs: tuple[NonEmptyString, ...] = () + evidence_refs: tuple[NonEmptyString, ...] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_state(self) -> ParticipantExecutionServiceStateModel: + if self.observed_generation > self.generation: + raise ValueError("observed_generation cannot exceed generation") + if self.reserved + self.in_flight > self.capacity: + raise ValueError("reserved and in_flight work cannot exceed capacity") + accepting_states = {"starting", "running"} + if self.accepting_new_work and self.observed_lifecycle not in accepting_states: + raise ValueError("accepting_new_work requires a starting or running observed lifecycle") + if self.accepting_new_work and self.readiness != "ready": + raise ValueError("accepting_new_work requires ready readback") + if self.draining != (self.observed_lifecycle == "draining"): + raise ValueError("draining must agree with the observed lifecycle") + if self.resources_released != (self.observed_lifecycle == "terminated"): + raise ValueError("resources_released must agree with terminated lifecycle") + if self.quiescent and (self.reserved or self.in_flight): + raise ValueError("quiescent execution cannot retain reserved or in-flight work") + for field_name in ( + "scheduler_state_refs", + "pacing_deviation_refs", + "evidence_refs", + ): + values = getattr(self, field_name) + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must contain unique values") + return self + + +__all__ = [ + "PARTICIPANT_EXECUTION_BINDING_SCHEMA_VERSION", + "PARTICIPANT_EXECUTION_CONTROL_SCHEMA_VERSION", + "PARTICIPANT_EXECUTION_SERVICE_STATE_SCHEMA_VERSION", + "ParticipantExecutionBindingModel", + "ParticipantExecutionControlAction", + "ParticipantExecutionControlRequestModel", + "ParticipantExecutionHealth", + "ParticipantExecutionLifecycle", + "ParticipantExecutionReadiness", + "ParticipantExecutionServiceStateModel", +] diff --git a/implementations/python/packages/raes_contracts/contracts/realization_plans.py b/implementations/python/packages/raes_contracts/contracts/realization_plans.py index 3ffe03584..9ab633e10 100644 --- a/implementations/python/packages/raes_contracts/contracts/realization_plans.py +++ b/implementations/python/packages/raes_contracts/contracts/realization_plans.py @@ -24,6 +24,7 @@ ParticipantSharedStateRecordModel, ParticipantTimeManagementContextModel, ) +from .participant_execution import ParticipantExecutionServiceStateModel from .participant_runtime import ( ParticipantAutonomousExecutionStateModel, ParticipantBehaviorHistoryEventModel, @@ -170,6 +171,7 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): participant_autonomous_execution_states: dict[str, ParticipantAutonomousExecutionStateModel] = Field( default_factory=dict ) + participant_execution_services: dict[str, ParticipantExecutionServiceStateModel] = Field(default_factory=dict) shared_state_records: dict[str, ParticipantSharedStateRecordModel] = Field(default_factory=dict) shared_state_history: dict[str, list[ParticipantSharedStateRecordModel]] = Field(default_factory=dict) joint_action_records: dict[str, ParticipantJointActionRecordModel] = Field(default_factory=dict) @@ -190,6 +192,9 @@ def _validate_entry_addresses(self) -> RuntimeSnapshotEnvelopeModel: raise ValueError( "Autonomous participant state map key must equal the embedded policy and participant address" ) + for map_key, state in self.participant_execution_services.items(): + if map_key != state.execution_scope_ref: + raise ValueError("Participant execution service map key must equal execution_scope_ref") return self diff --git a/implementations/python/packages/raes_contracts/manifest_authority.py b/implementations/python/packages/raes_contracts/manifest_authority.py index 456a46f24..a8a9226b9 100644 --- a/implementations/python/packages/raes_contracts/manifest_authority.py +++ b/implementations/python/packages/raes_contracts/manifest_authority.py @@ -53,6 +53,9 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-execution-binding-v1", + "participant-execution-control-v1", + "participant-execution-service-state-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", @@ -109,6 +112,16 @@ "runtime-snapshot-v1", } ) +_PARTICIPANT_AUTONOMOUS_EXECUTION_CONTRACTS = frozenset( + { + *_PARTICIPANT_INTERACTION_CONTRACTS, + "participant-execution-binding-v1", + "participant-execution-control-v1", + "participant-execution-service-state-v1", + "operation-receipt-v1", + "operation-status-v1", + } +) PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS = { PARTICIPANT_RUNTIME_ROLE_SCOPE: { @@ -119,7 +132,7 @@ }, PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE: { "action_contracts": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "autonomous_execution": _PARTICIPANT_INTERACTION_CONTRACTS, + "autonomous_execution": _PARTICIPANT_AUTONOMOUS_EXECUTION_CONTRACTS, "attribution_support": _PARTICIPANT_BEHAVIOR_CONTRACTS, "behavior_history": _PARTICIPANT_BEHAVIOR_CONTRACTS, "effects": _PARTICIPANT_BEHAVIOR_CONTRACTS, diff --git a/implementations/python/packages/raes_contracts/participant_binding.py b/implementations/python/packages/raes_contracts/participant_binding.py index e921b2124..6f2ef37ba 100644 --- a/implementations/python/packages/raes_contracts/participant_binding.py +++ b/implementations/python/packages/raes_contracts/participant_binding.py @@ -54,6 +54,7 @@ from .participant_binding_validation import ( string_tuple as _string_tuple, ) +from .participant_native_execution import ParticipantNativeActionExecution from .runtime_state import ApplyResult @@ -109,6 +110,9 @@ class ParticipantActionAdmissionRequest: state_transition_kind: str = "participant_action_admitted" post_state_digest: str | None = None requires_terminal_outcome: bool = False + target_addresses: tuple[str, ...] = () + execution_scope_ref: str | None = None + execution_generation: int | None = None def __post_init__(self) -> None: _require_non_empty(self.participant_address, "participant_address") @@ -139,6 +143,14 @@ def __post_init__(self) -> None: raise ValueError("validated_selection action_contract_address must match the admission request") if not isinstance(self.requires_terminal_outcome, bool): raise TypeError("requires_terminal_outcome must be a bool") + if (self.execution_scope_ref is None) != (self.execution_generation is None): + raise ValueError("execution_scope_ref and execution_generation must be provided together") + if self.execution_scope_ref is not None: + _require_non_empty(self.execution_scope_ref, "execution_scope_ref") + if self.execution_generation is None or self.execution_generation < 0: + raise ValueError("execution_generation must be non-negative") + if not self.target_addresses: + raise ValueError("generation-bound participant actions require target_addresses") if any(not isinstance(item, ParticipantTemporalRuntimeContextModel) for item in self.temporal_contexts): raise TypeError("temporal_contexts entries must be ParticipantTemporalRuntimeContextModel") if len({item.temporal_contract_id for item in self.temporal_contexts}) != len(self.temporal_contexts): @@ -151,6 +163,11 @@ def __post_init__(self) -> None: "observation_boundary_evidence_refs", _string_tuple(self.observation_boundary_evidence_refs, "observation_boundary_evidence_refs"), ) + object.__setattr__( + self, + "target_addresses", + _string_tuple(self.target_addresses, "target_addresses"), + ) violations = participant_action_admission_request_violations(self) if violations: raise ValueError(violations[0]) @@ -163,23 +180,6 @@ class ParticipantActionApplyResult(ApplyResult): action_result: ParticipantActionResultModel | None = None -@dataclass(frozen=True) -class ParticipantNativeActionExecution: - """Backend-native execution output used to commit portable action history.""" - - apply_result: ApplyResult - action_result: ParticipantActionResultModel | None = None - post_state_digest: str | None = None - - def __post_init__(self) -> None: - if not isinstance(self.apply_result, ApplyResult): - raise TypeError("apply_result must be an ApplyResult") - if self.action_result is not None and not isinstance(self.action_result, ParticipantActionResultModel): - raise TypeError("action_result must be a ParticipantActionResultModel or None") - if self.post_state_digest is not None: - _require_non_empty(self.post_state_digest, "post_state_digest") - - def participant_action_admission_request_violations( request: ParticipantActionAdmissionRequest, ) -> tuple[str, ...]: diff --git a/implementations/python/packages/raes_contracts/participant_native_execution.py b/implementations/python/packages/raes_contracts/participant_native_execution.py new file mode 100644 index 000000000..6d11b6d04 --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_native_execution.py @@ -0,0 +1,24 @@ +"""Backend-native participant action execution DTO.""" + +from dataclasses import dataclass + +from .contracts import ParticipantActionResultModel +from .participant_binding_validation import require_non_empty +from .runtime_state import ApplyResult + + +@dataclass(frozen=True) +class ParticipantNativeActionExecution: + """Backend-native execution output used to commit portable action history.""" + + apply_result: ApplyResult + action_result: ParticipantActionResultModel | None = None + post_state_digest: str | None = None + + def __post_init__(self) -> None: + if not isinstance(self.apply_result, ApplyResult): + raise TypeError("apply_result must be an ApplyResult") + if self.action_result is not None and not isinstance(self.action_result, ParticipantActionResultModel): + raise TypeError("action_result must be a ParticipantActionResultModel or None") + if self.post_state_digest is not None: + require_non_empty(self.post_state_digest, "post_state_digest") diff --git a/implementations/python/packages/raes_contracts/runtime_state.py b/implementations/python/packages/raes_contracts/runtime_state.py index 40f3b396a..1ab88d1e4 100644 --- a/implementations/python/packages/raes_contracts/runtime_state.py +++ b/implementations/python/packages/raes_contracts/runtime_state.py @@ -86,6 +86,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_autonomous_execution_states: dict[str, dict[str, Any]] = field(default_factory=dict) + participant_execution_services: dict[str, dict[str, Any]] = field(default_factory=dict) shared_state_records: dict[str, dict[str, Any]] = field(default_factory=dict) shared_state_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) joint_action_records: dict[str, dict[str, Any]] = field(default_factory=dict) @@ -160,6 +161,11 @@ def with_entries( "participant_autonomous_execution_states", self.participant_autonomous_execution_states, ), + participant_execution_services=_mapping_update( + updates, + "participant_execution_services", + self.participant_execution_services, + ), shared_state_records=_mapping_update( updates, "shared_state_records", @@ -210,6 +216,7 @@ def with_entries( "participant_behavior_history", "participant_control_history", "participant_autonomous_execution_states", + "participant_execution_services", "shared_state_records", "shared_state_history", "joint_action_records", diff --git a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py index e69deaf02..7ded3980e 100644 --- a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py +++ b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py @@ -2,7 +2,10 @@ from raes.scenario import InstantiatedScenario -from ..models import ParticipantAutonomousExecutionRuntime +from ..models import ( + ParticipantAutonomousExecutionRuntime, + ParticipantExecutionBindingRuntime, +) from .addresses import ( _action_contract_address, _behavior_specification_address, @@ -42,25 +45,36 @@ def _compile_autonomous_execution( else list(policy.temporal_constraint_refs) ) addressable_ref_index = _runtime_addressable_ref_index(scenario) - target_refs = [ - str(ref) - for action_ref in action_refs - for effect in scenario.action_contracts[ - _section_ref_name(action_ref, "action_contracts", scenario.action_contracts) - ].effects - for ref in effect.target_refs - ] - target_refs.extend( - str(ref) - for action_ref in action_refs - for precondition in scenario.action_contracts[ - _section_ref_name(action_ref, "action_contracts", scenario.action_contracts) - ].preconditions - for ref in precondition.support_refs - ) - target_addresses = _runtime_addresses_for_refs( - list(dict.fromkeys(target_refs)), - addressable_ref_index=addressable_ref_index, + execution_bindings_by_key: dict[tuple[str, tuple[str, ...]], ParticipantExecutionBindingRuntime] = {} + for action_ref in action_refs: + action_name = _section_ref_name( + action_ref, + "action_contracts", + scenario.action_contracts, + ) + action = scenario.action_contracts[action_name] + target_refs = [ + *(str(ref) for effect in action.effects for ref in effect.target_refs), + *(str(ref) for precondition in action.preconditions for ref in precondition.support_refs), + ] + action_contract_address = _action_contract_address(action_name) + target_addresses = _runtime_addresses_for_refs( + list(dict.fromkeys(target_refs)), + addressable_ref_index=addressable_ref_index, + ) + execution_bindings_by_key.setdefault( + (action_contract_address, target_addresses), + ParticipantExecutionBindingRuntime( + action_contract_address=action_contract_address, + target_addresses=target_addresses, + participant_implementation_ref=policy.participant_implementation_ref, + max_action_attempts=policy.max_action_attempts, + max_in_flight=policy.max_in_flight, + ), + ) + execution_bindings = tuple(execution_bindings_by_key.values()) + target_addresses = tuple( + dict.fromkeys(target for binding in execution_bindings for target in binding.target_addresses) ) return ParticipantAutonomousExecutionRuntime( address=address, @@ -91,6 +105,7 @@ def _compile_autonomous_execution( for ref in action_refs ), target_addresses=target_addresses, + execution_bindings=execution_bindings, observation_boundary_address=_observation_boundary_address( _section_ref_name( policy.observation_boundary_ref, diff --git a/implementations/python/packages/raes_processor/models/__init__.py b/implementations/python/packages/raes_processor/models/__init__.py index df6bfd3c1..652fbe2a4 100644 --- a/implementations/python/packages/raes_processor/models/__init__.py +++ b/implementations/python/packages/raes_processor/models/__init__.py @@ -114,6 +114,7 @@ ParticipantAutonomousExecutionRuntime, ParticipantBehaviorRuntime, ParticipantBehaviorSpecificationRuntime, + ParticipantExecutionBindingRuntime, ParticipantInjectDeliveryRuntime, ParticipantInteractiveAccessRuntime, ParticipantObservationBoundaryRuntime, @@ -281,6 +282,7 @@ "ParticipantBehaviorSpecificationRuntime", "ParticipantInjectDeliveryRuntime", "ParticipantAutonomousExecutionRuntime", + "ParticipantExecutionBindingRuntime", "ParticipantInteractiveAccessRuntime", "ParticipantEpisodeControlAction", "ParticipantEpisodeExecutionState", diff --git a/implementations/python/packages/raes_processor/models/behavior_resources.py b/implementations/python/packages/raes_processor/models/behavior_resources.py index bfb66f703..660eabc48 100644 --- a/implementations/python/packages/raes_processor/models/behavior_resources.py +++ b/implementations/python/packages/raes_processor/models/behavior_resources.py @@ -83,6 +83,17 @@ class ParticipantBehaviorRuntime(ResolvedResource): interpretation_mode: str = "role-neutral-projection" +@dataclass(frozen=True) +class ParticipantExecutionBindingRuntime: + """Compiled action-to-target relation for native participant execution.""" + + action_contract_address: str + target_addresses: tuple[str, ...] + participant_implementation_ref: str + max_action_attempts: int + max_in_flight: int + + @dataclass(frozen=True) class ParticipantAutonomousExecutionRuntime(ResolvedResource): """Compiled deterministic execution policy for ordinary participants.""" @@ -95,6 +106,7 @@ class ParticipantAutonomousExecutionRuntime(ResolvedResource): temporal_constraint_addresses: tuple[str, ...] = () action_contract_addresses: tuple[str, ...] = () target_addresses: tuple[str, ...] = () + execution_bindings: tuple[ParticipantExecutionBindingRuntime, ...] = () observation_boundary_address: str = "" selection_strategy: str = "" max_action_attempts: int = 0 diff --git a/implementations/python/packages/raes_runtime/backend_calls.py b/implementations/python/packages/raes_runtime/backend_calls.py index c9d59e477..35502813b 100644 --- a/implementations/python/packages/raes_runtime/backend_calls.py +++ b/implementations/python/packages/raes_runtime/backend_calls.py @@ -282,6 +282,9 @@ def _snapshot_carrier_addresses(snapshot: RuntimeSnapshot) -> set[str]: snapshot.participant_episode_results, snapshot.participant_episode_history, snapshot.participant_behavior_history, + snapshot.participant_control_history, + snapshot.participant_autonomous_execution_states, + snapshot.participant_execution_services, snapshot.shared_state_records, snapshot.shared_state_history, snapshot.joint_action_records, diff --git a/implementations/python/packages/raes_runtime/control_plane_api.py b/implementations/python/packages/raes_runtime/control_plane_api.py index 9102bd53d..0b54b1782 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api.py +++ b/implementations/python/packages/raes_runtime/control_plane_api.py @@ -19,6 +19,10 @@ RuntimeSnapshotEnvelopeModel, WorkflowCancellationRequestModel, ) +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) from raes_contracts.participant_episode import ParticipantEpisodeTerminalReason from raes_contracts.runtime_state import OperationReceipt @@ -28,6 +32,7 @@ _evaluation_plan, _operation_status_model, _orchestration_plan, + _ParticipantExecutionControlBody, _ParticipantInitializeBody, _ParticipantResetBody, _ParticipantRestartBody, @@ -183,10 +188,73 @@ def create_control_plane_app( _register_workflow_routes(app, control_plane) _register_participant_episode_routes(app, control_plane) _register_participant_control_routes(app, control_plane) + _register_participant_execution_routes(app, control_plane) register_participant_retrieval_routes(app, control_plane) return app +def _register_participant_execution_routes( + app: FastAPI, + control_plane: RuntimeControlPlane, +) -> None: + @app.post( + "/participant-executions/{execution_scope_ref}/control", + responses=_CONFLICT_RESPONSES, + ) + async def control_participant_execution( + execution_scope_ref: str, + request: Request, + identity: _MutatingIdentity, + body: _ParticipantExecutionControlBody, + ) -> OperationReceiptModel: + try: + control_request = ParticipantExecutionControlRequestModel( + execution_scope_ref=execution_scope_ref, + action=body.action, + expected_generation=body.expected_generation, + timeout_seconds=body.timeout_seconds, + ) + receipt = control_plane.control_participant_execution( + control_request, + idempotency_key=request.headers.get("idempotency-key", ""), + request_fingerprint=_request_fingerprint( + request, + getattr(request.state, "raw_body", b""), + ), + ) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + control_plane.record_audit( + action=f"participant_execution_{body.action}", + identity=identity.identity, + allowed=True, + target=str(request.url.path), + operation_id=receipt.operation_id, + ) + return _receipt_response(receipt) + + @app.get( + "/participant-executions/{execution_scope_ref}", + responses=_NOT_FOUND_RESPONSES, + ) + async def get_participant_execution_state( + execution_scope_ref: str, + request: Request, + identity: _ReadIdentity, + ) -> ParticipantExecutionServiceStateModel: + try: + state = control_plane.participant_execution_state(execution_scope_ref) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + control_plane.record_audit( + action="get_participant_execution_state", + identity=identity.identity, + allowed=True, + target=str(request.url.path), + ) + return state + + def _install_request_guards( app: FastAPI, control_plane: RuntimeControlPlane, 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 036c60703..30caabe38 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_models.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_models.py @@ -4,7 +4,7 @@ import hashlib from dataclasses import asdict -from typing import Any +from typing import Any, Literal from fastapi import Request from pydantic import BaseModel, ConfigDict, Field @@ -52,6 +52,13 @@ class _ParticipantTerminateBody(BaseModel): detail: str = Field(default="terminated by operator") +class _ParticipantExecutionControlBody(BaseModel): + model_config = ConfigDict(extra="forbid") + action: Literal["start", "pause", "resume", "drain", "reset", "teardown"] + expected_generation: int = Field(ge=0) + timeout_seconds: int | None = Field(default=None, ge=1) + + def _diagnostic_from_mapping(payload: dict[str, Any]) -> Diagnostic: return Diagnostic( code=str(payload.get("code", "runtime.control-plane")), @@ -158,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_autonomous_execution_states": dict(snapshot.participant_autonomous_execution_states), + "participant_execution_services": dict(snapshot.participant_execution_services), "shared_state_records": dict(snapshot.shared_state_records), "shared_state_history": dict(snapshot.shared_state_history), "joint_action_records": dict(snapshot.joint_action_records), diff --git a/implementations/python/packages/raes_runtime/control_plane_execution.py b/implementations/python/packages/raes_runtime/control_plane_execution.py index b08f43353..0a5e8257d 100644 --- a/implementations/python/packages/raes_runtime/control_plane_execution.py +++ b/implementations/python/packages/raes_runtime/control_plane_execution.py @@ -27,6 +27,36 @@ def execute_participant_action( address: str, idempotency_key: str, request_fingerprint: str, +) -> OperationReceipt: + lock = getattr(control_plane, "_participant_control_lock", None) + if lock is not None: + with lock: + return _execute_participant_action_locked( + control_plane, + method=method, + request=request, + address=address, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) + return _execute_participant_action_locked( + control_plane, + method=method, + request=request, + address=address, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) + + +def _execute_participant_action_locked( + control_plane: object, + *, + method: Callable[..., object], + request: object, + address: str, + idempotency_key: str, + request_fingerprint: str, ) -> OperationReceipt: existing = control_plane._idempotent_receipt( idempotency_key=idempotency_key, diff --git a/implementations/python/packages/raes_runtime/control_plane_store.py b/implementations/python/packages/raes_runtime/control_plane_store.py index 599cf56ce..13326329e 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store.py +++ b/implementations/python/packages/raes_runtime/control_plane_store.py @@ -134,6 +134,7 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: for participant_address, events in snapshot.participant_control_history.items() }, "participant_autonomous_execution_states": dict(snapshot.participant_autonomous_execution_states), + "participant_execution_services": dict(snapshot.participant_execution_services), "shared_state_records": dict(snapshot.shared_state_records), "shared_state_history": { state_address: list(records) for state_address, records in snapshot.shared_state_history.items() @@ -200,6 +201,7 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: for participant_address, events in payload.get("participant_control_history", {}).items() }, participant_autonomous_execution_states=dict(payload.get("participant_autonomous_execution_states", {})), + participant_execution_services=dict(payload.get("participant_execution_services", {})), shared_state_records=dict(payload.get("shared_state_records", {})), shared_state_history={ state_address: list(records) for state_address, records in payload.get("shared_state_history", {}).items() diff --git a/implementations/python/packages/raes_runtime/participant_clock_driver.py b/implementations/python/packages/raes_runtime/participant_clock_driver.py index 32af2170b..eccf90c3d 100644 --- a/implementations/python/packages/raes_runtime/participant_clock_driver.py +++ b/implementations/python/packages/raes_runtime/participant_clock_driver.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from fractions import Fraction +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel from raes_contracts.diagnostics import Diagnostic from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime @@ -60,12 +61,15 @@ def __init__( advance: Callable[[str, int], ApplyResult], service_due: Callable[[], ApplyResult], lock: threading.RLock, + publish_failure: Callable[[ApplyResult], None] | None = None, ) -> None: self._rates = _automatic_clock_rates(policies, time_model) + self._policy_clocks = {policy.address: policy.clock_address for policy in policies} self._snapshot = snapshot self._advance = advance self._service_due = service_due self._lock = lock + self._publish_failure = publish_failure self._stop = threading.Event() self._thread: threading.Thread | None = None self._failure: ApplyResult | None = None @@ -136,9 +140,10 @@ def _service_current_transition(self) -> ApplyResult | None: return self._service_due() if ticks == 0 else self._advance(clock_address, ticks) def _record_unexpected_failure(self, exc: BaseException) -> None: + snapshot = self._pacing_failure_snapshot(self._snapshot(), exc) self._failure = ApplyResult( success=False, - snapshot=self._snapshot(), + snapshot=snapshot, diagnostics=[ Diagnostic( code="runtime.participant-clock-driver-failed", @@ -148,6 +153,38 @@ def _record_unexpected_failure(self, exc: BaseException) -> None: ) ], ) + if self._publish_failure is not None: + self._publish_failure(self._failure) + + def _pacing_failure_snapshot( + self, + snapshot: RuntimeSnapshot, + exc: BaseException, + ) -> RuntimeSnapshot: + del exc + services = dict(snapshot.participant_execution_services) + for policy_address, clock_address in self._policy_clocks.items(): + payload = services.get(policy_address) + if payload is None: + continue + service = ParticipantExecutionServiceStateModel.model_validate(payload) + evidence_ref = f"evidence:{policy_address}:pacing-loss:{clock_address}:generation-{service.generation}" + services[policy_address] = service.model_copy( + update={ + "desired_lifecycle": "paused", + "observed_lifecycle": "paused", + "health": "degraded", + "readiness": "not_ready", + "accepting_new_work": False, + "last_transition_ref": (f"operation:{policy_address}:pacing-loss:generation-{service.generation}"), + "pacing_deviation_refs": tuple(dict.fromkeys([*service.pacing_deviation_refs, evidence_ref])), + "evidence_refs": tuple(dict.fromkeys([*service.evidence_refs, evidence_ref])), + } + ).model_dump(mode="json") + return snapshot.with_entries( + dict(snapshot.entries), + participant_execution_services=services, + ) def _next_transition(self) -> tuple[str, int, float] | None: with self._lock: diff --git a/implementations/python/packages/raes_runtime/participant_control.py b/implementations/python/packages/raes_runtime/participant_control.py index 633cdb895..f2e569af2 100644 --- a/implementations/python/packages/raes_runtime/participant_control.py +++ b/implementations/python/packages/raes_runtime/participant_control.py @@ -6,6 +6,10 @@ ParticipantDecisionSurfaceModel, ParticipantDecisionSurfaceSelectionModel, ) +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_binding import ( ParticipantActionAdmissionRequest, @@ -42,6 +46,7 @@ ) from .participant_control_mediation import record_participant_control from .participant_decision_surface_control_v2 import ParticipantDecisionSurfaceV2ControlMixin +from .participant_execution_control_boundary import backend_execution_control_method def _participant_binding_diagnostics( @@ -227,6 +232,45 @@ def record_participant_control( idempotency_key=idempotency_key, ) + def control_participant_execution( + self, + request: ParticipantExecutionControlRequestModel, + *, + idempotency_key: str = "", + request_fingerprint: str = "", + ) -> OperationReceipt: + """Submit one generation-fenced execution-service lifecycle mutation.""" + + participant_runtime = self._target.participant_runtime + method = getattr(participant_runtime, "control_execution", None) + if participant_runtime is None or not callable(method): + return self._reject_submission( + domain=RuntimeDomain.PARTICIPANT, + message=("Participant runtime does not expose portable execution control."), + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) + return execute_participant_action( + self, + method=backend_execution_control_method(method), + request=request, + address=(f"runtime.control-plane.participant-execution.{request.execution_scope_ref}.{request.action}"), + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) + + def participant_execution_state( + self, + execution_scope_ref: str, + ) -> ParticipantExecutionServiceStateModel: + """Read typed lifecycle, health, readiness, capacity, and evidence state.""" + + participant_runtime = self._target.participant_runtime + method = getattr(participant_runtime, "execution_state", None) + if participant_runtime is None or not callable(method): + raise ValueError("participant runtime does not expose execution-service readback") + return method(execution_scope_ref, self._snapshot) + def initialize_participant_episode( self, participant_address: str, diff --git a/implementations/python/packages/raes_runtime/participant_execution_control.py b/implementations/python/packages/raes_runtime/participant_execution_control.py index ea7566109..762cffdc4 100644 --- a/implementations/python/packages/raes_runtime/participant_execution_control.py +++ b/implementations/python/packages/raes_runtime/participant_execution_control.py @@ -129,10 +129,17 @@ def _start_participant_clock_driver(self) -> bool: advance=lambda clock_address, ticks: self.advance_time(clock_address, ticks=ticks), service_due=self.run_due_participant_actions, lock=self._participant_execution_lock, + publish_failure=self._publish_participant_clock_driver_failure, ) self._participant_clock_driver.start() return True + def _publish_participant_clock_driver_failure( + self, + result: ApplyResult, + ) -> None: + self._snapshot = result.snapshot + def _stop_participant_clock_driver(self) -> bool: if self._participant_clock_driver is not None: if not self._participant_clock_driver.stop(): diff --git a/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py b/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py new file mode 100644 index 000000000..7c25de060 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py @@ -0,0 +1,144 @@ +"""Validation boundary for backend-owned participant execution control.""" + +from __future__ import annotations + +from collections.abc import Callable + +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot + +from .backend_calls import _call_backend_apply + + +def _failure( + snapshot: RuntimeSnapshot, + request: ParticipantExecutionControlRequestModel, + code: str, + message: str, +) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code=code, + domain="participant", + address=request.execution_scope_ref, + message=message, + ) + ], + ) + + +def _precondition( + request: ParticipantExecutionControlRequestModel, + snapshot: RuntimeSnapshot, +) -> ApplyResult | None: + payload = snapshot.participant_execution_services.get(request.execution_scope_ref) + if payload is None: + return _failure( + snapshot, + request, + "runtime.participant-execution-not-found", + "Participant execution scope is not configured.", + ) + state = ParticipantExecutionServiceStateModel.model_validate(payload) + if state.generation != request.expected_generation: + return _failure( + snapshot, + request, + "runtime.participant-execution-stale-generation", + "Participant execution request generation does not match current state.", + ) + return None + + +def _expected_observation( + request: ParticipantExecutionControlRequestModel, +) -> tuple[str, str, bool]: + if request.action in {"start", "resume", "reset"}: + return "running", "ready", True + if request.action == "pause": + return "paused", "not_ready", False + if request.action == "drain": + return "quiescent", "not_ready", False + return "terminated", "not_ready", False + + +def _validate_observed_result( + request: ParticipantExecutionControlRequestModel, + predecessor: RuntimeSnapshot, + result: ApplyResult, +) -> ApplyResult: + if not result.success: + return result + payload = result.snapshot.participant_execution_services.get(request.execution_scope_ref) + if payload is None: + return _failure( + predecessor, + request, + "runtime.participant-execution-readback-missing", + "Backend control succeeded without publishing execution-service readback.", + ) + before = ParticipantExecutionServiceStateModel.model_validate( + predecessor.participant_execution_services[request.execution_scope_ref] + ) + observed = ParticipantExecutionServiceStateModel.model_validate(payload) + lifecycle, readiness, accepting = _expected_observation(request) + expected_generation = request.expected_generation + (1 if request.action == "reset" else 0) + invalid = ( + observed.observed_lifecycle != lifecycle + or observed.desired_lifecycle != lifecycle + or observed.readiness != readiness + or observed.accepting_new_work is not accepting + or observed.generation != expected_generation + or observed.observed_generation != expected_generation + or not observed.last_transition_ref + or observed.last_transition_ref == before.last_transition_ref + or not set(observed.evidence_refs).difference(before.evidence_refs) + ) + if request.action == "drain": + invalid = invalid or bool( + observed.reserved or observed.in_flight or observed.draining or not observed.quiescent + ) + if request.action == "teardown": + invalid = invalid or not observed.resources_released + if invalid: + return _failure( + predecessor, + request, + "runtime.participant-execution-readback-invalid", + "Backend control result did not prove the requested observed lifecycle transition.", + ) + return result + + +def backend_execution_control_method( + backend_method: Callable[..., object], +) -> Callable[[ParticipantExecutionControlRequestModel, RuntimeSnapshot], ApplyResult]: + """Wrap native control with generation preflight and observed-result checks.""" + + def apply( + request: ParticipantExecutionControlRequestModel, + snapshot: RuntimeSnapshot, + ) -> ApplyResult: + failure = _precondition(request, snapshot) + if failure is not None: + return failure + result = _call_backend_apply( + backend_method, + request, + snapshot, + address=f"runtime.participant-execution.{request.execution_scope_ref}.{request.action}", + snapshot=snapshot, + ) + return _validate_observed_result(request, snapshot, result) + + return apply + + +__all__ = ["backend_execution_control_method"] diff --git a/implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py b/implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py new file mode 100644 index 000000000..745813332 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py @@ -0,0 +1,158 @@ +"""Execution-service state coordination for the autonomous scheduler.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import asdict + +from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + + +def _payload_digest(payload: object) -> str: + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) + return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def execution_service_state( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + *, + policy_digest: str, +) -> ParticipantExecutionServiceStateModel: + """Build typed execution-service readback for one admitted policy.""" + + clock = next(item for item in time_model.clocks if item.address == policy.clock_address) + progression = next( + item for item in time_model.progression_policies if item.address == policy.progression_policy_address + ) + constraints = tuple( + asdict(item) for item in time_model.constraints if item.address in policy.temporal_constraint_addresses + ) + return ParticipantExecutionServiceStateModel( + execution_scope_ref=policy.address, + policy_address=policy.address, + desired_lifecycle="running", + observed_lifecycle="running", + generation=0, + observed_generation=0, + health="healthy", + readiness="ready", + accepting_new_work=True, + draining=False, + quiescent=True, + resources_released=False, + policy_digest=policy_digest, + binding_digest=_payload_digest(tuple(asdict(binding) for binding in policy.execution_bindings)), + time_declaration_digest=_payload_digest( + { + "clock": asdict(clock), + "progression": asdict(progression), + "constraints": constraints, + } + ), + scheduler_state_refs=tuple( + f"{policy.address}.state.{participant_address}" for participant_address in policy.participant_addresses + ), + capacity=policy.max_in_flight, + reserved=0, + in_flight=0, + last_transition_ref=f"operation:{policy.address}:start:generation-0", + evidence_refs=(f"evidence:{policy.address}:readiness:generation-0",), + ) + + +def reset_execution_service( + snapshot: RuntimeSnapshot, + policy_address: str, +) -> tuple[RuntimeSnapshot, bool]: + """Advance the service generation after a shared-clock reset.""" + + services = dict(snapshot.participant_execution_services) + payload = services.get(policy_address) + if payload is None: + return snapshot, False + service = ParticipantExecutionServiceStateModel.model_validate(payload) + generation = service.generation + 1 + evidence_ref = f"evidence:{policy_address}:shared-time-reset:generation-{generation}" + services[policy_address] = service.model_copy( + update={ + "desired_lifecycle": "running", + "observed_lifecycle": "running", + "generation": generation, + "observed_generation": generation, + "readiness": "ready", + "accepting_new_work": True, + "draining": False, + "quiescent": True, + "reserved": 0, + "in_flight": 0, + "last_transition_ref": (f"operation:{policy_address}:shared-time-reset:generation-{generation}"), + "evidence_refs": tuple(dict.fromkeys([*service.evidence_refs, evidence_ref])), + } + ).model_dump(mode="json") + return ( + snapshot.with_entries( + dict(snapshot.entries), + participant_execution_services=services, + ), + True, + ) + + +def set_execution_clock_lifecycle( + snapshot: RuntimeSnapshot, + clock_address: str, + lifecycle_state: str, +) -> ApplyResult: + """Coordinate shared-clock lifecycle with scheduler and service readback.""" + + states = dict(snapshot.participant_autonomous_execution_states) + services = dict(snapshot.participant_execution_services) + changed: list[str] = [] + affected_policies: set[str] = set() + for key, payload in list(states.items()): + state = ParticipantAutonomousExecutionStateModel.model_validate(payload) + if state.clock_address == clock_address and state.lifecycle_state not in {"completed", "failed"}: + states[key] = state.model_copy(update={"lifecycle_state": lifecycle_state}).model_dump(mode="json") + changed.append(key) + affected_policies.add(state.policy_address) + for policy_address in affected_policies: + service_payload = services.get(policy_address) + if service_payload is None: + continue + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + paused = lifecycle_state == "paused" + evidence_ref = f"evidence:{policy_address}:shared-time-{lifecycle_state}:generation-{service.generation}" + services[policy_address] = service.model_copy( + update={ + "desired_lifecycle": lifecycle_state, + "observed_lifecycle": lifecycle_state, + "readiness": "not_ready" if paused else "ready", + "accepting_new_work": not paused, + "last_transition_ref": ( + f"operation:{policy_address}:shared-time-{lifecycle_state}:generation-{service.generation}" + ), + "evidence_refs": tuple(dict.fromkeys([*service.evidence_refs, evidence_ref])), + } + ).model_dump(mode="json") + changed.append(policy_address) + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_autonomous_execution_states=states, + participant_execution_services=services, + ), + changed_addresses=changed, + ) + + +__all__ = [ + "execution_service_state", + "reset_execution_service", + "set_execution_clock_lifecycle", +] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index 43ad852a1..b0ea709df 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -2,12 +2,12 @@ from __future__ import annotations -import hashlib -import json from collections.abc import Iterable -from dataclasses import asdict -from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.contracts import ( + ParticipantAutonomousExecutionStateModel, +) +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_episode import ( ParticipantEpisodeInitializeRequest, @@ -22,10 +22,17 @@ draw_activity_integer, next_activity_timing, ) +from .participant_execution_scheduler_state import ( + execution_service_state, + reset_execution_service, + set_execution_clock_lifecycle, +) from .participant_scheduler_operations import ( SchedulerRunState, run_participant_due, + run_policy_due_concurrently, ) +from .participant_scheduler_policy import _policy_digest from .participant_scheduler_time import clock_coordinate @@ -33,68 +40,6 @@ def _state_key(policy_address: str, participant_address: str) -> str: return f"{policy_address}.state.{participant_address}" -def _policy_digest( - policy: ParticipantAutonomousExecutionRuntime, - time_model: CompiledTimeModel, -) -> str: - clock = next(item for item in time_model.clocks if item.address == policy.clock_address) - progression = next( - item for item in time_model.progression_policies if item.address == policy.progression_policy_address - ) - domain = next(item for item in time_model.domains if item.address == clock.time_domain_address) - constraints = sorted( - (asdict(item) for item in time_model.constraints if item.address in policy.temporal_constraint_addresses), - key=lambda item: str(item["address"]), - ) - payload = { - "address": policy.address, - "participant_addresses": policy.participant_addresses, - "participant_implementation_ref": policy.participant_implementation_ref, - "clock_address": policy.clock_address, - "progression_policy_address": policy.progression_policy_address, - "temporal_constraint_addresses": policy.temporal_constraint_addresses, - "action_contract_addresses": policy.action_contract_addresses, - "target_addresses": policy.target_addresses, - "observation_boundary_address": policy.observation_boundary_address, - "selection_strategy": policy.selection_strategy, - "max_action_attempts": policy.max_action_attempts, - "max_in_flight": policy.max_in_flight, - "failure_policy": policy.failure_policy, - "evaluation_authority_mode": policy.evaluation_authority_mode, - "objective_refs": policy.objective_refs, - "proof_producer_refs": policy.proof_producer_refs, - "score_authority_refs": policy.score_authority_refs, - "receipt_authority_refs": policy.receipt_authority_refs, - "resolved_clock": asdict(clock), - "resolved_time_domain": asdict(domain), - "resolved_progression_policy": asdict(progression), - "resolved_temporal_constraints": constraints, - } - if policy.profile == "participant-autonomous-execution/v2": - payload.update( - { - "profile": policy.profile, - "work_window_addresses": policy.work_window_addresses, - "pause_window_addresses": policy.pause_window_addresses, - "stochastic_control_ref": policy.stochastic_control_ref, - "timing_minimum_ticks": policy.timing_minimum_ticks, - "timing_maximum_ticks": policy.timing_maximum_ticks, - "outside_window_disposition": policy.outside_window_disposition, - "empty_eligible_disposition": policy.empty_eligible_disposition, - "action_candidate_ids": policy.action_candidate_ids, - "action_candidate_weights": policy.action_candidate_weights, - "action_candidate_dependencies": policy.action_candidate_dependencies, - "action_candidate_retry_failure_classes": (policy.action_candidate_retry_failure_classes), - "action_candidate_max_retries": policy.action_candidate_max_retries, - "action_candidate_cooldown_ticks": policy.action_candidate_cooldown_ticks, - "max_occurrences": policy.max_occurrences, - "max_burst_size": policy.max_burst_size, - } - ) - canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) - return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest() - - def _clock_tick(snapshot: RuntimeSnapshot, clock_address: str) -> int: if snapshot.time_model_state is None: raise ValueError("autonomous participant execution requires typed shared-time state") @@ -275,6 +220,43 @@ def initialize( return result working = result.snapshot changed.extend(result.changed_addresses) + services = dict(working.participant_execution_services) + expected_service = execution_service_state( + policy, + time_model, + policy_digest=_policy_digest(policy, time_model), + ) + existing_service = services.get(policy.address) + if existing_service is not None: + existing = ParticipantExecutionServiceStateModel.model_validate(existing_service) + if ( + existing.policy_digest != expected_service.policy_digest + or existing.binding_digest != expected_service.binding_digest + or existing.time_declaration_digest != expected_service.time_declaration_digest + ): + return ApplyResult( + success=False, + snapshot=working, + diagnostics=[ + Diagnostic( + code="runtime.participant-execution-state-conflict", + domain="participant", + address=policy.address, + message=( + "Existing participant execution service state " + "does not match the admitted policy, bindings, " + "or shared-time declaration." + ), + ) + ], + ) + else: + services[policy.address] = expected_service.model_dump(mode="json") + working = working.with_entries( + dict(working.entries), + participant_execution_services=services, + ) + changed.append(policy.address) return ApplyResult( success=True, snapshot=working, @@ -292,10 +274,42 @@ def run_due( run = SchedulerRunState(working=snapshot, diagnostics=[], changed=[]) resolved_activity_controls = activity_controls or {} for policy in policies: + service_payload = run.working.participant_execution_services.get(policy.address) + if service_payload is None: + return ApplyResult( + success=False, + snapshot=run.working, + diagnostics=[ + Diagnostic( + code="runtime.participant-execution-state-missing", + domain="participant", + address=policy.address, + message=("Autonomous participant execution requires typed execution-service state."), + ) + ], + ) + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + if ( + service.observed_lifecycle != "running" + or not service.accepting_new_work + or service.readiness != "ready" + ): + continue cadence_ticks = ( _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 ) current_tick = _clock_tick(run.working, policy.clock_address) + if run_policy_due_concurrently( + policy, + time_model, + participant_runtime, + current_tick, + cadence_ticks, + run, + ): + if run.failure is not None: + return run.result() + continue for participant_address in policy.participant_addresses: run_participant_due( policy, @@ -409,6 +423,12 @@ def reset_clock( participant_autonomous_execution_states=states, ) changed.extend([*result_changed, key]) + working, service_changed = reset_execution_service( + working, + policy.address, + ) + if service_changed: + changed.append(policy.address) return ApplyResult( success=True, snapshot=working, @@ -421,20 +441,10 @@ def set_clock_lifecycle( clock_address: str, lifecycle_state: str, ) -> ApplyResult: - states = dict(snapshot.participant_autonomous_execution_states) - changed: list[str] = [] - for key, payload in list(states.items()): - state = ParticipantAutonomousExecutionStateModel.model_validate(payload) - if state.clock_address == clock_address and state.lifecycle_state not in {"completed", "failed"}: - states[key] = state.model_copy(update={"lifecycle_state": lifecycle_state}).model_dump(mode="json") - changed.append(key) - return ApplyResult( - success=True, - snapshot=snapshot.with_entries( - dict(snapshot.entries), - participant_autonomous_execution_states=states, - ), - changed_addresses=changed, + return set_execution_clock_lifecycle( + snapshot, + clock_address, + lifecycle_state, ) diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py new file mode 100644 index 000000000..b361729de --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py @@ -0,0 +1,354 @@ +"""Bounded concurrent native execution and serialized participant commits.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.participant_binding import ParticipantActionAdmissionRequest +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + +from .participant_action_validation import autonomous_action_result_violation + +if TYPE_CHECKING: + from .participant_scheduler_types import SchedulerRunState, _DueActionContext + + +def _changed_mapping( + base: dict[str, object], + incoming: dict[str, object], +) -> dict[str, object]: + return {key: value for key, value in incoming.items() if base.get(key) != value} + + +def _merge_mapping_revision_checked( + *, + base: dict[str, object], + current: dict[str, object], + incoming: dict[str, object], + field_name: str, +) -> dict[str, object]: + merged = dict(current) + for key, value in _changed_mapping(base, incoming).items(): + current_value = current.get(key) + base_value = base.get(key) + if current_value != base_value and current_value != value: + raise ValueError(f"concurrent participant commit conflict in {field_name}[{key!r}]") + merged[key] = value + return merged + + +def _merge_concurrent_action_snapshot( + base: RuntimeSnapshot, + current: RuntimeSnapshot, + incoming: RuntimeSnapshot, +) -> RuntimeSnapshot: + """Merge one native result without replacing a newer whole snapshot.""" + + entries = _merge_mapping_revision_checked( + base=dict(base.entries), + current=dict(current.entries), + incoming=dict(incoming.entries), + field_name="entries", + ) + mapping_fields = ( + "participant_episode_results", + "participant_episode_history", + "participant_behavior_history", + "participant_control_history", + "shared_state_records", + "shared_state_history", + "joint_action_records", + "time_management_contexts", + ) + updates: dict[str, object] = {} + for field_name in mapping_fields: + updates[field_name] = _merge_mapping_revision_checked( + base=dict(getattr(base, field_name)), + current=dict(getattr(current, field_name)), + incoming=dict(getattr(incoming, field_name)), + field_name=field_name, + ) + metadata = dict(current.metadata) + metadata.update(incoming.metadata) + updates["metadata"] = metadata + return current.with_entries(entries, **updates) + + +def participant_generation_commit_diagnostic( + request: ParticipantActionAdmissionRequest, + authoritative: RuntimeSnapshot, +) -> Diagnostic | None: + """Fence native completion against the serialized commit owner's state.""" + + scope = request.execution_scope_ref + if scope is None: + return None + payload = authoritative.participant_execution_services.get(scope) + expected = request.execution_generation + if payload is not None: + service = ParticipantExecutionServiceStateModel.model_validate(payload) + if service.generation == expected and service.observed_generation == expected: + return None + return Diagnostic( + code="runtime.participant-execution-stale-completion", + domain="participant", + address=request.participant_address, + message=( + "Participant action completion was rejected because the authoritative serialized commit generation changed." + ), + ) + + +def _reserve_concurrent_actions( + run: SchedulerRunState, + contexts: tuple[_DueActionContext, ...], +) -> None: + states = dict(run.working.participant_autonomous_execution_states) + for context in contexts: + state = ParticipantAutonomousExecutionStateModel.model_validate(states[context.key]) + states[context.key] = state.model_copy( + update={ + "attempted_actions": state.attempted_actions + 1, + "in_flight": state.in_flight + 1, + } + ).model_dump(mode="json") + services = dict(run.working.participant_execution_services) + for policy_address in {context.policy.address for context in contexts}: + payload = services.get(policy_address) + if payload is None: + continue + service = ParticipantExecutionServiceStateModel.model_validate(payload) + count = sum(1 for context in contexts if context.policy.address == policy_address) + services[policy_address] = service.model_copy( + update={ + "reserved": 0, + "in_flight": count, + "quiescent": False, + } + ).model_dump(mode="json") + run.working = run.working.with_entries( + dict(run.working.entries), + participant_autonomous_execution_states=states, + participant_execution_services=services, + ) + + +def _finish_concurrent_service_state( + run: SchedulerRunState, + policy_address: str, +) -> None: + services = dict(run.working.participant_execution_services) + payload = services.get(policy_address) + if payload is None: + return + service = ParticipantExecutionServiceStateModel.model_validate(payload) + services[policy_address] = service.model_copy(update={"reserved": 0, "in_flight": 0, "quiescent": True}).model_dump( + mode="json" + ) + run.working = run.working.with_entries( + dict(run.working.entries), + participant_execution_services=services, + ) + + +def run_policy_due_concurrently( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + current_tick: int, + cadence_ticks: int, + run: SchedulerRunState, +) -> bool: + """Execute one due v1 occurrence per participant with bounded overlap.""" + + from .participant_scheduler_operations import ( + _bound_action_request, + _cadence_missed_result, + _next_action_state, + run_participant_due, + ) + from .participant_scheduler_types import _DueActionContext + + if policy.profile != "participant-autonomous-execution/v1": + return False + contexts: list[_DueActionContext] = [] + states: list[ParticipantAutonomousExecutionStateModel] = [] + for participant_address in policy.participant_addresses: + key = f"{policy.address}.state.{participant_address}" + state = ParticipantAutonomousExecutionStateModel.model_validate( + run.working.participant_autonomous_execution_states[key] + ) + if state.lifecycle_state == "running" and state.next_tick < current_tick: + run.failure = _cadence_missed_result( + run.working, + key, + current_tick, + state, + ) + return True + if not ( + state.lifecycle_state == "running" + and state.next_tick == current_tick + and state.attempted_actions < policy.max_action_attempts + ): + continue + contexts.append( + _DueActionContext( + policy=policy, + time_model=time_model, + participant_runtime=participant_runtime, + participant_address=participant_address, + key=key, + current_tick=current_tick, + cadence_ticks=cadence_ticks, + ) + ) + states.append(state) + if len(contexts) < 2 or policy.max_in_flight < 2: + return False + batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) + if not callable(batch_method): + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=[ + Diagnostic( + code="runtime.participant-concurrency-unsupported", + domain="participant", + address=policy.address, + message=("Backend declared bounded participant concurrency without an executable batch method."), + ) + ], + ) + return True + contexts_tuple = tuple(contexts[: policy.max_in_flight]) + states = states[: policy.max_in_flight] + requests = tuple( + _bound_action_request(context, run.working, state) + for context, state in zip(contexts_tuple, states, strict=True) + ) + _reserve_concurrent_actions(run, contexts_tuple) + base = run.working + results = batch_method( + requests, + base, + len(requests), + ) + if len(results) != len(requests): + raise ValueError("concurrent participant result count must match requests") + for context, state, request, result in zip( + contexts_tuple, + states, + requests, + results, + strict=True, + ): + stale_completion = participant_generation_commit_diagnostic( + request, + run.working, + ) + if stale_completion is not None: + run.diagnostics.append(stale_completion) + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + break + try: + run.working = _merge_concurrent_action_snapshot( + base, + run.working, + result.snapshot, + ) + except ValueError as exc: + run.diagnostics.append( + Diagnostic( + code="runtime.participant-concurrent-commit-conflict", + domain="participant", + address=context.key, + message=str(exc), + ) + ) + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + break + protocol_violation = autonomous_action_result_violation( + request, + result, + episode_id=state.episode_id, + predecessor=base, + ) + if protocol_violation is not None: + run.diagnostics.append( + Diagnostic( + code="runtime.participant-autonomous-action-protocol-invalid", + domain="participant", + address=context.participant_address, + message=protocol_violation, + ) + ) + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + break + action_result = result.action_result + action_succeeded = bool(result.success and action_result is not None and action_result.status == "succeeded") + next_state = _next_action_state( + context, + state, + request, + action_succeeded=action_succeeded, + protocol_failure=False, + ).model_copy(update={"in_flight": 0}) + scheduler_states = dict(run.working.participant_autonomous_execution_states) + scheduler_states[context.key] = next_state.model_dump(mode="json") + run.working = run.working.with_entries( + dict(run.working.entries), + participant_autonomous_execution_states=scheduler_states, + ) + run.diagnostics.extend(result.diagnostics) + run.changed.extend([*result.changed_addresses, context.key]) + if not action_succeeded and policy.failure_policy == "stop": + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + break + _finish_concurrent_service_state(run, policy.address) + if run.failure is None: + if not run_policy_due_concurrently( + policy, + time_model, + participant_runtime, + current_tick, + cadence_ticks, + run, + ): + for participant_address in policy.participant_addresses: + run_participant_due( + policy, + time_model, + participant_runtime, + participant_address, + current_tick, + cadence_ticks, + run, + ) + if run.failure is not None: + break + return True diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py index b0dd14580..789b88c85 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py @@ -2,13 +2,14 @@ from __future__ import annotations -from dataclasses import dataclass, replace +from dataclasses import replace from typing import cast from raes_contracts.contracts import ( ParticipantAutonomousExecutionStateModel, ParticipantTemporalRuntimeContextModel, ) +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_binding import ParticipantActionAdmissionRequest from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot @@ -28,37 +29,9 @@ annotate_activity_history, persist_activity_state, ) +from .participant_scheduler_concurrency import participant_generation_commit_diagnostic, run_policy_due_concurrently from .participant_scheduler_time import clock_coordinate, participant_time_domain - - -@dataclass -class SchedulerRunState: - """Mutable aggregate for one deterministic scheduler pass.""" - - working: RuntimeSnapshot - diagnostics: list[Diagnostic] - changed: list[str] - failure: ApplyResult | None = None - - def result(self) -> ApplyResult: - return self.failure or ApplyResult( - success=True, - snapshot=self.working, - diagnostics=self.diagnostics, - changed_addresses=list(dict.fromkeys(self.changed)), - ) - - -@dataclass(frozen=True) -class _DueActionContext: - policy: ParticipantAutonomousExecutionRuntime - time_model: CompiledTimeModel - participant_runtime: object - participant_address: str - key: str - current_tick: int - cadence_ticks: int - activity_control: ParticipantActivityRandomControl | None = None +from .participant_scheduler_types import SchedulerRunState, _DueActionContext def _cadence_missed_result( @@ -125,6 +98,15 @@ def _bound_action_request( ) if request.implementation_selection.manifest_ref != policy.participant_implementation_ref: raise ValueError("participant implementation selection does not match the autonomous execution policy") + matching_bindings = tuple( + binding for binding in policy.execution_bindings if binding.action_contract_address == action_address + ) + if len(matching_bindings) != 1: + raise ValueError("autonomous participant action must resolve exactly one execution binding") + service_payload = working.participant_execution_services.get(policy.address) + if service_payload is None: + raise ValueError("autonomous participant action requires execution-service state") + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) return cast( ParticipantActionAdmissionRequest, replace( @@ -137,6 +119,9 @@ def _bound_action_request( action_result=None, post_state_digest=None, requires_terminal_outcome=True, + target_addresses=matching_bindings[0].target_addresses, + execution_scope_ref=policy.address, + execution_generation=service.generation, ), ) @@ -201,6 +186,16 @@ def _run_one_due_action( request = _bound_action_request(context, run.working, state) predecessor = run.working result = context.participant_runtime.admit_action(request, predecessor) + stale_completion = participant_generation_commit_diagnostic(request, run.working) + if stale_completion is not None: + run.diagnostics.append(stale_completion) + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + return state protocol_violation = autonomous_action_result_violation( request, result, @@ -497,4 +492,8 @@ def run_participant_due( ) -__all__ = ["SchedulerRunState", "run_participant_due"] +__all__ = [ + "SchedulerRunState", + "run_participant_due", + "run_policy_due_concurrently", +] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_policy.py b/implementations/python/packages/raes_runtime/participant_scheduler_policy.py new file mode 100644 index 000000000..4a18401cb --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_policy.py @@ -0,0 +1,70 @@ +"""Canonical autonomous participant policy identity.""" + +import hashlib +import json +from dataclasses import asdict + +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + + +def _policy_digest( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, +) -> str: + clock = next(item for item in time_model.clocks if item.address == policy.clock_address) + progression = next( + item for item in time_model.progression_policies if item.address == policy.progression_policy_address + ) + domain = next(item for item in time_model.domains if item.address == clock.time_domain_address) + constraints = sorted( + (asdict(item) for item in time_model.constraints if item.address in policy.temporal_constraint_addresses), + key=lambda item: str(item["address"]), + ) + payload = { + "address": policy.address, + "participant_addresses": policy.participant_addresses, + "participant_implementation_ref": policy.participant_implementation_ref, + "clock_address": policy.clock_address, + "progression_policy_address": policy.progression_policy_address, + "temporal_constraint_addresses": policy.temporal_constraint_addresses, + "action_contract_addresses": policy.action_contract_addresses, + "target_addresses": policy.target_addresses, + "execution_bindings": tuple(asdict(binding) for binding in policy.execution_bindings), + "observation_boundary_address": policy.observation_boundary_address, + "selection_strategy": policy.selection_strategy, + "max_action_attempts": policy.max_action_attempts, + "max_in_flight": policy.max_in_flight, + "failure_policy": policy.failure_policy, + "evaluation_authority_mode": policy.evaluation_authority_mode, + "objective_refs": policy.objective_refs, + "proof_producer_refs": policy.proof_producer_refs, + "score_authority_refs": policy.score_authority_refs, + "receipt_authority_refs": policy.receipt_authority_refs, + "resolved_clock": asdict(clock), + "resolved_time_domain": asdict(domain), + "resolved_progression_policy": asdict(progression), + "resolved_temporal_constraints": constraints, + } + if policy.profile == "participant-autonomous-execution/v2": + payload.update( + { + "profile": policy.profile, + "work_window_addresses": policy.work_window_addresses, + "pause_window_addresses": policy.pause_window_addresses, + "stochastic_control_ref": policy.stochastic_control_ref, + "timing_minimum_ticks": policy.timing_minimum_ticks, + "timing_maximum_ticks": policy.timing_maximum_ticks, + "outside_window_disposition": policy.outside_window_disposition, + "empty_eligible_disposition": policy.empty_eligible_disposition, + "action_candidate_ids": policy.action_candidate_ids, + "action_candidate_weights": policy.action_candidate_weights, + "action_candidate_dependencies": policy.action_candidate_dependencies, + "action_candidate_retry_failure_classes": (policy.action_candidate_retry_failure_classes), + "action_candidate_max_retries": policy.action_candidate_max_retries, + "action_candidate_cooldown_ticks": policy.action_candidate_cooldown_ticks, + "max_occurrences": policy.max_occurrences, + "max_burst_size": policy.max_burst_size, + } + ) + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) + return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest() diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_types.py b/implementations/python/packages/raes_runtime/participant_scheduler_types.py new file mode 100644 index 000000000..344c3c193 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_types.py @@ -0,0 +1,41 @@ +"""Shared mutable and immutable state for autonomous scheduler operations.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + +from .participant_activity import ParticipantActivityRandomControl + + +@dataclass +class SchedulerRunState: + """Mutable aggregate for one deterministic scheduler pass.""" + + working: RuntimeSnapshot + diagnostics: list[Diagnostic] + changed: list[str] + failure: ApplyResult | None = None + + def result(self) -> ApplyResult: + return self.failure or ApplyResult( + success=True, + snapshot=self.working, + diagnostics=self.diagnostics, + changed_addresses=list(dict.fromkeys(self.changed)), + ) + + +@dataclass(frozen=True) +class _DueActionContext: + policy: ParticipantAutonomousExecutionRuntime + time_model: CompiledTimeModel + participant_runtime: object + participant_address: str + key: str + current_tick: int + cadence_ticks: int + activity_control: ParticipantActivityRandomControl | None = None diff --git a/implementations/python/packages/raes_runtime/registry.py b/implementations/python/packages/raes_runtime/registry.py index 0773913c4..7498ebee9 100644 --- a/implementations/python/packages/raes_runtime/registry.py +++ b/implementations/python/packages/raes_runtime/registry.py @@ -91,6 +91,12 @@ def _validate_runtime_target_shape( manifest.participant_runtime and manifest.participant_runtime.supports_autonomous_execution ), require_coordinated_reset=bool(manifest.time and manifest.time.supports_coordinated_participant_reset), + require_execution_control=bool( + manifest.participant_runtime and manifest.participant_runtime.supports_execution_control + ), + require_bounded_concurrency=bool( + manifest.participant_runtime and manifest.participant_runtime.supports_bounded_concurrency + ), ) _validate_time_runtime_methods( time_runtime, @@ -225,6 +231,8 @@ def _validate_participant_runtime_methods( *, require_autonomous_binding: bool, require_coordinated_reset: bool, + require_execution_control: bool, + require_bounded_concurrency: bool, ) -> None: _require_invokable_method( participant_runtime, @@ -296,6 +304,33 @@ def _validate_participant_runtime_methods( method_name="reset_many", invocation_args=((sample_request,), sample_snapshot), ) + if require_execution_control: + _require_invokable_method( + participant_runtime, + label="participant_runtime", + method_name="control_execution", + invocation_args=(sample_request, sample_snapshot), + ) + _require_invokable_method( + participant_runtime, + label="participant_runtime", + method_name="execution_state", + invocation_args=( + "participant.autonomous-execution.registry-probe", + sample_snapshot, + ), + ) + if require_bounded_concurrency: + _require_invokable_method( + participant_runtime, + label="participant_runtime", + method_name="admit_actions_concurrently", + invocation_args=( + (sample_admission_request, sample_admission_request), + sample_snapshot, + 2, + ), + ) def _validate_time_runtime_methods( diff --git a/implementations/python/tests/participant_execution_test_backend.py b/implementations/python/tests/participant_execution_test_backend.py new file mode 100644 index 000000000..d20e1e801 --- /dev/null +++ b/implementations/python/tests/participant_execution_test_backend.py @@ -0,0 +1,203 @@ +"""Native in-memory execution controller used by backend contract tests.""" + +from __future__ import annotations + +import threading +import time + +from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot + + +class NativeParticipantExecutionController: + """Backend-owned lifecycle implementation with observed native state.""" + + def __init__(self) -> None: + self._condition = threading.Condition() + self._active = 0 + self._accepting = True + self._resources_allocated = True + + def begin_action(self) -> None: + with self._condition: + if not self._accepting: + raise RuntimeError("native participant execution is not accepting work") + self._active += 1 + + def finish_action(self) -> None: + with self._condition: + self._active -= 1 + self._condition.notify_all() + + @staticmethod + def _failure( + request: ParticipantExecutionControlRequestModel, + snapshot: RuntimeSnapshot, + code: str, + ) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code=code, + domain="participant", + address=request.execution_scope_ref, + message="Native participant execution control failed.", + ) + ], + ) + + def _drain( + self, + request: ParticipantExecutionControlRequestModel, + snapshot: RuntimeSnapshot, + ) -> ApplyResult | None: + if request.action != "drain": + return None + assert request.timeout_seconds is not None + deadline = time.monotonic() + request.timeout_seconds + with self._condition: + self._accepting = False + while self._active: + remaining = deadline - time.monotonic() + if remaining <= 0 or not self._condition.wait(timeout=remaining): + return self._failure( + request, + snapshot, + "runtime.participant-execution-drain-timeout", + ) + return None + + @staticmethod + def _scheduler_updates( + snapshot: RuntimeSnapshot, + state: ParticipantExecutionServiceStateModel, + lifecycle: str, + *, + reset: bool, + ) -> dict[str, dict[str, object]]: + scheduler_states = dict(snapshot.participant_autonomous_execution_states) + for ref in state.scheduler_state_refs: + payload = scheduler_states.get(ref) + if payload is None: + continue + scheduler_state = ParticipantAutonomousExecutionStateModel.model_validate(payload) + updates: dict[str, object] = {"lifecycle_state": lifecycle} + if reset: + updates.update( + attempted_actions=0, + succeeded_actions=0, + failed_actions=0, + in_flight=0, + ) + scheduler_states[ref] = scheduler_state.model_copy(update=updates).model_dump(mode="json") + return scheduler_states + + def control( + self, + request: ParticipantExecutionControlRequestModel, + snapshot: RuntimeSnapshot, + ) -> ApplyResult: + payload = snapshot.participant_execution_services.get(request.execution_scope_ref) + if payload is None: + return self._failure( + request, + snapshot, + "runtime.participant-execution-not-found", + ) + state = ParticipantExecutionServiceStateModel.model_validate(payload) + if state.generation != request.expected_generation: + return self._failure( + request, + snapshot, + "runtime.participant-execution-stale-generation", + ) + drain_failure = self._drain(request, snapshot) + if drain_failure is not None: + return drain_failure + transition = self._transition(request, state) + if transition is None: + return self._failure( + request, + snapshot, + "runtime.participant-execution-transition-invalid", + ) + lifecycle, readiness, accepting, generation = transition + with self._condition: + self._accepting = accepting + if request.action == "start": + self._resources_allocated = True + if request.action == "teardown": + self._resources_allocated = False + active = self._active + evidence_ref = f"evidence:{request.execution_scope_ref}:backend-native-{request.action}:generation-{generation}" + next_state = state.model_copy( + update={ + "desired_lifecycle": lifecycle, + "observed_lifecycle": lifecycle, + "generation": generation, + "observed_generation": generation, + "readiness": readiness, + "accepting_new_work": accepting, + "draining": False, + "quiescent": active == 0, + "reserved": 0, + "in_flight": active, + "resources_released": not self._resources_allocated, + "last_transition_ref": ( + f"operation:{request.execution_scope_ref}:backend-native-{request.action}:generation-{generation}" + ), + "evidence_refs": tuple(dict.fromkeys([*state.evidence_refs, evidence_ref])), + } + ) + services = dict(snapshot.participant_execution_services) + services[request.execution_scope_ref] = next_state.model_dump(mode="json") + scheduler_lifecycle = "paused" if lifecycle == "paused" else "running" + scheduler_states = self._scheduler_updates( + snapshot, + state, + scheduler_lifecycle, + reset=request.action == "reset", + ) + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_execution_services=services, + participant_autonomous_execution_states=scheduler_states, + ), + changed_addresses=[ + request.execution_scope_ref, + *[ref for ref in state.scheduler_state_refs if ref in scheduler_states], + ], + ) + + @staticmethod + def _transition( + request: ParticipantExecutionControlRequestModel, + state: ParticipantExecutionServiceStateModel, + ) -> tuple[str, str, bool, int] | None: + action = request.action + lifecycle = state.observed_lifecycle + if action == "start" and lifecycle == "stopped": + return "running", "ready", True, state.generation + if action == "pause" and lifecycle == "running": + return "paused", "not_ready", False, state.generation + if action == "resume" and lifecycle == "paused": + return "running", "ready", True, state.generation + if action == "drain" and lifecycle in {"running", "paused", "quiescent"}: + return "quiescent", "not_ready", False, state.generation + if action == "reset" and lifecycle == "quiescent": + return "running", "ready", True, state.generation + 1 + if action == "teardown" and lifecycle == "quiescent": + return "terminated", "not_ready", False, state.generation + return None + + +__all__ = ["NativeParticipantExecutionController"] diff --git a/implementations/python/tests/test_dsl_437_benign_participant_execution.py b/implementations/python/tests/test_dsl_437_benign_participant_execution.py index 3a25879ea..9411ab290 100644 --- a/implementations/python/tests/test_dsl_437_benign_participant_execution.py +++ b/implementations/python/tests/test_dsl_437_benign_participant_execution.py @@ -9,11 +9,17 @@ import pytest import yaml +from implementations.python.tests.participant_execution_test_backend import ( + NativeParticipantExecutionController, +) from raes._errors import SDLValidationError from raes.parser import parse_sdl from raes.participant_behavior import ParticipantFailureClass from raes.participant_execution import ParticipantAutonomousExecutionPolicyV2 -from raes_backend_protocols.capabilities import ParticipantFeatureSupport +from raes_backend_protocols.capabilities import ( + ParticipantExecutionBinding, + ParticipantFeatureSupport, +) from raes_backend_protocols.capability_admission import participant_autonomous_execution_capability_gaps from raes_backend_protocols.manifest import backend_manifest_from_v2_model, backend_manifest_v2_model from raes_backend_protocols.participant_runtime_base import BaseParticipantRuntime @@ -27,6 +33,7 @@ ParticipantImplementationSelectionModel, RuntimeSnapshotEnvelopeModel, ) +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel from raes_contracts.contracts.random_stream import ( GovernedEntropyRefModel, PublicSeedModel, @@ -291,6 +298,10 @@ class _NativeParticipantRuntime(BaseParticipantRuntime): def __init__(self) -> None: super().__init__() self.native_actions: list[str] = [] + self._execution_controller = NativeParticipantExecutionController() + + def control_execution(self, request, snapshot): + return self._execution_controller.control(request, snapshot) def _model_action( self, @@ -299,26 +310,30 @@ def _model_action( *, episode_id: str, ) -> ParticipantNativeActionExecution: - self.native_actions.append(request.action_instance_id) - metadata = dict(snapshot.metadata) - metadata["last_native_action"] = request.action_instance_id - return ParticipantNativeActionExecution( - apply_result=ApplyResult( - success=True, - snapshot=snapshot.with_entries(dict(snapshot.entries), metadata=metadata), - changed_addresses=["native.service.customer-portal"], - ), - action_result=ParticipantActionResultModel( - status="succeeded", - participant_address=request.participant_address, - episode_id=episode_id, - action_instance_id=request.action_instance_id, - action_contract_address=request.action_contract_address, - observation_point=request.temporal_contexts[0].observation_point, - observations=["customer portal responded"], - evidence_refs=[], - ), - ) + self._execution_controller.begin_action() + try: + self.native_actions.append(request.action_instance_id) + metadata = dict(snapshot.metadata) + metadata["last_native_action"] = request.action_instance_id + return ParticipantNativeActionExecution( + apply_result=ApplyResult( + success=True, + snapshot=snapshot.with_entries(dict(snapshot.entries), metadata=metadata), + changed_addresses=["native.service.customer-portal"], + ), + action_result=ParticipantActionResultModel( + status="succeeded", + participant_address=request.participant_address, + episode_id=episode_id, + action_instance_id=request.action_instance_id, + action_contract_address=request.action_contract_address, + observation_point=request.temporal_contexts[0].observation_point, + observations=["customer portal responded"], + evidence_refs=[], + ), + ) + finally: + self._execution_controller.finish_action() def bind_autonomous_action( self, @@ -597,7 +612,7 @@ def _autonomous_manifest(runtime_model: object) -> object: ), max_autonomous_participants=8, max_autonomous_action_attempts=max(policy.max_action_attempts for policy in policies), - max_autonomous_in_flight=1, + max_autonomous_in_flight=max(policy.max_in_flight for policy in policies), max_autonomous_occurrences=max( (policy.max_occurrences or policy.max_action_attempts for policy in policies), default=1, @@ -608,6 +623,32 @@ def _autonomous_manifest(runtime_model: object) -> object: ) or 1, max_autonomous_burst_size=max((policy.max_burst_size for policy in policies), default=1), + execution_bindings=tuple( + ParticipantExecutionBinding( + binding_id=(f"{policy.address}.binding.{binding.action_contract_address.rsplit('.', 1)[-1]}"), + action_contract_address=binding.action_contract_address, + target_addresses=binding.target_addresses, + participant_implementation_ref=(binding.participant_implementation_ref), + constraint_refs=policy.temporal_constraint_addresses, + evidence_refs=(f"evidence:{policy.address}:native-execution",), + max_action_attempts=binding.max_action_attempts, + max_in_flight=binding.max_in_flight, + timeout_seconds=30, + max_retries=max( + policy.action_candidate_max_retries, + default=0, + ), + ) + for policy in policies + for binding in policy.execution_bindings + ), + supports_execution_control=True, + supported_execution_control_actions=frozenset( + {"start", "pause", "resume", "drain", "reset", "teardown"} + ), + supports_bounded_concurrency=True, + max_execution_services=8, + max_concurrent_actions=8, ), ), ) @@ -846,6 +887,11 @@ def test_planner_enforces_required_participant_features_and_exact_targets() -> N runtime_model, _ = _compiled() manifest = _autonomous_manifest(runtime_model) assert manifest.participant_runtime is not None + unsupported_target = "provision.node.unsupported.service.http" + unsupported_bindings = tuple( + replace(binding, target_addresses=(unsupported_target,)) + for binding in manifest.participant_runtime.execution_bindings + ) unsupported = replace( manifest, capabilities=replace( @@ -853,7 +899,8 @@ def test_planner_enforces_required_participant_features_and_exact_targets() -> N participant_runtime=replace( manifest.participant_runtime, supported_behavior_features=frozenset({"autonomous_execution"}), - supported_autonomous_target_addresses=frozenset(), + supported_autonomous_target_addresses=frozenset({unsupported_target}), + execution_bindings=unsupported_bindings, ), ), ) @@ -1555,7 +1602,7 @@ def test_scheduler_executes_native_actions_and_persists_shared_time_readback() - ) assert (state.attempted_actions, state.succeeded_actions, state.next_tick) == (1, 1, 10) assert state.time_segment == 0 - assert state.policy_digest == "sha256:d7c496e43e5782f4459b4b74b62e8b15b4f6d1ddba0ab4044c7fa3f406f502ef" + assert state.policy_digest == "sha256:e89d1e56a9c09893581257b39a595634599b4d3a8140cfb9ecba1a6df889f35b" events = first.snapshot.participant_behavior_history[state.participant_address] assert [event["event_type"] for event in events] == [ "action_attempted", @@ -1583,6 +1630,19 @@ def test_scheduler_executes_native_actions_and_persists_shared_time_readback() - paused.participant_autonomous_execution_states[state_key] ) assert paused_state.lifecycle_state == "paused" + paused_service = ParticipantExecutionServiceStateModel.model_validate( + paused.participant_execution_services[policy.address] + ) + assert paused_service.observed_lifecycle == "paused" + assert paused_service.accepting_new_work is False + paused_due = scheduler.run_due( + [policy], + runtime_model.time_model, + participant_runtime, + paused, + ) + assert paused_due.success + assert len(participant_runtime.native_actions) == 1 resumed = scheduler.set_clock_lifecycle(paused, policy.clock_address, "running").snapshot advanced = coordinator.advance(resumed, policy.clock_address, ticks=10) @@ -2084,6 +2144,13 @@ def failing_advance(_clock_address: str, _ticks: int) -> ApplyResult: assert driver.failure is not None assert driver.failure.diagnostics[0].code == "runtime.participant-clock-driver-failed" + execution_state = ParticipantExecutionServiceStateModel.model_validate( + driver.failure.snapshot.participant_execution_services[policy.address] + ) + assert execution_state.health == "degraded" + assert execution_state.readiness == "not_ready" + assert execution_state.accepting_new_work is False + assert execution_state.pacing_deviation_refs assert driver.stop() diff --git a/implementations/python/tests/test_issue_898_participant_execution_control.py b/implementations/python/tests/test_issue_898_participant_execution_control.py new file mode 100644 index 000000000..3a7cc5948 --- /dev/null +++ b/implementations/python/tests/test_issue_898_participant_execution_control.py @@ -0,0 +1,670 @@ +"""Issue #898 portable participant execution and lifecycle control.""" + +from __future__ import annotations + +import threading +from dataclasses import replace + +import pytest +import yaml +from implementations.python.tests.test_dsl_437_benign_participant_execution import ( + _autonomous_manifest, + _compiled, + _NativeParticipantRuntime, + _scenario_yaml, +) +from implementations.python.tests.test_runtime_control_plane_api import _test_security +from raes import parse_sdl +from raes_backend_protocols.capability_admission import ( + participant_autonomous_execution_capability_gaps, +) +from raes_backend_protocols.manifest import ( + backend_manifest_from_v2_model, + backend_manifest_v2_model, +) +from raes_backend_protocols.participant_runtime_base import BaseParticipantRuntime +from raes_backend_stubs.stubs import create_stub_target +from raes_conformance.conformance.profiles import BackendCapabilityProfile +from raes_conformance.conformance.target_probes import _target_adapter_cases +from raes_contracts.contracts import ParticipantTemporalRuntimeContextModel +from raes_contracts.contracts.participant_execution import ( + ParticipantExecutionBindingModel, + ParticipantExecutionControlRequestModel, + ParticipantExecutionServiceStateModel, +) +from raes_contracts.participant_episode import ParticipantEpisodeInitializeRequest +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_processor.compiler import compile_runtime_model +from raes_runtime.control_plane import RuntimeControlPlane +from raes_runtime.control_plane_api import create_control_plane_app +from raes_runtime.manager import RuntimeManager +from raes_runtime.participant_scheduler_concurrency import ( + participant_generation_commit_diagnostic, +) +from starlette.testclient import TestClient + + +def _binding() -> ParticipantExecutionBindingModel: + return ParticipantExecutionBindingModel( + binding_id="green-login-service", + action_contract_address="participant.action-contract.portal-login", + target_addresses=("service.customer-portal.https",), + participant_implementation_ref="participant-implementation-manifests.green-worker.v1", + constraint_refs=("constraint.green-login.capacity",), + evidence_refs=("evidence.green-login.native-action",), + max_action_attempts=24, + max_in_flight=2, + timeout_seconds=30, + max_retries=2, + ) + + +def _service_state( + **updates: object, +) -> ParticipantExecutionServiceStateModel: + payload: dict[str, object] = { + "execution_scope_ref": "participant.autonomous-execution.green-activity", + "policy_address": "participant.autonomous-execution.green-activity", + "desired_lifecycle": "stopped", + "observed_lifecycle": "stopped", + "generation": 0, + "observed_generation": 0, + "health": "healthy", + "readiness": "not_ready", + "accepting_new_work": False, + "draining": False, + "quiescent": True, + "resources_released": False, + "policy_digest": "sha256:" + "1" * 64, + "binding_digest": "sha256:" + "2" * 64, + "time_declaration_digest": "sha256:" + "3" * 64, + "scheduler_state_refs": ("participant.autonomous-execution.green-activity.state.participant.behavior.green",), + "capacity": 2, + "reserved": 0, + "in_flight": 0, + "last_transition_ref": "operation.participant-execution.configure.0", + "evidence_refs": ("evidence.participant-execution.health.0",), + } + payload.update(updates) + return ParticipantExecutionServiceStateModel.model_validate(payload) + + +def test_execution_binding_is_relational_and_finite() -> None: + binding = _binding() + + assert binding.action_contract_address == "participant.action-contract.portal-login" + assert binding.target_addresses == ("service.customer-portal.https",) + assert binding.max_in_flight == 2 + + with pytest.raises(ValueError, match="target_addresses"): + _binding().model_copy(update={"target_addresses": ()}).model_validate( + _binding().model_copy(update={"target_addresses": ()}).model_dump() + ) + with pytest.raises(ValueError, match="max_in_flight"): + ParticipantExecutionBindingModel.model_validate({**_binding().model_dump(), "max_in_flight": 0}) + + +def test_execution_control_request_requires_generation_and_bounded_drain() -> None: + request = ParticipantExecutionControlRequestModel( + execution_scope_ref="participant.autonomous-execution.green-activity", + action="drain", + expected_generation=3, + timeout_seconds=10, + ) + + assert request.expected_generation == 3 + assert request.timeout_seconds == 10 + + with pytest.raises(ValueError, match="timeout_seconds"): + ParticipantExecutionControlRequestModel( + execution_scope_ref=request.execution_scope_ref, + action="drain", + expected_generation=3, + ) + with pytest.raises(ValueError, match="only valid for drain"): + ParticipantExecutionControlRequestModel( + execution_scope_ref=request.execution_scope_ref, + action="pause", + expected_generation=3, + timeout_seconds=10, + ) + + +def test_execution_readback_separates_health_readiness_and_lifecycle() -> None: + state = _service_state( + desired_lifecycle="running", + observed_lifecycle="running", + generation=3, + observed_generation=3, + readiness="ready", + accepting_new_work=True, + last_transition_ref="operation.participant-execution.start.3", + evidence_refs=("evidence.participant-execution.health.3",), + ) + + assert state.health == "healthy" + assert state.readiness == "ready" + assert state.accepting_new_work is True + + with pytest.raises(ValueError, match="accepting_new_work"): + ParticipantExecutionServiceStateModel.model_validate( + { + **state.model_dump(), + "observed_lifecycle": "paused", + "accepting_new_work": True, + } + ) + with pytest.raises(ValueError, match="observed_generation"): + ParticipantExecutionServiceStateModel.model_validate({**state.model_dump(), "observed_generation": 4}) + + +def test_compiler_preserves_exact_action_to_target_execution_binding() -> None: + runtime_model = compile_runtime_model(parse_sdl(_scenario_yaml())) + policy = runtime_model.behavior_specifications[ + "participant.behavior-specification.participant-behavior" + ].autonomous_execution + + assert policy is not None + assert len(policy.execution_bindings) == 1 + binding = policy.execution_bindings[0] + assert binding.action_contract_address == "participant.action-contract.probe-customer-portal-login" + assert "provision.node.customer-portal.service.http" in binding.target_addresses + assert binding.participant_implementation_ref == policy.participant_implementation_ref + assert binding.max_in_flight == policy.max_in_flight + + +def test_autonomous_manifest_requires_execution_control_and_relational_bindings() -> None: + runtime_model, policy = _compiled() + manifest = _autonomous_manifest(runtime_model) + capability = manifest.participant_runtime + + assert capability is not None + assert capability.supports_execution_control is True + assert capability.supports_bounded_concurrency is True + assert capability.supported_execution_control_actions == frozenset( + {"start", "pause", "resume", "drain", "reset", "teardown"} + ) + assert capability.execution_bindings[0].action_contract_address == (policy.action_contract_addresses[0]) + assert capability.execution_bindings[0].target_addresses == (policy.execution_bindings[0].target_addresses) + + wire = backend_manifest_v2_model(manifest) + restored = backend_manifest_from_v2_model(wire) + assert restored.participant_runtime == capability + + +def test_admission_rejects_cartesian_action_and_target_declarations() -> None: + runtime_model, policy = _compiled() + manifest = _autonomous_manifest(runtime_model) + capability = manifest.participant_runtime + assert capability is not None + binding = capability.execution_bindings[0] + portal_target = "provision.node.customer-portal.service.http" + assert portal_target in binding.target_addresses + weakened_binding = replace( + binding, + target_addresses=tuple(target for target in binding.target_addresses if target != portal_target), + ) + weakened_manifest = replace( + manifest, + capabilities=replace( + manifest.capabilities, + participant_runtime=replace( + capability, + execution_bindings=(weakened_binding,), + ), + ), + ) + + gaps = participant_autonomous_execution_capability_gaps( + weakened_manifest, + (policy,), + runtime_model.time_model, + ) + + assert any("execution binding" in gap and portal_target in gap for gap in gaps) + + +def test_execution_lifecycle_rejects_stale_generation_and_preserves_evidence() -> None: + runtime = _NativeParticipantRuntime() + scope = "participant.autonomous-execution.green-activity" + snapshot = RuntimeSnapshot(participant_execution_services={scope: _service_state().model_dump(mode="json")}) + + started = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="start", + expected_generation=0, + ), + snapshot, + ) + assert started.success is True + state = ParticipantExecutionServiceStateModel.model_validate(started.snapshot.participant_execution_services[scope]) + assert state.observed_lifecycle == "running" + assert state.readiness == "ready" + assert state.accepting_new_work is True + + stale = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="pause", + expected_generation=1, + ), + started.snapshot, + ) + assert stale.success is False + assert stale.snapshot is started.snapshot + assert stale.diagnostics[0].code == "runtime.participant-execution-stale-generation" + + paused = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="pause", + expected_generation=0, + ), + started.snapshot, + ) + resumed = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="resume", + expected_generation=0, + ), + paused.snapshot, + ) + drained = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="drain", + expected_generation=0, + timeout_seconds=1, + ), + resumed.snapshot, + ) + reset = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="reset", + expected_generation=0, + ), + drained.snapshot, + ) + reset_state = ParticipantExecutionServiceStateModel.model_validate( + reset.snapshot.participant_execution_services[scope] + ) + assert reset_state.generation == 1 + assert reset_state.observed_generation == 1 + assert reset_state.observed_lifecycle == "running" + assert reset_state.evidence_refs + + drained_again = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="drain", + expected_generation=1, + timeout_seconds=1, + ), + reset.snapshot, + ) + torn_down = runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="teardown", + expected_generation=1, + ), + drained_again.snapshot, + ) + final_state = ParticipantExecutionServiceStateModel.model_validate( + torn_down.snapshot.participant_execution_services[scope] + ) + assert final_state.observed_lifecycle == "terminated" + assert final_state.resources_released is True + assert final_state.accepting_new_work is False + + +def test_base_runtime_does_not_synthesize_backend_lifecycle_success() -> None: + assert not callable(getattr(BaseParticipantRuntime(), "control_execution", None)) + + +def test_native_drain_waits_for_in_flight_work_within_bound() -> None: + runtime = _NativeParticipantRuntime() + scope = "participant.autonomous-execution.green-activity" + snapshot = RuntimeSnapshot( + participant_execution_services={ + scope: _service_state( + desired_lifecycle="running", + observed_lifecycle="running", + readiness="ready", + accepting_new_work=True, + ).model_dump(mode="json") + } + ) + runtime._execution_controller.begin_action() + outcome: list[object] = [] + + def drain() -> None: + outcome.append( + runtime.control_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="drain", + expected_generation=0, + timeout_seconds=1, + ), + snapshot, + ) + ) + + thread = threading.Thread(target=drain) + thread.start() + thread.join(timeout=0.05) + assert thread.is_alive() + runtime._execution_controller.finish_action() + thread.join(timeout=1) + + assert len(outcome) == 1 + assert outcome[0].success is True + + +def _generation_bound_request( + runtime: _NativeParticipantRuntime, + snapshot: RuntimeSnapshot, + *, + generation: int, +): + scope = "participant.autonomous-execution.green-activity" + request = runtime.bind_autonomous_action( + "participant.behavior.green", + "participant.action-contract.portal-login", + "participant.observation-boundary.portal", + "participant-implementation-manifests.green-worker.v1", + f"green-login-generation-{generation}", + ( + ParticipantTemporalRuntimeContextModel( + temporal_contract_id="time.constraint.green-login", + time_domain="scenario_time", + clock_authority="time.clock.scenario", + event_points=["submit", "start", "end", "observed"], + observation_point="time.clock.scenario@segment=0,tick=0", + reset_boundary="time.clock.scenario:segment=0", + ), + ), + snapshot, + ) + return replace( + request, + target_addresses=("service.customer-portal.https",), + execution_scope_ref=scope, + execution_generation=generation, + requires_terminal_outcome=True, + ) + + +def test_generation_fence_rejects_stale_work_before_native_execution() -> None: + runtime = _NativeParticipantRuntime() + scope = "participant.autonomous-execution.green-activity" + snapshot = RuntimeSnapshot( + participant_execution_services={ + scope: _service_state( + desired_lifecycle="running", + observed_lifecycle="running", + generation=1, + observed_generation=1, + readiness="ready", + accepting_new_work=True, + ).model_dump(mode="json") + } + ) + snapshot = runtime.initialize( + ParticipantEpisodeInitializeRequest(participant_address="participant.behavior.green"), + snapshot, + ).snapshot + + result = runtime.admit_action( + _generation_bound_request(runtime, snapshot, generation=0), + snapshot, + ) + + assert result.success is False + assert result.diagnostics[0].code == ("runtime.participant-execution-stale-work") + assert runtime.native_actions == [] + + +def test_generation_fence_uses_authoritative_serialized_commit_state() -> None: + scope = "participant.autonomous-execution.green-activity" + runtime = _NativeParticipantRuntime() + snapshot = RuntimeSnapshot( + participant_execution_services={ + scope: _service_state( + desired_lifecycle="running", + observed_lifecycle="running", + generation=1, + observed_generation=1, + readiness="ready", + accepting_new_work=True, + ).model_dump(mode="json") + } + ) + snapshot = runtime.initialize( + ParticipantEpisodeInitializeRequest(participant_address="participant.behavior.green"), + snapshot, + ).snapshot + request = _generation_bound_request(runtime, snapshot, generation=1) + worker_result = runtime.admit_action(request, snapshot) + service = ParticipantExecutionServiceStateModel.model_validate(snapshot.participant_execution_services[scope]) + services = dict(snapshot.participant_execution_services) + services[scope] = service.model_copy(update={"generation": 2, "observed_generation": 2}).model_dump(mode="json") + authoritative = snapshot.with_entries( + dict(snapshot.entries), + participant_execution_services=services, + ) + + diagnostic = participant_generation_commit_diagnostic(request, authoritative) + + assert worker_result.success is True + worker_service = ParticipantExecutionServiceStateModel.model_validate( + worker_result.snapshot.participant_execution_services[scope] + ) + assert worker_service.generation == 1 + assert diagnostic is not None + assert diagnostic.code == "runtime.participant-execution-stale-completion" + + +class _OverlappingParticipantRuntime(_NativeParticipantRuntime): + def __init__(self) -> None: + super().__init__() + self._barrier = threading.Barrier(2) + self._active_lock = threading.Lock() + self._active = 0 + self.peak_active = 0 + + def _model_action(self, request, snapshot, *, episode_id): + with self._active_lock: + self._active += 1 + self.peak_active = max(self.peak_active, self._active) + try: + self._barrier.wait(timeout=2) + return super()._model_action( + request, + snapshot, + episode_id=episode_id, + ) + finally: + with self._active_lock: + self._active -= 1 + + +def _two_green_participant_scenario(): + payload = yaml.safe_load(_scenario_yaml()) + payload["entities"]["enterprise-participant-2"] = { + **payload["entities"]["enterprise-participant"], + "mission": "Perform a second bounded green participant action.", + } + payload["agents"]["participant-agent-2"] = { + **payload["agents"]["participant-agent"], + "entity": "enterprise-participant-2", + "description": "Second ordinary green participant.", + } + specification = payload["behavior_specifications"]["participant-behavior"] + specification["participant_refs"].append("participant-agent-2") + specification["autonomous_execution"]["max_in_flight"] = 2 + return parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + + +def test_scheduler_executes_two_due_green_participants_with_bounded_overlap() -> None: + scenario = _two_green_participant_scenario() + runtime_model = compile_runtime_model(scenario) + participant_runtime = _OverlappingParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target) + + applied = manager.apply(manager.plan(scenario)) + + assert applied.success is True + assert participant_runtime.peak_active == 2 + assert len(participant_runtime.native_actions) == 2 + states = [ + ParticipantExecutionServiceStateModel.model_validate(payload) + for payload in applied.snapshot.participant_execution_services.values() + ] + assert len(states) == 1 + assert states[0].capacity == 2 + assert states[0].reserved == 0 + assert states[0].in_flight == 0 + + +def test_control_plane_exposes_authenticated_generation_bound_execution_control() -> None: + scope = "participant.autonomous-execution.green-activity" + initial_snapshot = RuntimeSnapshot(participant_execution_services={scope: _service_state().model_dump(mode="json")}) + target = replace( + create_stub_target(), + participant_runtime=_NativeParticipantRuntime(), + ) + control_plane = RuntimeControlPlane(target, initial_snapshot=initial_snapshot) + app = create_control_plane_app( + control_plane, + security=_test_security(target.name), + ) + backend_headers = { + "x-aces-client-verified": "true", + "x-aces-client-identity": "backend-service", + "idempotency-key": "start-generation-0", + } + auditor_headers = {"authorization": "Bearer test-auditor-token"} + + with TestClient(app) as client: + unauthenticated = client.post( + f"/participant-executions/{scope}/control", + json={"action": "start", "expected_generation": 0}, + ) + started = client.post( + f"/participant-executions/{scope}/control", + json={"action": "start", "expected_generation": 0}, + headers=backend_headers, + ) + repeated = client.post( + f"/participant-executions/{scope}/control", + json={"action": "start", "expected_generation": 0}, + headers=backend_headers, + ) + readback = client.get( + f"/participant-executions/{scope}", + headers=auditor_headers, + ) + status = client.get( + f"/operations/{started.json()['operation_id']}", + headers=auditor_headers, + ) + + assert unauthenticated.status_code == 401 + assert started.status_code == 200 + assert repeated.json()["operation_id"] == started.json()["operation_id"] + assert status.json()["state"] == "succeeded" + assert readback.status_code == 200 + assert readback.json()["observed_lifecycle"] == "running" + assert readback.json()["observed_generation"] == 0 + assert readback.json()["health"] == "healthy" + assert readback.json()["readiness"] == "ready" + + +def test_control_plane_rejects_synthetic_lifecycle_readback() -> None: + class _SyntheticControlRuntime(_NativeParticipantRuntime): + def control_execution(self, request, snapshot): + return ApplyResult( + success=True, + snapshot=snapshot, + changed_addresses=[request.execution_scope_ref], + ) + + scope = "participant.autonomous-execution.green-activity" + snapshot = RuntimeSnapshot(participant_execution_services={scope: _service_state().model_dump(mode="json")}) + target = replace( + create_stub_target(), + participant_runtime=_SyntheticControlRuntime(), + ) + control_plane = RuntimeControlPlane(target, initial_snapshot=snapshot) + + receipt = control_plane.control_participant_execution( + ParticipantExecutionControlRequestModel( + execution_scope_ref=scope, + action="start", + expected_generation=0, + ) + ) + status = control_plane.get_operation(receipt.operation_id) + + assert status is not None + assert status.state.value == "failed" + assert status.diagnostics[0].code == ("runtime.participant-execution-readback-invalid") + + +def test_live_conformance_conditionally_proves_autonomous_action_and_lifecycle() -> None: + runtime_model = compile_runtime_model(_two_green_participant_scenario()) + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=_NativeParticipantRuntime(), + ) + + cases = _target_adapter_cases( + target, + BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE, + ) + participant_execution_cases = tuple(case for case in cases if case.name.startswith("participant-execution-")) + + assert participant_execution_cases + assert all(case.passed for case in participant_execution_cases) + assert any( + case.name == "participant-execution-bounded-native-actions" and case.evidence_refs + for case in participant_execution_cases + ) + + +def test_live_conformance_rejects_autonomous_claim_without_executable_behavior() -> None: + class _InertParticipantRuntime(_NativeParticipantRuntime): + def _model_action(self, request, snapshot, *, episode_id): + execution = super()._model_action( + request, + snapshot, + episode_id=episode_id, + ) + return replace(execution, action_result=None) + + runtime_model = compile_runtime_model(_two_green_participant_scenario()) + target = replace( + create_stub_target(), + manifest=_autonomous_manifest(runtime_model), + participant_runtime=_InertParticipantRuntime(), + ) + + cases = _target_adapter_cases( + target, + BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE, + ) + action_case = next(case for case in cases if case.name == "participant-execution-bounded-native-actions") + + assert action_case.passed is False + assert action_case.diagnostics[0].code == ("conformance.participant-execution-action-inert") diff --git a/specs/formal/participant-semantics/autonomous-execution.md b/specs/formal/participant-semantics/autonomous-execution.md index d58db080b..7f038ffde 100644 --- a/specs/formal/participant-semantics/autonomous-execution.md +++ b/specs/formal/participant-semantics/autonomous-execution.md @@ -121,6 +121,17 @@ compilation. ## Execution And Evidence +Compilation derives an exact relation + +\[ +R_P \subseteq Q_P \times Targets \times Implementations +\] + +from each action contract's effect and precondition support references. A +backend execution binding must cover each required tuple in \(R_P\). Declaring +all actions and all targets separately does not establish the Cartesian +product and is insufficient for admission. + An action may commit only in this order: 1. resolve the run-selected participant implementation; @@ -146,6 +157,21 @@ match the selected participant implementation. `stop` marks the scheduler failed; `continue` advances the bounded attempt counter and cadence. +Every autonomous action request carries the resolved native target addresses, +execution-service scope, and execution generation. The runtime checks that the +service is running, ready, accepting work, and still at that generation before +calling the native adapter. It checks the generation again on the returned +snapshot before committing history. A stale work item or completion is +rejected without committing native state. + +When at least two participants are due and the admitted policy and backend +limits permit it, native calls execute concurrently against one immutable +predecessor. Results commit one at a time. For every changed portable map +entry, the commit requires the current value to equal either the predecessor +or the incoming value; otherwise it reports a concurrent-commit conflict. +Scheduler attempt/in-flight counters are reserved before dispatch and settled +after each serialized commit. + For v2, a retry is admitted only when the typed terminal failure class is in the selected candidate's declared retry set and the per-occurrence retry and global attempt bounds both remain. Each retry has a distinct attempt id and @@ -163,6 +189,28 @@ derived keys, raw blocks, or backend-private objects. ## Lifecycle +One execution-service scope owns each autonomous policy. Its portable state +separates desired/observed lifecycle, generation, health, readiness, work +admission, finite capacity, reservation/in-flight counters, quiescence, +resource release, shared-time provenance, and evidence. Legal control +transitions are: + +```text +stopped --start--> running --pause--> paused --resume--> running +running|paused --drain(timeout)--> quiescent +quiescent --reset(generation+1)--> running +quiescent --teardown--> terminated +``` + +Drain rejects new work and succeeds only after reserved and in-flight work are +zero within its finite timeout. Teardown is idempotent after termination. +Every transition publishes an operation reference and evidence reference. +Portable fields are readback, not an implementation of these operations. +The backend owns the native transition, scheduler/shared-time coordination, +bounded wait, and resource release. The control boundary rejects nominal +success unless the backend returns the action-specific observed state, a +changed operation reference, and new evidence. + Pause changes non-terminal scheduler states on the governed clock to `paused`; resume returns them to `running`. Reset begins a new shared-time segment, resets each bound participant episode, and restores the initial cadence, @@ -192,6 +240,14 @@ generations cannot alias prior draws. Participant/service state changes remain owned by native action results and existing episode/reset contracts; scheduler timestamps alone make no causal or rollback claim. +The service readback binds the policy, execution relation, and admitted +shared-time declaration by digest and names its scheduler states. Shared-clock +pause/resume changes both scheduler and execution-service readiness. A +shared-clock reset advances the execution generation. Loss of runtime wall +pacing marks the service degraded, not ready, and paused and appends an +explicit pacing-deviation evidence reference; it is never silently treated as +successful timing. + ## Backend Admission Let backend capability \(K\) declare supported strategies and finite maxima. @@ -224,11 +280,22 @@ V_P \in K.observationBoundaries targets(Q_P) \subseteq K.targets \] +\[ +R_P \subseteq K.executionBindings +\] + and every parent behavior feature required by \(P\) must be in the backend feature set. A reset-capable policy additionally requires the coordinated participant-reset capability and runtime method. Runtime state, typed native action outcome, history, and backend evidence establish what occurred. +Autonomous admission also requires all six execution-control actions, +`supports_bounded_concurrency`, positive execution-service capacity, and +`max_concurrent_actions \ge 2`. Conditional live conformance executes two +native actions and the lifecycle sequence. Schema-valid declarations without +typed native outcomes, operation accounting, service readback, or transition +evidence fail conformance. + V2 additionally requires exact admission of: - `participant-autonomous-execution/v2`; diff --git a/specs/formal/runtime-contracts/participant-backend-contracts.md b/specs/formal/runtime-contracts/participant-backend-contracts.md index db5696d50..927e027c3 100644 --- a/specs/formal/runtime-contracts/participant-backend-contracts.md +++ b/specs/formal/runtime-contracts/participant-backend-contracts.md @@ -88,6 +88,29 @@ and the term-level evidence criteria in This design ratifies that surface without amendment. See the API-405 section of `specs/formal/runtime-contracts/README.md`. +### Portable autonomous execution services + +Issue #898 extends only the `autonomous_execution` portion of the ratified +participant capability block. Autonomous support now includes relational +`execution_bindings`, the complete generation-fenced lifecycle action set, +bounded-concurrency support, and finite service/action capacity. The three +published carriers are: + +| Contract id | Carries | +| --- | --- | +| `participant-execution-binding-v1` | Exact action contract, native target-service addresses, selected implementation, constraints/evidence, and finite execution policy | +| `participant-execution-control-v1` | Expected-generation `start`, `pause`, `resume`, bounded `drain`, `reset`, and `teardown` request | +| `participant-execution-service-state-v1` | Lifecycle, generation, health, readiness, admission/capacity, quiescence/resource release, shared-time provenance, pacing deviations, and evidence | + +Control mutations reuse `operation-receipt-v1` and `operation-status-v1`; state +is embedded in `runtime-snapshot-v1.participant_execution_services`. These +carriers do not merge participant episode lifecycle, scheduler continuation, +or shared-clock state. Conditional live conformance is required because +schema-valid declarations cannot establish native action behavior. The shared +runtime does not manufacture lifecycle transitions: a backend-owned control +operation must coordinate those incumbent surfaces, and changed typed +readback plus new evidence must validate each successful operation. + ## API-407 - Feature Support And Constraint Declaration `capabilities.participant_runtime.feature_support` is a list of per-feature diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index f2a719505..b0fd634c7 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -74,6 +74,9 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: "participant-time-management-context-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", + "participant-execution-binding-v1", + "participant-execution-control-v1", + "participant-execution-service-state-v1", "participant-outcome-report-v1", "runtime-fact-binding-plane-v1", }: From 9b2f0dcf18c87585efc43848812382e3a62a6937 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 00:37:05 +0200 Subject: [PATCH 26/55] fix: restore Ground Control project binding --- .ground-control.yaml | 2 +- tools/policy/historical_identity_records.json | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.ground-control.yaml b/.ground-control.yaml index c5bab9e4e..4f1bc5d03 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -1,5 +1,5 @@ schema_version: 1 -project: raes-sdl +project: aces-sdl github_repo: RAESystem/rae workflow: test_command: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index b44595733..154229f49 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -2,6 +2,13 @@ "schema_version": "historical-identity-records/v2", "hash_algorithm": "sha256", "operational_bindings": [ + { + "path": ".ground-control.yaml", + "binding_class": "external-service-project-key", + "rationale": "Retains the existing service-owned Ground Control project designation without treating it as current RAES product identity.", + "occurrences": 1, + "content_sha256": "e0596809035a079630de05e4d350ed27fac0675526b247c3b069627ca4abe267" + }, { "path": "sonar-project.properties", "binding_class": "external-service-project-key", From 6ea34570c75d2f739eae219e45bab1cefef8aa35 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 01:19:19 +0200 Subject: [PATCH 27/55] fix(participant): reconcile execution contracts with RAES cutover --- .../entries/participant-execution-binding-v1.json | 6 +++--- .../entries/participant-execution-control-v1.json | 6 +++--- .../entries/participant-execution-service-state-v1.json | 6 +++--- .../participant-execution-binding-v1.json | 2 +- .../participant-execution-control-v1.json | 2 +- .../participant-execution-service-state-v1.json | 2 +- docs/index.md | 1 + .../tests/test_issue_898_participant_execution_control.py | 4 ++-- tools/policy/historical_identity_records.json | 6 +++--- 9 files changed, 18 insertions(+), 17 deletions(-) diff --git a/contracts/schema-publication/entries/participant-execution-binding-v1.json b/contracts/schema-publication/entries/participant-execution-binding-v1.json index 3e2a80659..38cfb3932 100644 --- a/contracts/schema-publication/entries/participant-execution-binding-v1.json +++ b/contracts/schema-publication/entries/participant-execution-binding-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-execution-binding-v1", "schema_path": "contracts/schemas/participant-runtime/participant-execution-binding-v1.json", "stability": "draft", - "content_hash": "3b80c5ca6cf152de84426f2e22c1d66bdba8157b18b9ed85c2ca051b323df818", + "content_hash": "fa935e3d933584be0f569db57f692a9f8dbff6877af5b711a3bdc9a5917bd105", "last_change": { - "summary": "Published the exact native participant action-to-target execution relation and finite execution bounds for issue #898.", - "content_hash": "3b80c5ca6cf152de84426f2e22c1d66bdba8157b18b9ed85c2ca051b323df818" + "summary": "Published the exact native participant action-to-target execution relation and finite execution bounds for issue #898 under the RAES schema identity.", + "content_hash": "fa935e3d933584be0f569db57f692a9f8dbff6877af5b711a3bdc9a5917bd105" } } diff --git a/contracts/schema-publication/entries/participant-execution-control-v1.json b/contracts/schema-publication/entries/participant-execution-control-v1.json index 022dd3d3d..e7979f76c 100644 --- a/contracts/schema-publication/entries/participant-execution-control-v1.json +++ b/contracts/schema-publication/entries/participant-execution-control-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-execution-control-v1", "schema_path": "contracts/schemas/participant-runtime/participant-execution-control-v1.json", "stability": "draft", - "content_hash": "efe00dae0e16258eb63f6c1ca2c833027ee1a659f5e7f41053d0d81a1ec69e50", + "content_hash": "7aead7ff4a66d15a87fd4504e2fe5cb077f60e0667c274e7734d3b5ccc5357b8", "last_change": { - "summary": "Published generation-fenced start, pause, resume, bounded drain, reset, and teardown requests for issue #898.", - "content_hash": "efe00dae0e16258eb63f6c1ca2c833027ee1a659f5e7f41053d0d81a1ec69e50" + "summary": "Published generation-fenced start, pause, resume, bounded drain, reset, and teardown requests for issue #898 under the RAES schema identity.", + "content_hash": "7aead7ff4a66d15a87fd4504e2fe5cb077f60e0667c274e7734d3b5ccc5357b8" } } diff --git a/contracts/schema-publication/entries/participant-execution-service-state-v1.json b/contracts/schema-publication/entries/participant-execution-service-state-v1.json index d2a596d99..eff9dd06d 100644 --- a/contracts/schema-publication/entries/participant-execution-service-state-v1.json +++ b/contracts/schema-publication/entries/participant-execution-service-state-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-execution-service-state-v1", "schema_path": "contracts/schemas/participant-runtime/participant-execution-service-state-v1.json", "stability": "draft", - "content_hash": "59bb66a44d86b6466bb2511abb9f62148291d762ab0a4a93ca912c2fb8c970bc", + "content_hash": "3cfd82795d44408fa86f3c89e221023a1924e596c8a72334b9f8ead01a33e9e8", "last_change": { - "summary": "Published participant execution lifecycle, generation, health, readiness, concurrency, shared-time provenance, and evidence readback for issue #898.", - "content_hash": "59bb66a44d86b6466bb2511abb9f62148291d762ab0a4a93ca912c2fb8c970bc" + "summary": "Published participant execution lifecycle, generation, health, readiness, concurrency, shared-time provenance, and evidence readback for issue #898 under the RAES schema identity.", + "content_hash": "3cfd82795d44408fa86f3c89e221023a1924e596c8a72334b9f8ead01a33e9e8" } } diff --git a/contracts/schemas/participant-runtime/participant-execution-binding-v1.json b/contracts/schemas/participant-runtime/participant-execution-binding-v1.json index 2b10f2fc4..22a380bb6 100644 --- a/contracts/schemas/participant-runtime/participant-execution-binding-v1.json +++ b/contracts/schemas/participant-runtime/participant-execution-binding-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/participant-execution-binding-v1.json", + "$id": "https://raes.dev/schemas/participant-execution-binding-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Exact executable relation between one action and its native targets.", diff --git a/contracts/schemas/participant-runtime/participant-execution-control-v1.json b/contracts/schemas/participant-runtime/participant-execution-control-v1.json index a6ab92b30..1fdf3ddb4 100644 --- a/contracts/schemas/participant-runtime/participant-execution-control-v1.json +++ b/contracts/schemas/participant-runtime/participant-execution-control-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/participant-execution-control-v1.json", + "$id": "https://raes.dev/schemas/participant-execution-control-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Generation-fenced lifecycle mutation for one admitted execution scope.", diff --git a/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json b/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json index df5a7bebe..8c2aa607f 100644 --- a/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json +++ b/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json @@ -1,5 +1,5 @@ { - "$id": "https://aces.dev/schemas/participant-execution-service-state-v1.json", + "$id": "https://raes.dev/schemas/participant-execution-service-state-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "Typed health, readiness, lifecycle, capacity, and evidence readback.", diff --git a/docs/index.md b/docs/index.md index 9dd51c4fb..6bf9cd803 100644 --- a/docs/index.md +++ b/docs/index.md @@ -161,6 +161,7 @@ specs/formal lessons/README migration/README migration/autonomous-execution-v2 +migration/participant-execution-control migration/raes-rename research/experiment-core/index research/realization-envelope/index diff --git a/implementations/python/tests/test_issue_898_participant_execution_control.py b/implementations/python/tests/test_issue_898_participant_execution_control.py index 3a7cc5948..eab238621 100644 --- a/implementations/python/tests/test_issue_898_participant_execution_control.py +++ b/implementations/python/tests/test_issue_898_participant_execution_control.py @@ -549,8 +549,8 @@ def test_control_plane_exposes_authenticated_generation_bound_execution_control( security=_test_security(target.name), ) backend_headers = { - "x-aces-client-verified": "true", - "x-aces-client-identity": "backend-service", + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", "idempotency-key": "start-generation-0", } auditor_headers = {"authorization": "Bearer test-auditor-token"} diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index b44595733..e4d3863b8 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -478,7 +478,7 @@ "record_class": "accepted-adr", "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", "occurrences": 2, - "content_sha256": "4e791ecbd467547be40932d4b7580124571709016fcc207b21600e943e12b68c" + "content_sha256": "966cb55a1a2d74350403996a5ae2eaa81e59876d35f0f3000ddcf2941600aff7" }, { "path": "docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md", @@ -492,7 +492,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", "occurrences": 4, - "content_sha256": "fbbaa8aee300174af664fea63031221f93bd45d734566ddf1f255b3c05e98e25" + "content_sha256": "52f0eed782027820a62ef271a6c83fa23763d96410185c4278e626312e30a969" }, { "path": "docs/decisions/cage-2-replication-design.md", @@ -1563,7 +1563,7 @@ "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 24, - "content_sha256": "87d29b6a072b0fac2f8c9008072236cac001d4df0777842b81e22cd7b04a35ee" + "content_sha256": "050e205672cf340fa167e2a2d7b3e2739f058794eb28d34b68612767249a511a" }, { "path": "docs/research/participant-backend-contracts/index.md", From 8769424f15ae288b5f76e1de25b13628cffc39e4 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 01:37:04 +0200 Subject: [PATCH 28/55] Document RAES ecosystem naming boundaries --- .ground-control.yaml | 4 +- ...sue-907-raes-env-packs-naming-preflight.md | 150 +++++++++++++++++ docs/migration/raes-rename.md | 41 +++++ .../tests/test_deprecation_lifecycle.py | 22 ++- .../tests/test_identity_cutover_policy.py | 158 +++++++++++++++++- specs/evolution/deprecation-records.yaml | 13 ++ tools/check_deprecation_lifecycle.py | 1 + tools/check_identity_cutover.py | 1 + tools/policy/historical_identity_records.json | 21 +++ 9 files changed, 404 insertions(+), 7 deletions(-) create mode 100644 docs/decisions/issue-907-raes-env-packs-naming-preflight.md diff --git a/.ground-control.yaml b/.ground-control.yaml index c5bab9e4e..fac765825 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -1,5 +1,7 @@ schema_version: 1 -project: raes-sdl +# Ground Control still owns the pre-cutover project identifier. ADR-096 +# requires external provisioning before this binding can move. +project: aces-sdl github_repo: RAESystem/rae workflow: test_command: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify diff --git a/docs/decisions/issue-907-raes-env-packs-naming-preflight.md b/docs/decisions/issue-907-raes-env-packs-naming-preflight.md new file mode 100644 index 000000000..53b2747a6 --- /dev/null +++ b/docs/decisions/issue-907-raes-env-packs-naming-preflight.md @@ -0,0 +1,150 @@ +# Issue 907 RAES and Environment-Packs Naming Preflight + +Date: 2026-07-27 + +Issue: #907. Requirement: none. The GitHub issue is the authoritative delivery +contract. + +This note fixes the architecture boundaries for implementation. It does not +publish a package, mutate a PyPI project, define a downstream pack format, or +add a compatibility mode. + +## Base-Synchronization Correction + +The first preflight pass occurred before PR #921 merged. That merge completed +issue #908 / GOV-944 and accepted ADR-096, which supersedes the earlier +assumption that current ACES-bearing contract and wire identifiers would remain +unchanged. The implementation for issue #907 treats the merged identity +cutover as authoritative. + +The resulting downstream sequence is release-bounded: + +1. Consumers pinned to RAES 1.1.0 continue to use that release's ACES-bearing + contract, schema, wire, workflow, runtime, and host values. +2. Consumers migrate those values atomically when adopting the next breaking + RAES release containing PR #921, targeted as 2.0.0. +3. Consumers do not guess `raes-*` translations, combine values from both + release lines, or add aliases, fallback reads, and last-one-wins rules. + +The release's normative schemas, fixtures, and migration evidence define the +new values. This note records sequencing; it does not create a second identity +inventory. + +Ground Control's project identifier is an external service-owned operational +binding, not a repository-owned product identity. The service currently exposes +only its pre-cutover project record. ADR-096 requires replacement provisioning +before repository configuration points to a new external identifier, so the +existing logical binding remains registered as an exact content-bound +operational binding until that provisioning occurs. This is the same bounded +class of exception as the retained SonarCloud project key; it does not authorize +a runtime alias or a general naming fallback. + +## Architecture Decisions + +### Scenario remains the SDL term; environment pack is a packaging term + +`Scenario` remains the RAES SDL authored-content concept, and +`instantiate_scenario()` remains the operation that binds its variables into an +instantiated scenario. The normative SDL document phases and contract names +continue to use *scenario*. + +An **environment pack** is a downstream packaging and distribution unit. It may +contain SDL scenarios and other reusable assets. It is not: + +- an alias or replacement for `Scenario`; +- a new SDL document phase, schema, model, parser entry point, or validator; +- equivalent to an agentic environment or realized environment; or +- authority to restate upstream scenario, concept, or trust-policy semantics. + +The downstream pack owner may define pack layout and release mechanics. This +repository remains the authority for the contained SDL and governed contract +meanings. + +### Retire the `aces-sdl` PyPI project with a final pointer and archival + +The `aces-sdl` PyPI project is end-of-life. Its replacement is `raes`; migration +is directly to RAES imports and commands. No removed import shim or compatibility +package is restored. + +Close the old project with one final 0.23.2 legacy-line release whose code +behavior is unchanged from 0.23.1 and whose distribution metadata and long +description state that: + +- the project is retired and receives no further fixes; +- `raes` is the replacement distribution; +- old imports do not carry forward; and +- consumers must follow `docs/migration/raes-rename.md`. + +The EOL release must not depend on `raes`, provide aliases, or install an empty +placeholder. After its metadata and artifacts are verified on PyPI, archive the +project. Do not delete or broadly yank historical releases. + +The current release path remains exclusively for `raes`. The one-time legacy +publication is a separate protected action: it must build from immutable +reviewed source, use the protected PyPI environment and short-lived OIDC +credentials, and retain no token. Issue #907 records this decision but does not +authorize the upload or archival action. + +## Canonical Incumbents To Reuse + +- **Identity cutover:** ADR-096, + `docs/decisions/issue-908-raes-identity-cutover-preflight.md`, + `docs/migration/raes-rename.md`, `tools/check_identity_cutover.py`, and + `tools/policy/historical_identity_records.json`. +- **Normative authority and contract evolution:** ADR-009, ADR-019, ADR-061, + `contracts/schema-publication-manifest.json`, + `contracts/schema-publication/entries/`, `schema_bundle()`, + `tools/check_schema_publication.py`, and + `tools/check_generated_schemas.py`. +- **Compatibility and lifecycle:** ADR-075, + `specs/evolution/versioning-deprecation-and-migration.md`, + `specs/evolution/deprecation-records.yaml`, and + `tools/check_deprecation_lifecycle.py`. +- **SDL vocabulary and phases:** ADR-001, `specs/sdl/`, + `docs/explain/reference/glossary.md`, `raes.scenario.Scenario`, + `raes.instantiate.instantiate_scenario`, parser and semantic validators, and + the authored and instantiated schemas and fixtures. +- **Concept and reusable-asset authority:** ADR-012, ADR-062, ADR-071, + `specs/concept-authority/`, `contracts/concept-authority/`, and + `specs/supply-chain/reusable-asset-trust-integrity.md`. Environment packs + consume these surfaces; they do not clone them. +- **Release and verification:** release-please configuration, + `.github/workflows/release-please.yml`, the pinned PyPI publish action, + protected `pypi` environment, OIDC trusted publishing, ADR-014, + `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, + `tools/check_repo_policy.py`, and `tools/verify_all.py`. + +## Cross-Cutting Boundaries + +| Layer | Required behavior | +|---|---| +| Contracts and schemas | PR #921 remains the sole identity-cutover implementation. Issue #907 adds sequencing guidance, not aliases, schema edits, or a parallel identifier inventory. | +| SDL parse and instantiation | `Scenario` and `instantiate_scenario()` remain canonical. Pack metadata does not enter an SDL model merely because a pack contains SDL. | +| Concept and trust policy | Environment packs reference the upstream authorities and validators. They do not copy schemas or create a pack-local trust model. | +| Authentication and authorization | No runtime auth surface changes. Existing strict defaults and denial behavior remain in force. | +| Secrets and publication | No credential is added to source, docs, fixtures, logs, environment dumps, or process arguments. The future protected publication uses short-lived OIDC. | +| Persistence and host state | No read-time rewrite, cleanup service, or migration store is introduced. Operators migrate persisted values and resources at the breaking-release boundary. | +| Historical evidence | This dated correction retains exact pre-cutover naming only through ADR-096's content-bound historical-record manifest. | +| Lifecycle evidence | The legacy distribution name remains readable only in its exact content-bound ADR-075 lifecycle record; code and tests use the neutral record id `legacy-python-distribution`. | + +## Extensibility Seams + +- A future contract change extends its owning schema-publication entry, + fixtures, validators, compatibility record, and versioned reader boundary. +- A future pack format extends the downstream pack manifest and containment + references; it does not extend `Scenario` with package metadata. +- A future distribution retirement adds an exact `python-distribution` + lifecycle record. It does not make the RAES publisher accept an arbitrary + project name. + +## Non-Goals And Anti-Patterns + +- No additional contract, schema, profile, discriminator, URI, auth header, + workflow key, host identifier, or persisted artifact rename. +- No SDL model/API rename and no `EnvironmentPack` model in this repository. +- No compatibility shim, alias registry, automatic source migrator, generic + rename service, new exception hierarchy, or migration database. +- No downstream pack implementation. +- No package publication, PyPI project mutation, or release-workflow change. +- No mixed old/new identity handling, global replacement, copied authority, + stored publication token, or deletion of sound historical releases. diff --git a/docs/migration/raes-rename.md b/docs/migration/raes-rename.md index c4bb5cf16..07fea0daf 100644 --- a/docs/migration/raes-rename.md +++ b/docs/migration/raes-rename.md @@ -52,3 +52,44 @@ The hard cut does not automatically rewrite persisted artifacts or clean resources created under an earlier identity. Operators should complete any required environment cleanup before deploying the cutover release; the repository does not discover or destroy old-name resources. + +## Downstream Cutover Sequencing + +Issue #908 migrated current contract, schema, wire, workflow, runtime, and host +identities together. Consumers pinned to RAES 1.1.0 continue to use that +release's pre-cutover values. They must switch all affected values atomically +when adopting the next breaking RAES release that contains the identity +cutover, targeted as 2.0.0. + +Consumers must not guess replacement spellings, mix identities from the two +release lines, or introduce aliases and fallback reads. The release's schemas, +fixtures, and migration evidence are the source of truth for the new values. + +## Scenario And Environment-Pack Vocabulary + +`Scenario` remains the RAES SDL authored-content concept, and +`instantiate_scenario()` continues to produce an instantiated scenario. An +**environment pack** is a downstream packaging and distribution unit that may +contain SDL scenarios and other reusable assets. It is not another name for a +scenario, an SDL document phase or model, or a realized environment. + +The intended split is therefore: packs are environment packs; the SDL content +they carry remains scenarios. A pack repository owns its layout and release +mechanics, while this repository remains authoritative for SDL, concept, and +reusable-asset trust-policy meanings. + +## Legacy PyPI Distribution Retirement + +The legacy PyPI distribution named in issue #907 is end-of-life, and `raes` is +its replacement. One final 0.23.2 release will be cut from the immutable 0.23.1 +legacy lineage with unchanged code behavior and an updated long description +that points consumers to `raes` and this migration guide. It will not depend on +`raes`, install a placeholder, or restore retired import aliases. + +After the final artifact name, version, contents, metadata, and replacement +links are verified on PyPI, the legacy project will be archived. Existing +historical releases will remain available rather than being deleted or broadly +yanked. The current release-please workflow remains exclusive to `raes`; the +one-time legacy publication requires an immutable reviewed source, the +protected PyPI environment, and short-lived OIDC credentials rather than a +stored token. diff --git a/implementations/python/tests/test_deprecation_lifecycle.py b/implementations/python/tests/test_deprecation_lifecycle.py index ec7160a09..1c28a0bb8 100644 --- a/implementations/python/tests/test_deprecation_lifecycle.py +++ b/implementations/python/tests/test_deprecation_lifecycle.py @@ -34,7 +34,7 @@ def _good_ledger() -> dict: - # Includes the canonical retention-floor record so the positive case and + # Includes all canonical (retention-floor) records so the positive case and # every mutation starting point satisfies CANONICAL_DEPRECATION_RECORD_IDS. # records[0] is a canonical record; mutation tests operate on it. return { @@ -66,6 +66,17 @@ def _good_ledger() -> dict: "notice_window": "supported indefinitely as a backward-compatible alias", "verification_evidence": "parser normalises the legacy field; tests exercise both forms", }, + { + "id": "legacy-python-distribution", + "surface_class": "python-distribution", + "identifier": "the legacy PyPI distribution", + "status": "deprecated", + "first_notice": "ADR-093 and issue #907", + "replacement": "the raes PyPI distribution", + "migration_reference": "docs/migration/raes-rename.md", + "notice_window": "publish the final pointer release, verify it, then archive the project", + "verification_evidence": "the current release path publishes only raes", + }, ], } @@ -342,11 +353,11 @@ def test_empty_ledger_is_rejected(tmp_path: Path) -> None: def test_dropping_a_canonical_record_is_rejected(tmp_path: Path) -> None: - # Deleting an established record (leaving the other) must fail: an existing + # Deleting an established record (leaving the others) must fail: an existing # deprecation is permanent lifecycle history, not something a later diff can # silently remove. ledger = _good_ledger() - dropped = ledger["records"].pop() # remove sdl-import-path-field + dropped = ledger["records"].pop() # remove the legacy distribution record _write_repo(tmp_path, ledger) failures = evaluate_deprecation_records(tmp_path) assert any(f.rule_id == "deprecation-records-canonical-record-missing" for f in failures) @@ -361,7 +372,10 @@ def test_dropping_a_canonical_record_is_rejected(tmp_path: Path) -> None: def test_canonical_record_ids_are_pinned() -> None: - assert {"sdl-import-path-field"} == CANONICAL_DEPRECATION_RECORD_IDS + assert { + "legacy-python-distribution", + "sdl-import-path-field", + } == CANONICAL_DEPRECATION_RECORD_IDS def test_surface_classes_cover_known_matrix_rows() -> None: diff --git a/implementations/python/tests/test_identity_cutover_policy.py b/implementations/python/tests/test_identity_cutover_policy.py index aa23755a8..9a5938a08 100644 --- a/implementations/python/tests/test_identity_cutover_policy.py +++ b/implementations/python/tests/test_identity_cutover_policy.py @@ -1,10 +1,12 @@ from __future__ import annotations +import copy import hashlib import json import subprocess from pathlib import Path +import pytest from tools.check_identity_cutover import evaluate_identity_cutover REPO_ROOT = Path(__file__).resolve().parents[3] @@ -31,10 +33,16 @@ def _git(repo_root: Path, *args: str) -> None: ) -def _record(path: str, content: bytes, *, occurrences: int = 1) -> dict[str, object]: +def _record( + path: str, + content: bytes, + *, + occurrences: int = 1, + record_class: str = "dated-design-record", +) -> dict[str, object]: return { "path": path, - "record_class": "dated-design-record", + "record_class": record_class, "rationale": "Preserves a dated design decision from before the identity cutover.", "occurrences": occurrences, "content_sha256": hashlib.sha256(content).hexdigest(), @@ -74,10 +82,134 @@ def _seed_repo( _git(repo_root, "add", "-A") +def _seed_manifest_fixture(repo_root: Path) -> dict[str, object]: + record_content = f"Historical {RETIRED_UPPER} record.\n".encode() + binding_content = f"projectKey=service_{RETIRED_LOWER}\n".encode() + record_path = "docs/decisions/issue-1-preflight.md" + binding_path = "service-project.properties" + _seed_repo( + repo_root, + files={ + record_path: record_content, + binding_path: binding_content, + }, + records=[_record(record_path, record_content)], + bindings=[_binding(binding_path, binding_content)], + ) + return json.loads((repo_root / MANIFEST_PATH).read_text(encoding="utf-8")) + + +def _write_manifest(repo_root: Path, manifest: object) -> None: + _write( + repo_root / MANIFEST_PATH, + json.dumps(manifest, indent=2, sort_keys=True) + "\n", + ) + + +def _manifest_rule_ids(repo_root: Path) -> set[str]: + return {failure.rule_id for failure in evaluate_identity_cutover(repo_root)} + + def test_current_repository_satisfies_identity_cutover() -> None: assert evaluate_identity_cutover(REPO_ROOT) == [] +@pytest.mark.parametrize( + ("field", "invalid_value"), + [ + ("schema_version", "wrong-schema"), + ("hash_algorithm", "sha512"), + ("records", {}), + ("operational_bindings", {}), + ], +) +def test_invalid_manifest_top_level_fields_fail(tmp_path: Path, field: str, invalid_value: object) -> None: + manifest = _seed_manifest_fixture(tmp_path) + manifest[field] = invalid_value + _write_manifest(tmp_path, manifest) + + assert "identity-cutover-manifest" in _manifest_rule_ids(tmp_path) + + +@pytest.mark.parametrize("collection", ["records", "operational_bindings"]) +@pytest.mark.parametrize( + "mutation", + [ + "non-object", + "missing-key", + "extra-key", + "unknown-class", + "blank-rationale", + "bad-digest", + ], +) +def test_invalid_manifest_entry_shapes_fail(tmp_path: Path, collection: str, mutation: str) -> None: + manifest = _seed_manifest_fixture(tmp_path) + entries = manifest[collection] + assert isinstance(entries, list) + entry = entries[0] + assert isinstance(entry, dict) + + if mutation == "non-object": + entries[0] = "not-an-object" + elif mutation == "missing-key": + del entry["content_sha256"] + elif mutation == "extra-key": + entry["unexpected"] = "field" + elif mutation == "unknown-class": + class_field = "record_class" if collection == "records" else "binding_class" + entry[class_field] = "unknown-class" + elif mutation == "blank-rationale": + entry["rationale"] = " " + else: + entry["content_sha256"] = "not-a-sha256" + + _write_manifest(tmp_path, manifest) + + assert "identity-cutover-manifest" in _manifest_rule_ids(tmp_path) + + +@pytest.mark.parametrize("collection", ["records", "operational_bindings"]) +@pytest.mark.parametrize("invalid_occurrences", [True, 0, -1, "1"]) +def test_invalid_manifest_occurrence_counts_fail( + tmp_path: Path, + collection: str, + invalid_occurrences: object, +) -> None: + manifest = _seed_manifest_fixture(tmp_path) + entries = manifest[collection] + assert isinstance(entries, list) + entry = entries[0] + assert isinstance(entry, dict) + entry["occurrences"] = invalid_occurrences + _write_manifest(tmp_path, manifest) + + assert "identity-cutover-manifest" in _manifest_rule_ids(tmp_path) + + +@pytest.mark.parametrize("collection", ["records", "operational_bindings"]) +def test_duplicate_manifest_paths_fail(tmp_path: Path, collection: str) -> None: + manifest = _seed_manifest_fixture(tmp_path) + entries = manifest[collection] + assert isinstance(entries, list) + entries.append(copy.deepcopy(entries[0])) + _write_manifest(tmp_path, manifest) + + assert "identity-cutover-manifest" in _manifest_rule_ids(tmp_path) + + +def test_path_shared_between_record_and_binding_fails(tmp_path: Path) -> None: + manifest = _seed_manifest_fixture(tmp_path) + records = manifest["records"] + bindings = manifest["operational_bindings"] + assert isinstance(records, list) and isinstance(records[0], dict) + assert isinstance(bindings, list) and isinstance(bindings[0], dict) + bindings[0]["path"] = records[0]["path"] + _write_manifest(tmp_path, manifest) + + assert "identity-cutover-manifest" in _manifest_rule_ids(tmp_path) + + def test_live_retired_identity_fails_in_visible_hidden_and_binary_files(tmp_path: Path) -> None: _seed_repo( tmp_path, @@ -111,6 +243,18 @@ def test_exact_content_bound_historical_record_passes(tmp_path: Path) -> None: assert evaluate_identity_cutover(tmp_path) == [] +def test_exact_content_bound_lifecycle_record_passes(tmp_path: Path) -> None: + content = f'identifier: "retired {RETIRED_LOWER}-sdl distribution"\n'.encode() + path = "specs/evolution/deprecation-records.yaml" + _seed_repo( + tmp_path, + files={path: content}, + records=[_record(path, content, record_class="lifecycle-record")], + ) + + assert evaluate_identity_cutover(tmp_path) == [] + + def test_changed_historical_record_fails_closed(tmp_path: Path) -> None: original = f"Historical {RETIRED_UPPER} record.\n".encode() path = "docs/decisions/issue-1-preflight.md" @@ -178,6 +322,16 @@ def test_unsafe_or_untracked_historical_path_fails_closed(tmp_path: Path) -> Non assert any(failure.rule_id == "identity-cutover-manifest-path" for failure in failures) +def test_unsafe_operational_binding_path_fails_closed(tmp_path: Path) -> None: + manifest = _seed_manifest_fixture(tmp_path) + bindings = manifest["operational_bindings"] + assert isinstance(bindings, list) and isinstance(bindings[0], dict) + bindings[0]["path"] = "../outside.properties" + _write_manifest(tmp_path, manifest) + + assert "identity-cutover-manifest-path" in _manifest_rule_ids(tmp_path) + + def test_identity_cutover_check_is_registered_in_canonical_policy_graph() -> None: noxfile_source = (REPO_ROOT / "noxfile.py").read_text(encoding="utf-8") assert '"tools/check_identity_cutover.py"' in noxfile_source diff --git a/specs/evolution/deprecation-records.yaml b/specs/evolution/deprecation-records.yaml index a038d8055..69065eb4d 100644 --- a/specs/evolution/deprecation-records.yaml +++ b/specs/evolution/deprecation-records.yaml @@ -35,6 +35,19 @@ adr_refs: spec: specs/evolution/versioning-deprecation-and-migration.md records: + # The old PyPI project needs one final pointer release before archival. This + # is distribution lifecycle, not an import compatibility promise: the final + # release changes metadata and guidance only and does not restore old imports. + - id: legacy-python-distribution + surface_class: python-distribution + identifier: "the legacy `aces-sdl` PyPI distribution, with 0.23.1 as its latest published release before the EOL pointer" + status: deprecated + first_notice: "ADR-093 and issue #907 (RAES rename and environment-pack naming convergence)" + replacement: "the `raes` PyPI distribution and direct `raes` / `raes_*` imports" + migration_reference: "docs/migration/raes-rename.md" + notice_window: "Publish one final metadata-only 0.23.2 pointer release from the immutable 0.23.1 legacy lineage, verify it on PyPI, then archive the `aces-sdl` project. The archive is removal eligibility; no later fixes or releases are planned." + verification_evidence: "The current pyproject and release workflow publish only `raes`; version and packaging tests reject the old command/import surface. The final legacy artifact must be verified separately for its exact name, version, unchanged code payload, EOL description, and `raes` replacement link before archival." + # The module-import `path:` field, superseded by `source:` in ADR-053. The # parser still accepts it as a backward-compatible alias, normalizing it to # the `local:{path}` source form. diff --git a/tools/check_deprecation_lifecycle.py b/tools/check_deprecation_lifecycle.py index df84ed10c..3971bdf2d 100644 --- a/tools/check_deprecation_lifecycle.py +++ b/tools/check_deprecation_lifecycle.py @@ -94,6 +94,7 @@ # this floor -- exactly the predictability GOV-902 requires. CANONICAL_DEPRECATION_RECORD_IDS: frozenset[str] = frozenset( { + "legacy-python-distribution", "sdl-import-path-field", } ) diff --git a/tools/check_identity_cutover.py b/tools/check_identity_cutover.py index 58a77f33f..f8fd894d6 100644 --- a/tools/check_identity_cutover.py +++ b/tools/check_identity_cutover.py @@ -37,6 +37,7 @@ "accepted-adr", "dated-design-record", "historical-index", + "lifecycle-record", "provenance-record", "release-history", "research-record", diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index b44595733..0c7f41c03 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -2,6 +2,13 @@ "schema_version": "historical-identity-records/v2", "hash_algorithm": "sha256", "operational_bindings": [ + { + "path": ".ground-control.yaml", + "binding_class": "external-service-project-key", + "rationale": "Retains the existing service-owned Ground Control project identifier until its replacement is externally provisioned.", + "occurrences": 1, + "content_sha256": "f89fa216218b0c68786c464b7777ceb194e3c6aa0c3bc12a61a245068d967c7e" + }, { "path": "sonar-project.properties", "binding_class": "external-service-project-key", @@ -1278,6 +1285,13 @@ "occurrences": 2, "content_sha256": "3128021de32125f2d8ce6270e1386c2c8cc5dd3af45514fb8e335a17c9469940" }, + { + "path": "docs/decisions/issue-907-raes-env-packs-naming-preflight.md", + "record_class": "dated-design-record", + "rationale": "Preserves the dated issue preflight and its base-synchronization correction as historical identity-cutover evidence.", + "occurrences": 4, + "content_sha256": "82c441869cca975874236fa02946298bb4dbf70ecd04336550652193673e0022" + }, { "path": "docs/decisions/issue-97-asr-511-515-validation-strength-disclosure-preflight.md", "record_class": "dated-design-record", @@ -1788,6 +1802,13 @@ "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 4, "content_sha256": "778711fccb5b5912a6fd106d080b4eac9dd354e4ccd4574204274fea653fecbc" + }, + { + "path": "specs/evolution/deprecation-records.yaml", + "record_class": "lifecycle-record", + "rationale": "Preserves the exact retired distribution identifier in its governed deprecation and removal record.", + "occurrences": 2, + "content_sha256": "eff30897103368c6583701e886078f810188bcc47f4d9183853faeb1739fbdcb" } ] } From ddd2ea031ded98d80af9fc0977257d13b4eeb2fc Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 01:45:45 +0200 Subject: [PATCH 29/55] refactor(participant): simplify execution control paths --- .../participant_capabilities.py | 9 + .../participant_execution_runtime.py | 46 ++- .../contracts/participant_execution.py | 17 +- .../raes_contracts/participant_binding.py | 124 ++++--- .../participant_execution_control_boundary.py | 110 ++++-- .../raes_runtime/participant_scheduler.py | 318 ++++++++++------- .../participant_scheduler_concurrency.py | 335 +++++++++--------- .../python/packages/raes_runtime/registry.py | 42 ++- ...issue_898_participant_execution_control.py | 24 +- 9 files changed, 601 insertions(+), 424 deletions(-) diff --git a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py index 6078c15b0..99196a11f 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -283,6 +283,11 @@ def _validate_enabled_autonomous_execution(self) -> None: raise ValueError(f"autonomous execution requires positive {label}") def _validate_execution_control(self) -> None: + self._validate_execution_control_actions() + self._validate_execution_capacity() + self._validate_execution_bindings() + + def _validate_execution_control_actions(self) -> None: if not self.supports_execution_control: raise ValueError("autonomous execution requires execution control support") missing_actions = PARTICIPANT_EXECUTION_CONTROL_ACTIONS - self.supported_execution_control_actions @@ -291,12 +296,16 @@ def _validate_execution_control(self) -> None: unknown_actions = self.supported_execution_control_actions - PARTICIPANT_EXECUTION_CONTROL_ACTIONS if unknown_actions: raise ValueError("unsupported execution control actions: " + ", ".join(sorted(unknown_actions))) + + def _validate_execution_capacity(self) -> None: if not self.supports_bounded_concurrency: raise ValueError("autonomous execution requires bounded concurrency support") if self.max_execution_services is None or self.max_execution_services < 1: raise ValueError("autonomous execution requires positive max_execution_services") if self.max_concurrent_actions is None or self.max_concurrent_actions < 2: raise ValueError("bounded concurrency requires max_concurrent_actions of at least 2") + + def _validate_execution_bindings(self) -> None: if not self.execution_bindings: raise ValueError("autonomous execution requires relational execution_bindings") binding_ids = tuple(binding.binding_id for binding in self.execution_bindings) diff --git a/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py index 029586d00..135666590 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py +++ b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py @@ -20,8 +20,8 @@ class ParticipantExecutionRuntimeMixin: """Generation fencing, lifecycle readback, and bounded native dispatch.""" + @staticmethod def execution_state( - self, execution_scope_ref: str, snapshot: RuntimeSnapshot, ) -> ParticipantExecutionServiceStateModel: @@ -29,6 +29,29 @@ def execution_state( return participant_execution_state(execution_scope_ref, snapshot) + @staticmethod + def _execution_generation_failure_reason( + request: ParticipantActionAdmissionRequest, + snapshot: RuntimeSnapshot, + *, + completion: bool, + ) -> str | None: + scope = request.execution_scope_ref + payload = snapshot.participant_execution_services.get(scope) if scope is not None else None + if payload is None: + return "execution-service state is missing" + state = ParticipantExecutionServiceStateModel.model_validate(payload) + if ( + state.generation != request.execution_generation + or state.observed_generation != request.execution_generation + ): + return "execution generation changed" + if not completion and ( + state.observed_lifecycle != "running" or not state.accepting_new_work or state.readiness != "ready" + ): + return "execution service is not accepting work" + return None + @staticmethod def _execution_generation_failure( request: ParticipantActionAdmissionRequest, @@ -37,22 +60,13 @@ def _execution_generation_failure( completion: bool, predecessor: RuntimeSnapshot | None = None, ) -> ParticipantActionApplyResult | None: - scope = request.execution_scope_ref - if scope is None: + if request.execution_scope_ref is None: return None - payload = snapshot.participant_execution_services.get(scope) - expected = request.execution_generation - reason = None - if payload is None: - reason = "execution-service state is missing" - else: - state = ParticipantExecutionServiceStateModel.model_validate(payload) - if state.generation != expected or state.observed_generation != expected: - reason = "execution generation changed" - elif not completion and ( - state.observed_lifecycle != "running" or not state.accepting_new_work or state.readiness != "ready" - ): - reason = "execution service is not accepting work" + reason = ParticipantExecutionRuntimeMixin._execution_generation_failure_reason( + request, + snapshot, + completion=completion, + ) if reason is None: return None phase = "completion" if completion else "work" diff --git a/implementations/python/packages/raes_contracts/contracts/participant_execution.py b/implementations/python/packages/raes_contracts/contracts/participant_execution.py index 87e1be5ba..05bb35320 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_execution.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_execution.py @@ -110,21 +110,37 @@ class ParticipantExecutionServiceStateModel(ContractModel): @model_validator(mode="after") def _validate_state(self) -> ParticipantExecutionServiceStateModel: + self._validate_generations() + self._validate_capacity() + self._validate_admission_readback() + self._validate_lifecycle_readback() + self._validate_unique_references() + return self + + def _validate_generations(self) -> None: if self.observed_generation > self.generation: raise ValueError("observed_generation cannot exceed generation") + + def _validate_capacity(self) -> None: if self.reserved + self.in_flight > self.capacity: raise ValueError("reserved and in_flight work cannot exceed capacity") + + def _validate_admission_readback(self) -> None: accepting_states = {"starting", "running"} if self.accepting_new_work and self.observed_lifecycle not in accepting_states: raise ValueError("accepting_new_work requires a starting or running observed lifecycle") if self.accepting_new_work and self.readiness != "ready": raise ValueError("accepting_new_work requires ready readback") + + def _validate_lifecycle_readback(self) -> None: if self.draining != (self.observed_lifecycle == "draining"): raise ValueError("draining must agree with the observed lifecycle") if self.resources_released != (self.observed_lifecycle == "terminated"): raise ValueError("resources_released must agree with terminated lifecycle") if self.quiescent and (self.reserved or self.in_flight): raise ValueError("quiescent execution cannot retain reserved or in-flight work") + + def _validate_unique_references(self) -> None: for field_name in ( "scheduler_state_refs", "pacing_deviation_refs", @@ -133,7 +149,6 @@ def _validate_state(self) -> ParticipantExecutionServiceStateModel: values = getattr(self, field_name) if len(values) != len(set(values)): raise ValueError(f"{field_name} must contain unique values") - return self __all__ = [ diff --git a/implementations/python/packages/raes_contracts/participant_binding.py b/implementations/python/packages/raes_contracts/participant_binding.py index 6f2ef37ba..d8c3c47af 100644 --- a/implementations/python/packages/raes_contracts/participant_binding.py +++ b/implementations/python/packages/raes_contracts/participant_binding.py @@ -115,64 +115,82 @@ class ParticipantActionAdmissionRequest: execution_generation: int | None = None def __post_init__(self) -> None: - _require_non_empty(self.participant_address, "participant_address") - _require_prefixed( - self.action_contract_address, - _ACTION_CONTRACT_PREFIX, - "action_contract_address", - ) - _require_prefixed( - self.observation_boundary_address, - _OBSERVATION_BOUNDARY_PREFIX, - "observation_boundary_address", - ) - _require_non_empty(self.action_instance_id, "action_instance_id") - _require_non_empty(self.state_transition_kind, "state_transition_kind") - if self.post_state_digest is not None: - _require_non_empty(self.post_state_digest, "post_state_digest") - if not isinstance(self.implementation_manifest, ParticipantImplementationManifestModel): - raise TypeError("implementation_manifest must be a ParticipantImplementationManifestModel") - if not isinstance(self.implementation_selection, ParticipantImplementationSelectionModel): - raise TypeError("implementation_selection must be a ParticipantImplementationSelectionModel") - if self.action_result is not None and not isinstance(self.action_result, ParticipantActionResultModel): - raise TypeError("action_result must be a ParticipantActionResultModel or None") - if self.validated_selection is not None: - if not isinstance(self.validated_selection, ParticipantValidatedActionSelection): - raise TypeError("validated_selection must be a ParticipantValidatedActionSelection or None") - if self.validated_selection.action_contract_address != self.action_contract_address: - raise ValueError("validated_selection action_contract_address must match the admission request") - if not isinstance(self.requires_terminal_outcome, bool): - raise TypeError("requires_terminal_outcome must be a bool") - if (self.execution_scope_ref is None) != (self.execution_generation is None): - raise ValueError("execution_scope_ref and execution_generation must be provided together") - if self.execution_scope_ref is not None: - _require_non_empty(self.execution_scope_ref, "execution_scope_ref") - if self.execution_generation is None or self.execution_generation < 0: - raise ValueError("execution_generation must be non-negative") - if not self.target_addresses: - raise ValueError("generation-bound participant actions require target_addresses") - if any(not isinstance(item, ParticipantTemporalRuntimeContextModel) for item in self.temporal_contexts): - raise TypeError("temporal_contexts entries must be ParticipantTemporalRuntimeContextModel") - if len({item.temporal_contract_id for item in self.temporal_contexts}) != len(self.temporal_contexts): - raise ValueError("temporal_contexts temporal_contract_id values must be unique") - object.__setattr__(self, "evidence_refs", _string_tuple(self.evidence_refs, "evidence_refs")) - object.__setattr__(self, "visible_refs", _string_tuple(self.visible_refs, "visible_refs")) - object.__setattr__(self, "disclosed_refs", _string_tuple(self.disclosed_refs, "disclosed_refs")) - object.__setattr__( - self, - "observation_boundary_evidence_refs", - _string_tuple(self.observation_boundary_evidence_refs, "observation_boundary_evidence_refs"), - ) - object.__setattr__( - self, - "target_addresses", - _string_tuple(self.target_addresses, "target_addresses"), - ) + _validate_admission_request_identifiers(self) + _validate_admission_request_models(self) + _validate_admission_request_selection(self) + _validate_admission_request_execution(self) + _validate_admission_request_temporal_contexts(self) + _normalize_admission_request_tuples(self) violations = participant_action_admission_request_violations(self) if violations: raise ValueError(violations[0]) +def _validate_admission_request_identifiers(request: ParticipantActionAdmissionRequest) -> None: + _require_non_empty(request.participant_address, "participant_address") + _require_prefixed(request.action_contract_address, _ACTION_CONTRACT_PREFIX, "action_contract_address") + _require_prefixed( + request.observation_boundary_address, + _OBSERVATION_BOUNDARY_PREFIX, + "observation_boundary_address", + ) + _require_non_empty(request.action_instance_id, "action_instance_id") + _require_non_empty(request.state_transition_kind, "state_transition_kind") + if request.post_state_digest is not None: + _require_non_empty(request.post_state_digest, "post_state_digest") + + +def _validate_admission_request_models(request: ParticipantActionAdmissionRequest) -> None: + if not isinstance(request.implementation_manifest, ParticipantImplementationManifestModel): + raise TypeError("implementation_manifest must be a ParticipantImplementationManifestModel") + if not isinstance(request.implementation_selection, ParticipantImplementationSelectionModel): + raise TypeError("implementation_selection must be a ParticipantImplementationSelectionModel") + if request.action_result is not None and not isinstance(request.action_result, ParticipantActionResultModel): + raise TypeError("action_result must be a ParticipantActionResultModel or None") + + +def _validate_admission_request_selection(request: ParticipantActionAdmissionRequest) -> None: + selection = request.validated_selection + if selection is None: + return + if not isinstance(selection, ParticipantValidatedActionSelection): + raise TypeError("validated_selection must be a ParticipantValidatedActionSelection or None") + if selection.action_contract_address != request.action_contract_address: + raise ValueError("validated_selection action_contract_address must match the admission request") + + +def _validate_admission_request_execution(request: ParticipantActionAdmissionRequest) -> None: + if not isinstance(request.requires_terminal_outcome, bool): + raise TypeError("requires_terminal_outcome must be a bool") + if (request.execution_scope_ref is None) != (request.execution_generation is None): + raise ValueError("execution_scope_ref and execution_generation must be provided together") + if request.execution_scope_ref is None: + return + _require_non_empty(request.execution_scope_ref, "execution_scope_ref") + if request.execution_generation is None or request.execution_generation < 0: + raise ValueError("execution_generation must be non-negative") + if not request.target_addresses: + raise ValueError("generation-bound participant actions require target_addresses") + + +def _validate_admission_request_temporal_contexts(request: ParticipantActionAdmissionRequest) -> None: + if any(not isinstance(item, ParticipantTemporalRuntimeContextModel) for item in request.temporal_contexts): + raise TypeError("temporal_contexts entries must be ParticipantTemporalRuntimeContextModel") + if len({item.temporal_contract_id for item in request.temporal_contexts}) != len(request.temporal_contexts): + raise ValueError("temporal_contexts temporal_contract_id values must be unique") + + +def _normalize_admission_request_tuples(request: ParticipantActionAdmissionRequest) -> None: + for field_name in ( + "evidence_refs", + "visible_refs", + "disclosed_refs", + "observation_boundary_evidence_refs", + "target_addresses", + ): + object.__setattr__(request, field_name, _string_tuple(getattr(request, field_name), field_name)) + + @dataclass class ParticipantActionApplyResult(ApplyResult): """Control-plane result plus the independently reported native action outcome.""" 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 7c25de060..c80237220 100644 --- a/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py +++ b/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py @@ -57,25 +57,72 @@ def _precondition( return None -def _expected_observation( +_EXPECTED_OBSERVATIONS = { + "start": ("running", "ready", True), + "resume": ("running", "ready", True), + "reset": ("running", "ready", True), + "pause": ("paused", "not_ready", False), + "drain": ("quiescent", "not_ready", False), + "teardown": ("terminated", "not_ready", False), +} + + +def _expected_observation(request: ParticipantExecutionControlRequestModel) -> tuple[str, str, bool]: + return _EXPECTED_OBSERVATIONS[request.action] + + +def _common_readback_matches( request: ParticipantExecutionControlRequestModel, -) -> tuple[str, str, bool]: - if request.action in {"start", "resume", "reset"}: - return "running", "ready", True - if request.action == "pause": - return "paused", "not_ready", False + before: ParticipantExecutionServiceStateModel, + observed: ParticipantExecutionServiceStateModel, +) -> bool: + return ( + _observed_state_matches(request, observed) + and _has_new_transition_evidence(before, observed) + ) + + +def _observed_state_matches( + request: ParticipantExecutionControlRequestModel, + observed: ParticipantExecutionServiceStateModel, +) -> bool: + lifecycle, readiness, accepting = _expected_observation(request) + expected_generation = request.expected_generation + (request.action == "reset") + return ( + observed.observed_lifecycle == lifecycle + and observed.desired_lifecycle == lifecycle + and observed.readiness == readiness + and observed.accepting_new_work is accepting + and observed.generation == expected_generation + and observed.observed_generation == expected_generation + ) + + +def _has_new_transition_evidence( + before: ParticipantExecutionServiceStateModel, + observed: ParticipantExecutionServiceStateModel, +) -> bool: + return ( + bool(observed.last_transition_ref) + and observed.last_transition_ref != before.last_transition_ref + and bool(set(observed.evidence_refs).difference(before.evidence_refs)) + ) + + +def _action_readback_matches( + request: ParticipantExecutionControlRequestModel, + observed: ParticipantExecutionServiceStateModel, +) -> bool: if request.action == "drain": - return "quiescent", "not_ready", False - return "terminated", "not_ready", False + return not (observed.reserved or observed.in_flight or observed.draining or not observed.quiescent) + return request.action != "teardown" or observed.resources_released -def _validate_observed_result( +def _successful_observed_result( request: ParticipantExecutionControlRequestModel, predecessor: RuntimeSnapshot, result: ApplyResult, ) -> ApplyResult: - if not result.success: - return result payload = result.snapshot.participant_execution_services.get(request.execution_scope_ref) if payload is None: return _failure( @@ -88,33 +135,22 @@ def _validate_observed_result( predecessor.participant_execution_services[request.execution_scope_ref] ) observed = ParticipantExecutionServiceStateModel.model_validate(payload) - lifecycle, readiness, accepting = _expected_observation(request) - expected_generation = request.expected_generation + (1 if request.action == "reset" else 0) - invalid = ( - observed.observed_lifecycle != lifecycle - or observed.desired_lifecycle != lifecycle - or observed.readiness != readiness - or observed.accepting_new_work is not accepting - or observed.generation != expected_generation - or observed.observed_generation != expected_generation - or not observed.last_transition_ref - or observed.last_transition_ref == before.last_transition_ref - or not set(observed.evidence_refs).difference(before.evidence_refs) + if _common_readback_matches(request, before, observed) and _action_readback_matches(request, observed): + return result + return _failure( + predecessor, + request, + "runtime.participant-execution-readback-invalid", + "Backend control result did not prove the requested observed lifecycle transition.", ) - if request.action == "drain": - invalid = invalid or bool( - observed.reserved or observed.in_flight or observed.draining or not observed.quiescent - ) - if request.action == "teardown": - invalid = invalid or not observed.resources_released - if invalid: - return _failure( - predecessor, - request, - "runtime.participant-execution-readback-invalid", - "Backend control result did not prove the requested observed lifecycle transition.", - ) - return result + + +def _validate_observed_result( + request: ParticipantExecutionControlRequestModel, + predecessor: RuntimeSnapshot, + result: ApplyResult, +) -> ApplyResult: + return result if not result.success else _successful_observed_result(request, predecessor, result) def backend_execution_control_method( diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index b0ea709df..9169edd0d 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Iterable +from dataclasses import dataclass from raes_contracts.contracts import ( ParticipantAutonomousExecutionStateModel, @@ -192,6 +193,182 @@ def _initialize_participant( return ApplyResult(success=True, snapshot=working, changed_addresses=changed) +@dataclass(frozen=True) +class _ClockResetContext: + policy: ParticipantAutonomousExecutionRuntime + time_model: CompiledTimeModel + participant_runtime: object + segment: int + current_tick: int + reset_participants: bool + activity_control: ParticipantActivityRandomControl | None + next_tick: int + timing_disposition: str + + +def _clock_reset_context( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + segment: int, + current_tick: int, + reset_participants: bool, + activity_control: ParticipantActivityRandomControl | None, +) -> _ClockResetContext: + next_tick = current_tick + timing_disposition = "cadence" + if activity_control is None: + next_tick, cadence_ticks = _cadence(policy, time_model) + if next_tick < current_tick: + next_tick += ((current_tick - next_tick + cadence_ticks - 1) // cadence_ticks) * cadence_ticks + return _ClockResetContext( + policy=policy, + time_model=time_model, + participant_runtime=participant_runtime, + segment=segment, + current_tick=current_tick, + reset_participants=reset_participants, + activity_control=activity_control, + next_tick=next_tick, + timing_disposition=timing_disposition, + ) + + +def _reset_scheduler_participant( + context: _ClockResetContext, + snapshot: RuntimeSnapshot, + participant_address: str, +) -> ApplyResult: + working = snapshot + changed: list[str] = [] + if context.reset_participants: + reset = context.participant_runtime.reset( + ParticipantEpisodeResetRequest( + participant_address=participant_address, + episode_id=f"{participant_address}-autonomous-{context.segment}", + reason=f"shared clock reset to segment {context.segment}", + ), + working, + ) + if not reset.success: + return reset + working = reset.snapshot + changed.extend(reset.changed_addresses) + key = _state_key(context.policy.address, participant_address) + state = ParticipantAutonomousExecutionStateModel.model_validate( + working.participant_autonomous_execution_states[key] + ) + next_tick = context.next_tick + timing_disposition = context.timing_disposition + burst_size = 1 + if context.activity_control is not None: + burst_size = draw_activity_integer( + policy=context.policy, + participant_address=participant_address, + time_segment=context.segment, + occurrence_ordinal=0, + control=context.activity_control, + local_coordinate=2, + minimum=1, + maximum=context.policy.max_burst_size, + ) + timing = next_activity_timing( + policy=context.policy, + time_model=context.time_model, + participant_address=participant_address, + time_segment=context.segment, + occurrence_ordinal=0, + current_tick=context.current_tick, + control=context.activity_control, + ) + next_tick = timing.tick if timing.tick is not None else context.current_tick + timing_disposition = timing.disposition + states = dict(working.participant_autonomous_execution_states) + states[key] = state.model_copy( + update={ + "episode_id": working.participant_episode_results[participant_address]["episode_id"], + "lifecycle_state": "running", + "time_segment": context.segment, + "next_tick": next_tick, + "next_action_index": 0, + "attempted_actions": 0, + "succeeded_actions": 0, + "failed_actions": 0, + "in_flight": 0, + "last_action_instance_id": None, + "occurrence_ordinal": 0, + "current_retry": 0, + "burst_position": 0, + "last_candidate_id": None, + "completed_candidate_ids": [], + "candidate_cooldown_until": {}, + "burst_size": burst_size, + "next_timing_disposition": timing_disposition, + } + ).model_dump(mode="json") + working = working.with_entries( + dict(working.entries), + participant_autonomous_execution_states=states, + ) + changed.append(key) + return ApplyResult(success=True, snapshot=working, changed_addresses=changed) + + +def _missing_execution_service_result( + policy: ParticipantAutonomousExecutionRuntime, + run: SchedulerRunState, +) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=run.working, + diagnostics=[ + Diagnostic( + code="runtime.participant-execution-state-missing", + domain="participant", + address=policy.address, + message="Autonomous participant execution requires typed execution-service state.", + ) + ], + ) + + +def _execution_service_accepts_work(service: ParticipantExecutionServiceStateModel) -> bool: + return service.observed_lifecycle == "running" and service.accepting_new_work and service.readiness == "ready" + + +def _run_due_policy( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + activity_controls: dict[str, ParticipantActivityRandomControl], + run: SchedulerRunState, +) -> None: + service_payload = run.working.participant_execution_services.get(policy.address) + if service_payload is None: + run.failure = _missing_execution_service_result(policy, run) + return + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + if not _execution_service_accepts_work(service): + return + cadence_ticks = _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 + current_tick = _clock_tick(run.working, policy.clock_address) + if run_policy_due_concurrently(policy, time_model, participant_runtime, current_tick, cadence_ticks, run): + return + for participant_address in policy.participant_addresses: + run_participant_due( + policy, + time_model, + participant_runtime, + participant_address, + current_tick, + cadence_ticks, + run, + activity_controls, + ) + if run.failure is not None: + return + + class ParticipantScheduler: """Deterministically selects due actions and delegates native execution.""" @@ -274,55 +451,9 @@ def run_due( run = SchedulerRunState(working=snapshot, diagnostics=[], changed=[]) resolved_activity_controls = activity_controls or {} for policy in policies: - service_payload = run.working.participant_execution_services.get(policy.address) - if service_payload is None: - return ApplyResult( - success=False, - snapshot=run.working, - diagnostics=[ - Diagnostic( - code="runtime.participant-execution-state-missing", - domain="participant", - address=policy.address, - message=("Autonomous participant execution requires typed execution-service state."), - ) - ], - ) - service = ParticipantExecutionServiceStateModel.model_validate(service_payload) - if ( - service.observed_lifecycle != "running" - or not service.accepting_new_work - or service.readiness != "ready" - ): - continue - cadence_ticks = ( - _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 - ) - current_tick = _clock_tick(run.working, policy.clock_address) - if run_policy_due_concurrently( - policy, - time_model, - participant_runtime, - current_tick, - cadence_ticks, - run, - ): - if run.failure is not None: - return run.result() - continue - for participant_address in policy.participant_addresses: - run_participant_due( - policy, - time_model, - participant_runtime, - participant_address, - current_tick, - cadence_ticks, - run, - resolved_activity_controls, - ) - if run.failure is not None: - return run.result() + _run_due_policy(policy, time_model, participant_runtime, resolved_activity_controls, run) + if run.failure is not None: + break return run.result() @staticmethod @@ -347,82 +478,21 @@ def reset_clock( continue current_tick = _clock_tick(snapshot, clock_address) activity_control = activity_control_for(policy, resolved_activity_controls) - if activity_control is None: - first_tick, cadence_ticks = _cadence(policy, time_model) - next_tick = first_tick - timing_disposition = "cadence" - if next_tick < current_tick: - next_tick += ((current_tick - next_tick + cadence_ticks - 1) // cadence_ticks) * cadence_ticks + context = _clock_reset_context( + policy, + time_model, + participant_runtime, + segment, + current_tick, + reset_participants, + activity_control, + ) for participant_address in policy.participant_addresses: - result_changed: list[str] = [] - if reset_participants: - result = participant_runtime.reset( - ParticipantEpisodeResetRequest( - participant_address=participant_address, - episode_id=f"{participant_address}-autonomous-{segment}", - reason=f"shared clock reset to segment {segment}", - ), - working, - ) - if not result.success: - return result - working = result.snapshot - result_changed.extend(result.changed_addresses) - key = _state_key(policy.address, participant_address) - state = ParticipantAutonomousExecutionStateModel.model_validate( - working.participant_autonomous_execution_states[key] - ) - if activity_control is not None: - burst_size = draw_activity_integer( - policy=policy, - participant_address=participant_address, - time_segment=segment, - occurrence_ordinal=0, - control=activity_control, - local_coordinate=2, - minimum=1, - maximum=policy.max_burst_size, - ) - timing = next_activity_timing( - policy=policy, - time_model=time_model, - participant_address=participant_address, - time_segment=segment, - occurrence_ordinal=0, - current_tick=current_tick, - control=activity_control, - ) - selected_tick = timing.tick - timing_disposition = timing.disposition - next_tick = selected_tick if selected_tick is not None else current_tick - states = dict(working.participant_autonomous_execution_states) - states[key] = state.model_copy( - update={ - "episode_id": working.participant_episode_results[participant_address]["episode_id"], - "lifecycle_state": "running", - "time_segment": segment, - "next_tick": next_tick, - "next_action_index": 0, - "attempted_actions": 0, - "succeeded_actions": 0, - "failed_actions": 0, - "in_flight": 0, - "last_action_instance_id": None, - "occurrence_ordinal": 0, - "current_retry": 0, - "burst_position": 0, - "last_candidate_id": None, - "completed_candidate_ids": [], - "candidate_cooldown_until": {}, - "burst_size": burst_size if activity_control is not None else 1, - "next_timing_disposition": timing_disposition, - } - ).model_dump(mode="json") - working = working.with_entries( - dict(working.entries), - participant_autonomous_execution_states=states, - ) - changed.extend([*result_changed, key]) + result = _reset_scheduler_participant(context, working, participant_address) + if not result.success: + return result + working = result.snapshot + changed.extend(result.changed_addresses) working, service_changed = reset_execution_service( working, policy.address, diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py index b361729de..2df31ba58 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py @@ -7,7 +7,7 @@ from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel from raes_contracts.diagnostics import Diagnostic -from raes_contracts.participant_binding import ParticipantActionAdmissionRequest +from raes_contracts.participant_binding import ParticipantActionAdmissionRequest, ParticipantActionApplyResult from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime @@ -155,26 +155,17 @@ def _finish_concurrent_service_state( ) -def run_policy_due_concurrently( +def _due_contexts( policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel, participant_runtime: object, current_tick: int, cadence_ticks: int, run: SchedulerRunState, -) -> bool: - """Execute one due v1 occurrence per participant with bounded overlap.""" - - from .participant_scheduler_operations import ( - _bound_action_request, - _cadence_missed_result, - _next_action_state, - run_participant_due, - ) +) -> tuple[list[_DueActionContext], list[ParticipantAutonomousExecutionStateModel]]: + from .participant_scheduler_operations import _cadence_missed_result from .participant_scheduler_types import _DueActionContext - if policy.profile != "participant-autonomous-execution/v1": - return False contexts: list[_DueActionContext] = [] states: list[ParticipantAutonomousExecutionStateModel] = [] for participant_address in policy.participant_addresses: @@ -183,172 +174,190 @@ def run_policy_due_concurrently( run.working.participant_autonomous_execution_states[key] ) if state.lifecycle_state == "running" and state.next_tick < current_tick: - run.failure = _cadence_missed_result( - run.working, - key, - current_tick, - state, - ) - return True - if not ( + run.failure = _cadence_missed_result(run.working, key, current_tick, state) + break + due = ( state.lifecycle_state == "running" and state.next_tick == current_tick and state.attempted_actions < policy.max_action_attempts - ): - continue - contexts.append( - _DueActionContext( - policy=policy, - time_model=time_model, - participant_runtime=participant_runtime, - participant_address=participant_address, - key=key, - current_tick=current_tick, - cadence_ticks=cadence_ticks, + ) + if due: + contexts.append( + _DueActionContext( + policy=policy, + time_model=time_model, + participant_runtime=participant_runtime, + participant_address=participant_address, + key=key, + current_tick=current_tick, + cadence_ticks=cadence_ticks, + ) ) + states.append(state) + return contexts, states + + +def _set_concurrent_failure(run: SchedulerRunState, diagnostic: Diagnostic) -> None: + run.diagnostics.append(diagnostic) + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + + +def _unsupported_concurrency_failure(policy: ParticipantAutonomousExecutionRuntime, run: SchedulerRunState) -> None: + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=[ + Diagnostic( + code="runtime.participant-concurrency-unsupported", + domain="participant", + address=policy.address, + message="Backend declared bounded participant concurrency without an executable batch method.", + ) + ], + ) + + +def _commit_concurrent_result( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + result: ParticipantActionApplyResult, + base: RuntimeSnapshot, + run: SchedulerRunState, +) -> None: + from .participant_scheduler_operations import _next_action_state + + stale_completion = participant_generation_commit_diagnostic(request, run.working) + if stale_completion is not None: + _set_concurrent_failure(run, stale_completion) + return + try: + run.working = _merge_concurrent_action_snapshot(base, run.working, result.snapshot) + except ValueError as exc: + _set_concurrent_failure( + run, + Diagnostic( + code="runtime.participant-concurrent-commit-conflict", + domain="participant", + address=context.key, + message=str(exc), + ), ) - states.append(state) - if len(contexts) < 2 or policy.max_in_flight < 2: - return False - batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) - if not callable(batch_method): + return + protocol_violation = autonomous_action_result_violation( + request, + result, + episode_id=state.episode_id, + predecessor=base, + ) + if protocol_violation is not None: + _set_concurrent_failure( + run, + Diagnostic( + code="runtime.participant-autonomous-action-protocol-invalid", + domain="participant", + address=context.participant_address, + message=protocol_violation, + ), + ) + return + action_result = result.action_result + action_succeeded = bool(result.success and action_result is not None and action_result.status == "succeeded") + next_state = _next_action_state( + context, + state, + request, + action_succeeded=action_succeeded, + protocol_failure=False, + ).model_copy(update={"in_flight": 0}) + scheduler_states = dict(run.working.participant_autonomous_execution_states) + scheduler_states[context.key] = next_state.model_dump(mode="json") + run.working = run.working.with_entries( + dict(run.working.entries), + participant_autonomous_execution_states=scheduler_states, + ) + run.diagnostics.extend(result.diagnostics) + run.changed.extend([*result.changed_addresses, context.key]) + if not action_succeeded and context.policy.failure_policy == "stop": run.failure = ApplyResult( success=False, snapshot=run.working, - diagnostics=[ - Diagnostic( - code="runtime.participant-concurrency-unsupported", - domain="participant", - address=policy.address, - message=("Backend declared bounded participant concurrency without an executable batch method."), - ) - ], + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), ) + + +def _finish_due_policy( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + current_tick: int, + cadence_ticks: int, + run: SchedulerRunState, +) -> None: + from .participant_scheduler_operations import run_participant_due + + if run.failure is not None: + return + if run_policy_due_concurrently(policy, time_model, participant_runtime, current_tick, cadence_ticks, run): + return + for participant_address in policy.participant_addresses: + run_participant_due( + policy, + time_model, + participant_runtime, + participant_address, + current_tick, + cadence_ticks, + run, + ) + if run.failure is not None: + break + + +def run_policy_due_concurrently( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + current_tick: int, + cadence_ticks: int, + run: SchedulerRunState, +) -> bool: + """Execute one due v1 occurrence per participant with bounded overlap.""" + + if policy.profile != "participant-autonomous-execution/v1": + return False + contexts, states = _due_contexts(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) + if run.failure is not None: + return True + if len(contexts) < 2 or policy.max_in_flight < 2: + return False + batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) + if not callable(batch_method): + _unsupported_concurrency_failure(policy, run) return True - contexts_tuple = tuple(contexts[: policy.max_in_flight]) - states = states[: policy.max_in_flight] + selected_contexts = tuple(contexts[: policy.max_in_flight]) + selected_states = states[: policy.max_in_flight] + from .participant_scheduler_operations import _bound_action_request + requests = tuple( _bound_action_request(context, run.working, state) - for context, state in zip(contexts_tuple, states, strict=True) + for context, state in zip(selected_contexts, selected_states, strict=True) ) - _reserve_concurrent_actions(run, contexts_tuple) + _reserve_concurrent_actions(run, selected_contexts) base = run.working - results = batch_method( - requests, - base, - len(requests), - ) + results = batch_method(requests, base, len(requests)) if len(results) != len(requests): raise ValueError("concurrent participant result count must match requests") - for context, state, request, result in zip( - contexts_tuple, - states, - requests, - results, - strict=True, - ): - stale_completion = participant_generation_commit_diagnostic( - request, - run.working, - ) - if stale_completion is not None: - run.diagnostics.append(stale_completion) - run.failure = ApplyResult( - success=False, - snapshot=run.working, - diagnostics=run.diagnostics, - changed_addresses=list(dict.fromkeys(run.changed)), - ) - break - try: - run.working = _merge_concurrent_action_snapshot( - base, - run.working, - result.snapshot, - ) - except ValueError as exc: - run.diagnostics.append( - Diagnostic( - code="runtime.participant-concurrent-commit-conflict", - domain="participant", - address=context.key, - message=str(exc), - ) - ) - run.failure = ApplyResult( - success=False, - snapshot=run.working, - diagnostics=run.diagnostics, - changed_addresses=list(dict.fromkeys(run.changed)), - ) - break - protocol_violation = autonomous_action_result_violation( - request, - result, - episode_id=state.episode_id, - predecessor=base, - ) - if protocol_violation is not None: - run.diagnostics.append( - Diagnostic( - code="runtime.participant-autonomous-action-protocol-invalid", - domain="participant", - address=context.participant_address, - message=protocol_violation, - ) - ) - run.failure = ApplyResult( - success=False, - snapshot=run.working, - diagnostics=run.diagnostics, - changed_addresses=list(dict.fromkeys(run.changed)), - ) - break - action_result = result.action_result - action_succeeded = bool(result.success and action_result is not None and action_result.status == "succeeded") - next_state = _next_action_state( - context, - state, - request, - action_succeeded=action_succeeded, - protocol_failure=False, - ).model_copy(update={"in_flight": 0}) - scheduler_states = dict(run.working.participant_autonomous_execution_states) - scheduler_states[context.key] = next_state.model_dump(mode="json") - run.working = run.working.with_entries( - dict(run.working.entries), - participant_autonomous_execution_states=scheduler_states, - ) - run.diagnostics.extend(result.diagnostics) - run.changed.extend([*result.changed_addresses, context.key]) - if not action_succeeded and policy.failure_policy == "stop": - run.failure = ApplyResult( - success=False, - snapshot=run.working, - diagnostics=run.diagnostics, - changed_addresses=list(dict.fromkeys(run.changed)), - ) + for context, state, request, result in zip(selected_contexts, selected_states, requests, results, strict=True): + _commit_concurrent_result(context, state, request, result, base, run) + if run.failure is not None: break _finish_concurrent_service_state(run, policy.address) - if run.failure is None: - if not run_policy_due_concurrently( - policy, - time_model, - participant_runtime, - current_tick, - cadence_ticks, - run, - ): - for participant_address in policy.participant_addresses: - run_participant_due( - policy, - time_model, - participant_runtime, - participant_address, - current_tick, - cadence_ticks, - run, - ) - if run.failure is not None: - break + _finish_due_policy(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) return True diff --git a/implementations/python/packages/raes_runtime/registry.py b/implementations/python/packages/raes_runtime/registry.py index 7498ebee9..2cbb60a4a 100644 --- a/implementations/python/packages/raes_runtime/registry.py +++ b/implementations/python/packages/raes_runtime/registry.py @@ -22,6 +22,24 @@ _TIME_CLOCK_PROBE = "time.clock.probe" +@dataclass(frozen=True) +class _ParticipantRuntimeMethodRequirements: + autonomous_binding: bool + coordinated_reset: bool + execution_control: bool + bounded_concurrency: bool + + +def _participant_runtime_method_requirements(manifest: BackendManifest) -> _ParticipantRuntimeMethodRequirements: + capability = manifest.participant_runtime + return _ParticipantRuntimeMethodRequirements( + autonomous_binding=bool(capability and capability.supports_autonomous_execution), + coordinated_reset=bool(manifest.time and manifest.time.supports_coordinated_participant_reset), + execution_control=bool(capability and capability.supports_execution_control), + bounded_concurrency=bool(capability and capability.supports_bounded_concurrency), + ) + + def _require_invokable_method( component: object | None, *, @@ -87,16 +105,7 @@ def _validate_runtime_target_shape( sample_request, sample_admission_request, sample_snapshot, - require_autonomous_binding=bool( - manifest.participant_runtime and manifest.participant_runtime.supports_autonomous_execution - ), - require_coordinated_reset=bool(manifest.time and manifest.time.supports_coordinated_participant_reset), - require_execution_control=bool( - manifest.participant_runtime and manifest.participant_runtime.supports_execution_control - ), - require_bounded_concurrency=bool( - manifest.participant_runtime and manifest.participant_runtime.supports_bounded_concurrency - ), + requirements=_participant_runtime_method_requirements(manifest), ) _validate_time_runtime_methods( time_runtime, @@ -229,10 +238,7 @@ def _validate_participant_runtime_methods( sample_admission_request: ParticipantActionAdmissionRequest, sample_snapshot: object, *, - require_autonomous_binding: bool, - require_coordinated_reset: bool, - require_execution_control: bool, - require_bounded_concurrency: bool, + requirements: _ParticipantRuntimeMethodRequirements, ) -> None: _require_invokable_method( participant_runtime, @@ -282,7 +288,7 @@ def _validate_participant_runtime_methods( method_name="history", invocation_args=(), ) - if require_autonomous_binding: + if requirements.autonomous_binding: _require_invokable_method( participant_runtime, label="participant_runtime", @@ -297,14 +303,14 @@ def _validate_participant_runtime_methods( sample_snapshot, ), ) - if require_coordinated_reset: + if requirements.coordinated_reset: _require_invokable_method( participant_runtime, label="participant_runtime", method_name="reset_many", invocation_args=((sample_request,), sample_snapshot), ) - if require_execution_control: + if requirements.execution_control: _require_invokable_method( participant_runtime, label="participant_runtime", @@ -320,7 +326,7 @@ def _validate_participant_runtime_methods( sample_snapshot, ), ) - if require_bounded_concurrency: + if requirements.bounded_concurrency: _require_invokable_method( participant_runtime, label="participant_runtime", diff --git a/implementations/python/tests/test_issue_898_participant_execution_control.py b/implementations/python/tests/test_issue_898_participant_execution_control.py index eab238621..2462590e9 100644 --- a/implementations/python/tests/test_issue_898_participant_execution_control.py +++ b/implementations/python/tests/test_issue_898_participant_execution_control.py @@ -96,12 +96,12 @@ def test_execution_binding_is_relational_and_finite() -> None: assert binding.target_addresses == ("service.customer-portal.https",) assert binding.max_in_flight == 2 + invalid_target_binding = _binding().model_copy(update={"target_addresses": ()}).model_dump() + invalid_max_in_flight_binding = {**_binding().model_dump(), "max_in_flight": 0} with pytest.raises(ValueError, match="target_addresses"): - _binding().model_copy(update={"target_addresses": ()}).model_validate( - _binding().model_copy(update={"target_addresses": ()}).model_dump() - ) + ParticipantExecutionBindingModel.model_validate(invalid_target_binding) with pytest.raises(ValueError, match="max_in_flight"): - ParticipantExecutionBindingModel.model_validate({**_binding().model_dump(), "max_in_flight": 0}) + ParticipantExecutionBindingModel.model_validate(invalid_max_in_flight_binding) def test_execution_control_request_requires_generation_and_bounded_drain() -> None: @@ -146,16 +146,16 @@ def test_execution_readback_separates_health_readiness_and_lifecycle() -> None: assert state.readiness == "ready" assert state.accepting_new_work is True + invalid_admission_readback = { + **state.model_dump(), + "observed_lifecycle": "paused", + "accepting_new_work": True, + } + invalid_generation_readback = {**state.model_dump(), "observed_generation": 4} with pytest.raises(ValueError, match="accepting_new_work"): - ParticipantExecutionServiceStateModel.model_validate( - { - **state.model_dump(), - "observed_lifecycle": "paused", - "accepting_new_work": True, - } - ) + ParticipantExecutionServiceStateModel.model_validate(invalid_admission_readback) with pytest.raises(ValueError, match="observed_generation"): - ParticipantExecutionServiceStateModel.model_validate({**state.model_dump(), "observed_generation": 4}) + ParticipantExecutionServiceStateModel.model_validate(invalid_generation_readback) def test_compiler_preserves_exact_action_to_target_execution_binding() -> None: From 9aa2107df526d70084b658b06d407598811e83f7 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 01:46:21 +0200 Subject: [PATCH 30/55] ci: configure Read the Docs builds --- .readthedocs.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..76a2b2558 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,21 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +sphinx: + configuration: docs/conf.py + fail_on_warning: true + +python: + install: + - method: uv + command: sync + path: implementations/python + extras: + - docs From acda47281a2e0cb024c8d91395365626ddc973cd Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 01:50:05 +0200 Subject: [PATCH 31/55] refactor(participant): separate scheduler reset operations --- .../raes_contracts/participant_binding.py | 47 ++---- .../raes_runtime/participant_scheduler.py | 141 +----------------- .../participant_scheduler_reset.py | 133 +++++++++++++++++ .../participant_scheduler_time.py | 14 +- 4 files changed, 165 insertions(+), 170 deletions(-) create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_reset.py diff --git a/implementations/python/packages/raes_contracts/participant_binding.py b/implementations/python/packages/raes_contracts/participant_binding.py index d8c3c47af..d7a3d7e7b 100644 --- a/implementations/python/packages/raes_contracts/participant_binding.py +++ b/implementations/python/packages/raes_contracts/participant_binding.py @@ -115,18 +115,15 @@ class ParticipantActionAdmissionRequest: execution_generation: int | None = None def __post_init__(self) -> None: - _validate_admission_request_identifiers(self) - _validate_admission_request_models(self) - _validate_admission_request_selection(self) - _validate_admission_request_execution(self) - _validate_admission_request_temporal_contexts(self) - _normalize_admission_request_tuples(self) + _validate_admission_request_basics(self) + _validate_admission_request_selection_and_execution(self) + _validate_and_normalize_admission_request_contexts(self) violations = participant_action_admission_request_violations(self) if violations: raise ValueError(violations[0]) -def _validate_admission_request_identifiers(request: ParticipantActionAdmissionRequest) -> None: +def _validate_admission_request_basics(request: ParticipantActionAdmissionRequest) -> None: _require_non_empty(request.participant_address, "participant_address") _require_prefixed(request.action_contract_address, _ACTION_CONTRACT_PREFIX, "action_contract_address") _require_prefixed( @@ -138,9 +135,6 @@ def _validate_admission_request_identifiers(request: ParticipantActionAdmissionR _require_non_empty(request.state_transition_kind, "state_transition_kind") if request.post_state_digest is not None: _require_non_empty(request.post_state_digest, "post_state_digest") - - -def _validate_admission_request_models(request: ParticipantActionAdmissionRequest) -> None: if not isinstance(request.implementation_manifest, ParticipantImplementationManifestModel): raise TypeError("implementation_manifest must be a ParticipantImplementationManifestModel") if not isinstance(request.implementation_selection, ParticipantImplementationSelectionModel): @@ -149,38 +143,29 @@ def _validate_admission_request_models(request: ParticipantActionAdmissionReques raise TypeError("action_result must be a ParticipantActionResultModel or None") -def _validate_admission_request_selection(request: ParticipantActionAdmissionRequest) -> None: +def _validate_admission_request_selection_and_execution(request: ParticipantActionAdmissionRequest) -> None: selection = request.validated_selection - if selection is None: - return - if not isinstance(selection, ParticipantValidatedActionSelection): + if selection is not None and not isinstance(selection, ParticipantValidatedActionSelection): raise TypeError("validated_selection must be a ParticipantValidatedActionSelection or None") - if selection.action_contract_address != request.action_contract_address: + if selection is not None and selection.action_contract_address != request.action_contract_address: raise ValueError("validated_selection action_contract_address must match the admission request") - - -def _validate_admission_request_execution(request: ParticipantActionAdmissionRequest) -> None: if not isinstance(request.requires_terminal_outcome, bool): raise TypeError("requires_terminal_outcome must be a bool") if (request.execution_scope_ref is None) != (request.execution_generation is None): raise ValueError("execution_scope_ref and execution_generation must be provided together") - if request.execution_scope_ref is None: - return - _require_non_empty(request.execution_scope_ref, "execution_scope_ref") - if request.execution_generation is None or request.execution_generation < 0: - raise ValueError("execution_generation must be non-negative") - if not request.target_addresses: - raise ValueError("generation-bound participant actions require target_addresses") + if request.execution_scope_ref is not None: + _require_non_empty(request.execution_scope_ref, "execution_scope_ref") + if request.execution_generation is None or request.execution_generation < 0: + raise ValueError("execution_generation must be non-negative") + if not request.target_addresses: + raise ValueError("generation-bound participant actions require target_addresses") -def _validate_admission_request_temporal_contexts(request: ParticipantActionAdmissionRequest) -> None: +def _validate_and_normalize_admission_request_contexts(request: ParticipantActionAdmissionRequest) -> None: if any(not isinstance(item, ParticipantTemporalRuntimeContextModel) for item in request.temporal_contexts): raise TypeError("temporal_contexts entries must be ParticipantTemporalRuntimeContextModel") if len({item.temporal_contract_id for item in request.temporal_contexts}) != len(request.temporal_contexts): raise ValueError("temporal_contexts temporal_contract_id values must be unique") - - -def _normalize_admission_request_tuples(request: ParticipantActionAdmissionRequest) -> None: for field_name in ( "evidence_refs", "visible_refs", @@ -198,9 +183,7 @@ class ParticipantActionApplyResult(ApplyResult): action_result: ParticipantActionResultModel | None = None -def participant_action_admission_request_violations( - request: ParticipantActionAdmissionRequest, -) -> tuple[str, ...]: +def participant_action_admission_request_violations(request: ParticipantActionAdmissionRequest) -> tuple[str, ...]: """Return manifest/selection compatibility violations for a binding request.""" return ( diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index 9169edd0d..dc6d139d2 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Iterable -from dataclasses import dataclass from raes_contracts.contracts import ( ParticipantAutonomousExecutionStateModel, @@ -12,7 +11,6 @@ from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_episode import ( ParticipantEpisodeInitializeRequest, - ParticipantEpisodeResetRequest, ) from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime @@ -34,6 +32,8 @@ run_policy_due_concurrently, ) from .participant_scheduler_policy import _policy_digest +from .participant_scheduler_reset import clock_reset_context, reset_scheduler_participant +from .participant_scheduler_time import cadence as _cadence from .participant_scheduler_time import clock_coordinate @@ -50,18 +50,6 @@ def _clock_tick(snapshot: RuntimeSnapshot, clock_address: str) -> int: return clock.coordinate.tick -def _cadence(policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel) -> tuple[int, int]: - selected = [ - constraint - for constraint in time_model.constraints - if constraint.address in policy.temporal_constraint_addresses and constraint.kind == "cadence" - ] - if len(selected) != 1 or selected[0].cadence_ticks is None: - raise ValueError("autonomous participant execution requires exactly one cadence constraint") - constraint = selected[0] - return constraint.start_tick or 0, constraint.cadence_ticks - - def _state_identity(state: ParticipantAutonomousExecutionStateModel) -> tuple[object, ...]: return ( state.policy_address, @@ -193,127 +181,6 @@ def _initialize_participant( return ApplyResult(success=True, snapshot=working, changed_addresses=changed) -@dataclass(frozen=True) -class _ClockResetContext: - policy: ParticipantAutonomousExecutionRuntime - time_model: CompiledTimeModel - participant_runtime: object - segment: int - current_tick: int - reset_participants: bool - activity_control: ParticipantActivityRandomControl | None - next_tick: int - timing_disposition: str - - -def _clock_reset_context( - policy: ParticipantAutonomousExecutionRuntime, - time_model: CompiledTimeModel, - participant_runtime: object, - segment: int, - current_tick: int, - reset_participants: bool, - activity_control: ParticipantActivityRandomControl | None, -) -> _ClockResetContext: - next_tick = current_tick - timing_disposition = "cadence" - if activity_control is None: - next_tick, cadence_ticks = _cadence(policy, time_model) - if next_tick < current_tick: - next_tick += ((current_tick - next_tick + cadence_ticks - 1) // cadence_ticks) * cadence_ticks - return _ClockResetContext( - policy=policy, - time_model=time_model, - participant_runtime=participant_runtime, - segment=segment, - current_tick=current_tick, - reset_participants=reset_participants, - activity_control=activity_control, - next_tick=next_tick, - timing_disposition=timing_disposition, - ) - - -def _reset_scheduler_participant( - context: _ClockResetContext, - snapshot: RuntimeSnapshot, - participant_address: str, -) -> ApplyResult: - working = snapshot - changed: list[str] = [] - if context.reset_participants: - reset = context.participant_runtime.reset( - ParticipantEpisodeResetRequest( - participant_address=participant_address, - episode_id=f"{participant_address}-autonomous-{context.segment}", - reason=f"shared clock reset to segment {context.segment}", - ), - working, - ) - if not reset.success: - return reset - working = reset.snapshot - changed.extend(reset.changed_addresses) - key = _state_key(context.policy.address, participant_address) - state = ParticipantAutonomousExecutionStateModel.model_validate( - working.participant_autonomous_execution_states[key] - ) - next_tick = context.next_tick - timing_disposition = context.timing_disposition - burst_size = 1 - if context.activity_control is not None: - burst_size = draw_activity_integer( - policy=context.policy, - participant_address=participant_address, - time_segment=context.segment, - occurrence_ordinal=0, - control=context.activity_control, - local_coordinate=2, - minimum=1, - maximum=context.policy.max_burst_size, - ) - timing = next_activity_timing( - policy=context.policy, - time_model=context.time_model, - participant_address=participant_address, - time_segment=context.segment, - occurrence_ordinal=0, - current_tick=context.current_tick, - control=context.activity_control, - ) - next_tick = timing.tick if timing.tick is not None else context.current_tick - timing_disposition = timing.disposition - states = dict(working.participant_autonomous_execution_states) - states[key] = state.model_copy( - update={ - "episode_id": working.participant_episode_results[participant_address]["episode_id"], - "lifecycle_state": "running", - "time_segment": context.segment, - "next_tick": next_tick, - "next_action_index": 0, - "attempted_actions": 0, - "succeeded_actions": 0, - "failed_actions": 0, - "in_flight": 0, - "last_action_instance_id": None, - "occurrence_ordinal": 0, - "current_retry": 0, - "burst_position": 0, - "last_candidate_id": None, - "completed_candidate_ids": [], - "candidate_cooldown_until": {}, - "burst_size": burst_size, - "next_timing_disposition": timing_disposition, - } - ).model_dump(mode="json") - working = working.with_entries( - dict(working.entries), - participant_autonomous_execution_states=states, - ) - changed.append(key) - return ApplyResult(success=True, snapshot=working, changed_addresses=changed) - - def _missing_execution_service_result( policy: ParticipantAutonomousExecutionRuntime, run: SchedulerRunState, @@ -478,7 +345,7 @@ def reset_clock( continue current_tick = _clock_tick(snapshot, clock_address) activity_control = activity_control_for(policy, resolved_activity_controls) - context = _clock_reset_context( + context = clock_reset_context( policy, time_model, participant_runtime, @@ -488,7 +355,7 @@ def reset_clock( activity_control, ) for participant_address in policy.participant_addresses: - result = _reset_scheduler_participant(context, working, participant_address) + result = reset_scheduler_participant(context, working, participant_address) if not result.success: return result working = result.snapshot diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_reset.py b/implementations/python/packages/raes_runtime/participant_scheduler_reset.py new file mode 100644 index 000000000..1ea65ebac --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_reset.py @@ -0,0 +1,133 @@ +"""Clock-boundary reset operations for autonomous participant scheduling.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.participant_episode import ParticipantEpisodeResetRequest +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + +from .participant_activity import ParticipantActivityRandomControl, draw_activity_integer, next_activity_timing +from .participant_scheduler_time import cadence + + +@dataclass(frozen=True) +class ClockResetContext: + policy: ParticipantAutonomousExecutionRuntime + time_model: CompiledTimeModel + participant_runtime: object + segment: int + current_tick: int + reset_participants: bool + activity_control: ParticipantActivityRandomControl | None + next_tick: int + timing_disposition: str + + +def clock_reset_context( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + segment: int, + current_tick: int, + reset_participants: bool, + activity_control: ParticipantActivityRandomControl | None, +) -> ClockResetContext: + next_tick = current_tick + if activity_control is None: + next_tick, cadence_ticks = cadence(policy, time_model) + if next_tick < current_tick: + next_tick += ((current_tick - next_tick + cadence_ticks - 1) // cadence_ticks) * cadence_ticks + return ClockResetContext( + policy=policy, + time_model=time_model, + participant_runtime=participant_runtime, + segment=segment, + current_tick=current_tick, + reset_participants=reset_participants, + activity_control=activity_control, + next_tick=next_tick, + timing_disposition="cadence", + ) + + +def reset_scheduler_participant( + context: ClockResetContext, + snapshot: RuntimeSnapshot, + participant_address: str, +) -> ApplyResult: + working = snapshot + changed: list[str] = [] + if context.reset_participants: + reset = context.participant_runtime.reset( + ParticipantEpisodeResetRequest( + participant_address=participant_address, + episode_id=f"{participant_address}-autonomous-{context.segment}", + reason=f"shared clock reset to segment {context.segment}", + ), + working, + ) + if not reset.success: + return reset + working = reset.snapshot + changed.extend(reset.changed_addresses) + key = f"{context.policy.address}.state.{participant_address}" + state = ParticipantAutonomousExecutionStateModel.model_validate( + working.participant_autonomous_execution_states[key] + ) + next_tick = context.next_tick + timing_disposition = context.timing_disposition + burst_size = 1 + if context.activity_control is not None: + burst_size = draw_activity_integer( + policy=context.policy, + participant_address=participant_address, + time_segment=context.segment, + occurrence_ordinal=0, + control=context.activity_control, + local_coordinate=2, + minimum=1, + maximum=context.policy.max_burst_size, + ) + timing = next_activity_timing( + policy=context.policy, + time_model=context.time_model, + participant_address=participant_address, + time_segment=context.segment, + occurrence_ordinal=0, + current_tick=context.current_tick, + control=context.activity_control, + ) + next_tick = timing.tick if timing.tick is not None else context.current_tick + timing_disposition = timing.disposition + states = dict(working.participant_autonomous_execution_states) + states[key] = state.model_copy( + update={ + "episode_id": working.participant_episode_results[participant_address]["episode_id"], + "lifecycle_state": "running", + "time_segment": context.segment, + "next_tick": next_tick, + "next_action_index": 0, + "attempted_actions": 0, + "succeeded_actions": 0, + "failed_actions": 0, + "in_flight": 0, + "last_action_instance_id": None, + "occurrence_ordinal": 0, + "current_retry": 0, + "burst_position": 0, + "last_candidate_id": None, + "completed_candidate_ids": [], + "candidate_cooldown_until": {}, + "burst_size": burst_size, + "next_timing_disposition": timing_disposition, + } + ).model_dump(mode="json") + working = working.with_entries( + dict(working.entries), + participant_autonomous_execution_states=states, + ) + changed.append(key) + return ApplyResult(success=True, snapshot=working, changed_addresses=changed) diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_time.py b/implementations/python/packages/raes_runtime/participant_scheduler_time.py index ce861d412..8ac2e6976 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_time.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_time.py @@ -15,6 +15,18 @@ def clock_coordinate(snapshot: RuntimeSnapshot, clock_address: str) -> tuple[int return clock.coordinate.segment, clock.coordinate.tick +def cadence(policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel) -> tuple[int, int]: + selected = [ + constraint + for constraint in time_model.constraints + if constraint.address in policy.temporal_constraint_addresses and constraint.kind == "cadence" + ] + if len(selected) != 1 or selected[0].cadence_ticks is None: + raise ValueError("autonomous participant execution requires exactly one cadence constraint") + constraint = selected[0] + return constraint.start_tick or 0, constraint.cadence_ticks + + def participant_time_domain( policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel, @@ -30,4 +42,4 @@ def participant_time_domain( }[domain.kind] -__all__ = ["clock_coordinate", "participant_time_domain"] +__all__ = ["cadence", "clock_coordinate", "participant_time_domain"] From b865398e10cd08d90e05036efe28ab9c3d5c8aad Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 01:51:33 +0200 Subject: [PATCH 32/55] style(participant): format control boundary --- .../raes_runtime/participant_execution_control_boundary.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 c80237220..8fb118a6e 100644 --- a/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py +++ b/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py @@ -76,10 +76,7 @@ def _common_readback_matches( before: ParticipantExecutionServiceStateModel, observed: ParticipantExecutionServiceStateModel, ) -> bool: - return ( - _observed_state_matches(request, observed) - and _has_new_transition_evidence(before, observed) - ) + return _observed_state_matches(request, observed) and _has_new_transition_evidence(before, observed) def _observed_state_matches( From 1e4c1c28212028e356c8d00b90671026c954fb07 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 02:04:44 +0200 Subject: [PATCH 33/55] Fix SonarCloud findings (cycle 1) --- implementations/python/tests/test_deprecation_lifecycle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/implementations/python/tests/test_deprecation_lifecycle.py b/implementations/python/tests/test_deprecation_lifecycle.py index 1c28a0bb8..b362c3f7b 100644 --- a/implementations/python/tests/test_deprecation_lifecycle.py +++ b/implementations/python/tests/test_deprecation_lifecycle.py @@ -372,10 +372,12 @@ def test_dropping_a_canonical_record_is_rejected(tmp_path: Path) -> None: def test_canonical_record_ids_are_pinned() -> None: - assert { + expected = { "legacy-python-distribution", "sdl-import-path-field", - } == CANONICAL_DEPRECATION_RECORD_IDS + } + actual = CANONICAL_DEPRECATION_RECORD_IDS + assert actual == expected def test_surface_classes_cover_known_matrix_rows() -> None: From 816d2fda5e66500dcef1a43f3c888754d7e9086f Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 02:15:43 +0200 Subject: [PATCH 34/55] refactor(participant): reduce scheduler branch complexity --- .../participant_execution_runtime.py | 30 ++++++----- .../raes_contracts/participant_binding.py | 9 ++-- .../raes_runtime/participant_scheduler.py | 45 ++++++++-------- .../participant_scheduler_concurrency.py | 54 ++++++++++--------- 4 files changed, 74 insertions(+), 64 deletions(-) diff --git a/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py index 135666590..25b7830dc 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py +++ b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py @@ -38,19 +38,23 @@ def _execution_generation_failure_reason( ) -> str | None: scope = request.execution_scope_ref payload = snapshot.participant_execution_services.get(scope) if scope is not None else None - if payload is None: - return "execution-service state is missing" - state = ParticipantExecutionServiceStateModel.model_validate(payload) - if ( - state.generation != request.execution_generation - or state.observed_generation != request.execution_generation - ): - return "execution generation changed" - if not completion and ( - state.observed_lifecycle != "running" or not state.accepting_new_work or state.readiness != "ready" - ): - return "execution service is not accepting work" - return None + reason = "execution-service state is missing" if payload is None else None + if payload is not None: + state = ParticipantExecutionServiceStateModel.model_validate(payload) + if ( + state.generation != request.execution_generation + or state.observed_generation != request.execution_generation + ): + reason = "execution generation changed" + if ( + reason is None + and not completion + and ( + state.observed_lifecycle != "running" or not state.accepting_new_work or state.readiness != "ready" + ) + ): + reason = "execution service is not accepting work" + return reason @staticmethod def _execution_generation_failure( diff --git a/implementations/python/packages/raes_contracts/participant_binding.py b/implementations/python/packages/raes_contracts/participant_binding.py index d7a3d7e7b..126d5eeaf 100644 --- a/implementations/python/packages/raes_contracts/participant_binding.py +++ b/implementations/python/packages/raes_contracts/participant_binding.py @@ -145,10 +145,11 @@ def _validate_admission_request_basics(request: ParticipantActionAdmissionReques def _validate_admission_request_selection_and_execution(request: ParticipantActionAdmissionRequest) -> None: selection = request.validated_selection - if selection is not None and not isinstance(selection, ParticipantValidatedActionSelection): - raise TypeError("validated_selection must be a ParticipantValidatedActionSelection or None") - if selection is not None and selection.action_contract_address != request.action_contract_address: - raise ValueError("validated_selection action_contract_address must match the admission request") + if selection is not None: + if not isinstance(selection, ParticipantValidatedActionSelection): + raise TypeError("validated_selection must be a ParticipantValidatedActionSelection or None") + if selection.action_contract_address != request.action_contract_address: + raise ValueError("validated_selection action_contract_address must match the admission request") if not isinstance(request.requires_terminal_outcome, bool): raise TypeError("requires_terminal_outcome must be a bool") if (request.execution_scope_ref is None) != (request.execution_generation is None): diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index dc6d139d2..bd3da6ce2 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -213,27 +213,30 @@ def _run_due_policy( service_payload = run.working.participant_execution_services.get(policy.address) if service_payload is None: run.failure = _missing_execution_service_result(policy, run) - return - service = ParticipantExecutionServiceStateModel.model_validate(service_payload) - if not _execution_service_accepts_work(service): - return - cadence_ticks = _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 - current_tick = _clock_tick(run.working, policy.clock_address) - if run_policy_due_concurrently(policy, time_model, participant_runtime, current_tick, cadence_ticks, run): - return - for participant_address in policy.participant_addresses: - run_participant_due( - policy, - time_model, - participant_runtime, - participant_address, - current_tick, - cadence_ticks, - run, - activity_controls, - ) - if run.failure is not None: - return + else: + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + if _execution_service_accepts_work(service): + cadence_ticks = ( + _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 + ) + current_tick = _clock_tick(run.working, policy.clock_address) + concurrent = run_policy_due_concurrently( + policy, time_model, participant_runtime, current_tick, cadence_ticks, run + ) + if not concurrent: + for participant_address in policy.participant_addresses: + run_participant_due( + policy, + time_model, + participant_runtime, + participant_address, + current_tick, + cadence_ticks, + run, + activity_controls, + ) + if run.failure is not None: + break class ParticipantScheduler: diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py index 2df31ba58..cc0229b96 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py @@ -333,31 +333,33 @@ def run_policy_due_concurrently( if policy.profile != "participant-autonomous-execution/v1": return False contexts, states = _due_contexts(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) - if run.failure is not None: - return True - if len(contexts) < 2 or policy.max_in_flight < 2: + enough_due_work = len(contexts) >= 2 and policy.max_in_flight >= 2 + if run.failure is None and not enough_due_work: return False - batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) - if not callable(batch_method): - _unsupported_concurrency_failure(policy, run) - return True - selected_contexts = tuple(contexts[: policy.max_in_flight]) - selected_states = states[: policy.max_in_flight] - from .participant_scheduler_operations import _bound_action_request - - requests = tuple( - _bound_action_request(context, run.working, state) - for context, state in zip(selected_contexts, selected_states, strict=True) - ) - _reserve_concurrent_actions(run, selected_contexts) - base = run.working - results = batch_method(requests, base, len(requests)) - if len(results) != len(requests): - raise ValueError("concurrent participant result count must match requests") - for context, state, request, result in zip(selected_contexts, selected_states, requests, results, strict=True): - _commit_concurrent_result(context, state, request, result, base, run) - if run.failure is not None: - break - _finish_concurrent_service_state(run, policy.address) - _finish_due_policy(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) + if run.failure is None: + batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) + if callable(batch_method): + selected_contexts = tuple(contexts[: policy.max_in_flight]) + selected_states = states[: policy.max_in_flight] + from .participant_scheduler_operations import _bound_action_request + + requests = tuple( + _bound_action_request(context, run.working, state) + for context, state in zip(selected_contexts, selected_states, strict=True) + ) + _reserve_concurrent_actions(run, selected_contexts) + base = run.working + results = batch_method(requests, base, len(requests)) + if len(results) != len(requests): + raise ValueError("concurrent participant result count must match requests") + for context, state, request, result in zip( + selected_contexts, selected_states, requests, results, strict=True + ): + _commit_concurrent_result(context, state, request, result, base, run) + if run.failure is not None: + break + _finish_concurrent_service_state(run, policy.address) + _finish_due_policy(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) + else: + _unsupported_concurrency_failure(policy, run) return True From baaaa506713c269ee7313dbe4ecf5fc727327bda Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 02:33:43 +0200 Subject: [PATCH 35/55] refactor(participant): isolate concurrent scheduler work --- .../participant_execution_runtime.py | 23 +++++-- .../raes_runtime/participant_scheduler.py | 50 +++++++------- .../participant_scheduler_concurrency.py | 69 +++++++++++-------- 3 files changed, 83 insertions(+), 59 deletions(-) diff --git a/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py index 25b7830dc..d72b88eb9 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py +++ b/implementations/python/packages/raes_backend_protocols/participant_execution_runtime.py @@ -29,6 +29,20 @@ def execution_state( return participant_execution_state(execution_scope_ref, snapshot) + @staticmethod + def _execution_generation_changed( + state: ParticipantExecutionServiceStateModel, + request: ParticipantActionAdmissionRequest, + ) -> bool: + return ( + state.generation != request.execution_generation + or state.observed_generation != request.execution_generation + ) + + @staticmethod + def _execution_service_accepts_work(state: ParticipantExecutionServiceStateModel) -> bool: + return state.observed_lifecycle == "running" and state.accepting_new_work and state.readiness == "ready" + @staticmethod def _execution_generation_failure_reason( request: ParticipantActionAdmissionRequest, @@ -41,17 +55,12 @@ def _execution_generation_failure_reason( reason = "execution-service state is missing" if payload is None else None if payload is not None: state = ParticipantExecutionServiceStateModel.model_validate(payload) - if ( - state.generation != request.execution_generation - or state.observed_generation != request.execution_generation - ): + if ParticipantExecutionRuntimeMixin._execution_generation_changed(state, request): reason = "execution generation changed" if ( reason is None and not completion - and ( - state.observed_lifecycle != "running" or not state.accepting_new_work or state.readiness != "ready" - ) + and not ParticipantExecutionRuntimeMixin._execution_service_accepts_work(state) ): reason = "execution service is not accepting work" return reason diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index bd3da6ce2..c100c25b7 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -203,6 +203,24 @@ def _execution_service_accepts_work(service: ParticipantExecutionServiceStateMod return service.observed_lifecycle == "running" and service.accepting_new_work and service.readiness == "ready" +def _run_serial_due( + policy, time_model, participant_runtime, current_tick, cadence_ticks, activity_controls, run +) -> None: + for participant_address in policy.participant_addresses: + run_participant_due( + policy, + time_model, + participant_runtime, + participant_address, + current_tick, + cadence_ticks, + run, + activity_controls, + ) + if run.failure is not None: + break + + def _run_due_policy( policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel, @@ -213,30 +231,14 @@ def _run_due_policy( service_payload = run.working.participant_execution_services.get(policy.address) if service_payload is None: run.failure = _missing_execution_service_result(policy, run) - else: - service = ParticipantExecutionServiceStateModel.model_validate(service_payload) - if _execution_service_accepts_work(service): - cadence_ticks = ( - _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 - ) - current_tick = _clock_tick(run.working, policy.clock_address) - concurrent = run_policy_due_concurrently( - policy, time_model, participant_runtime, current_tick, cadence_ticks, run - ) - if not concurrent: - for participant_address in policy.participant_addresses: - run_participant_due( - policy, - time_model, - participant_runtime, - participant_address, - current_tick, - cadence_ticks, - run, - activity_controls, - ) - if run.failure is not None: - break + return + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + if not _execution_service_accepts_work(service): + return + cadence_ticks = _cadence(policy, time_model)[1] if policy.profile == "participant-autonomous-execution/v1" else 0 + current_tick = _clock_tick(run.working, policy.clock_address) + if not run_policy_due_concurrently(policy, time_model, participant_runtime, current_tick, cadence_ticks, run): + _run_serial_due(policy, time_model, participant_runtime, current_tick, cadence_ticks, activity_controls, run) class ParticipantScheduler: diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py index cc0229b96..e6e869052 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py @@ -320,6 +320,41 @@ def _finish_due_policy( break +def _execute_concurrent_batch( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + current_tick: int, + cadence_ticks: int, + run: SchedulerRunState, + contexts: list[_DueActionContext], + states: list[ParticipantAutonomousExecutionStateModel], +) -> None: + batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) + if not callable(batch_method): + _unsupported_concurrency_failure(policy, run) + return + selected_contexts = tuple(contexts[: policy.max_in_flight]) + selected_states = states[: policy.max_in_flight] + from .participant_scheduler_operations import _bound_action_request + + requests = tuple( + _bound_action_request(context, run.working, state) + for context, state in zip(selected_contexts, selected_states, strict=True) + ) + _reserve_concurrent_actions(run, selected_contexts) + base = run.working + results = batch_method(requests, base, len(requests)) + if len(results) != len(requests): + raise ValueError("concurrent participant result count must match requests") + for context, state, request, result in zip(selected_contexts, selected_states, requests, results, strict=True): + _commit_concurrent_result(context, state, request, result, base, run) + if run.failure is not None: + break + _finish_concurrent_service_state(run, policy.address) + _finish_due_policy(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) + + def run_policy_due_concurrently( policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel, @@ -333,33 +368,11 @@ def run_policy_due_concurrently( if policy.profile != "participant-autonomous-execution/v1": return False contexts, states = _due_contexts(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) - enough_due_work = len(contexts) >= 2 and policy.max_in_flight >= 2 - if run.failure is None and not enough_due_work: + if run.failure is not None: + return True + if len(contexts) < 2 or policy.max_in_flight < 2: return False - if run.failure is None: - batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) - if callable(batch_method): - selected_contexts = tuple(contexts[: policy.max_in_flight]) - selected_states = states[: policy.max_in_flight] - from .participant_scheduler_operations import _bound_action_request - - requests = tuple( - _bound_action_request(context, run.working, state) - for context, state in zip(selected_contexts, selected_states, strict=True) - ) - _reserve_concurrent_actions(run, selected_contexts) - base = run.working - results = batch_method(requests, base, len(requests)) - if len(results) != len(requests): - raise ValueError("concurrent participant result count must match requests") - for context, state, request, result in zip( - selected_contexts, selected_states, requests, results, strict=True - ): - _commit_concurrent_result(context, state, request, result, base, run) - if run.failure is not None: - break - _finish_concurrent_service_state(run, policy.address) - _finish_due_policy(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) - else: - _unsupported_concurrency_failure(policy, run) + _execute_concurrent_batch( + policy, time_model, participant_runtime, current_tick, cadence_ticks, run, contexts, states + ) return True From 9c5939c8aff61bebe599eacba6ed1eb391a3ece7 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 02:57:40 +0200 Subject: [PATCH 36/55] refactor(participant): type scheduler execution batches --- .../raes_runtime/participant_scheduler.py | 8 +- .../participant_scheduler_concurrency.py | 76 ++++++++++++------- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index c100c25b7..3541f9c1e 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -204,7 +204,13 @@ def _execution_service_accepts_work(service: ParticipantExecutionServiceStateMod def _run_serial_due( - policy, time_model, participant_runtime, current_tick, cadence_ticks, activity_controls, run + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + current_tick: int, + cadence_ticks: int, + activity_controls: dict[str, ParticipantActivityRandomControl], + run: SchedulerRunState, ) -> None: for participant_address in policy.participant_addresses: run_participant_due( diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py index e6e869052..e1d700031 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel @@ -320,39 +321,49 @@ def _finish_due_policy( break -def _execute_concurrent_batch( - policy: ParticipantAutonomousExecutionRuntime, - time_model: CompiledTimeModel, - participant_runtime: object, - current_tick: int, - cadence_ticks: int, - run: SchedulerRunState, - contexts: list[_DueActionContext], - states: list[ParticipantAutonomousExecutionStateModel], -) -> None: - batch_method = getattr(participant_runtime, "admit_actions_concurrently", None) +@dataclass(frozen=True) +class _ConcurrentBatch: + policy: ParticipantAutonomousExecutionRuntime + time_model: CompiledTimeModel + participant_runtime: object + current_tick: int + cadence_ticks: int + run: SchedulerRunState + contexts: list[_DueActionContext] + states: list[ParticipantAutonomousExecutionStateModel] + + +def _execute_concurrent_batch(batch: _ConcurrentBatch) -> None: + batch_method = getattr(batch.participant_runtime, "admit_actions_concurrently", None) if not callable(batch_method): - _unsupported_concurrency_failure(policy, run) + _unsupported_concurrency_failure(batch.policy, batch.run) return - selected_contexts = tuple(contexts[: policy.max_in_flight]) - selected_states = states[: policy.max_in_flight] + selected_contexts = tuple(batch.contexts[: batch.policy.max_in_flight]) + selected_states = batch.states[: batch.policy.max_in_flight] from .participant_scheduler_operations import _bound_action_request requests = tuple( - _bound_action_request(context, run.working, state) + _bound_action_request(context, batch.run.working, state) for context, state in zip(selected_contexts, selected_states, strict=True) ) - _reserve_concurrent_actions(run, selected_contexts) - base = run.working + _reserve_concurrent_actions(batch.run, selected_contexts) + base = batch.run.working results = batch_method(requests, base, len(requests)) if len(results) != len(requests): raise ValueError("concurrent participant result count must match requests") for context, state, request, result in zip(selected_contexts, selected_states, requests, results, strict=True): - _commit_concurrent_result(context, state, request, result, base, run) - if run.failure is not None: + _commit_concurrent_result(context, state, request, result, base, batch.run) + if batch.run.failure is not None: break - _finish_concurrent_service_state(run, policy.address) - _finish_due_policy(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) + _finish_concurrent_service_state(batch.run, batch.policy.address) + _finish_due_policy( + batch.policy, + batch.time_model, + batch.participant_runtime, + batch.current_tick, + batch.cadence_ticks, + batch.run, + ) def run_policy_due_concurrently( @@ -368,11 +379,20 @@ def run_policy_due_concurrently( if policy.profile != "participant-autonomous-execution/v1": return False contexts, states = _due_contexts(policy, time_model, participant_runtime, current_tick, cadence_ticks, run) - if run.failure is not None: - return True - if len(contexts) < 2 or policy.max_in_flight < 2: + enough_due_work = len(contexts) >= 2 and policy.max_in_flight >= 2 + if run.failure is None and not enough_due_work: return False - _execute_concurrent_batch( - policy, time_model, participant_runtime, current_tick, cadence_ticks, run, contexts, states - ) - return True + if run.failure is None: + _execute_concurrent_batch( + _ConcurrentBatch( + policy=policy, + time_model=time_model, + participant_runtime=participant_runtime, + current_tick=current_tick, + cadence_ticks=cadence_ticks, + run=run, + contexts=contexts, + states=states, + ) + ) + return run.failure is not None or enough_due_work From b488c67d59c2f9ab8b0be5ef4e92100014d54016 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 05:31:54 +0200 Subject: [PATCH 37/55] Add scoped participant resource budgets --- .../backend-manifest-v2/valid/stub.json | 5 + .../valid/token-commit-event.json | 22 + .../valid/complete-resource-vector.json | 156 +++ .../valid/token-budget-state.json | 27 + .../valid/configured-inference-pool.json | 27 + .../provenance/sdl-lineage-ledger-v1.json | 206 +++- .../entries/backend-manifest-v2.json | 6 +- .../entries/backend-profile-v1.json | 6 +- .../instantiated-scenario-snapshot-v1.json | 6 +- .../entries/instantiated-scenario-v1.json | 6 +- ...pant-behavior-history-event-stream-v1.json | 6 +- ...articipant-execution-service-state-v1.json | 6 +- .../entries/participant-history-view-v1.json | 6 +- .../participant-resource-budget-event-v1.json | 10 + ...participant-resource-budget-policy-v1.json | 10 + .../participant-resource-budget-state-v1.json | 10 + ...participant-resource-pool-capacity-v1.json | 10 + .../entries/processor-manifest-v2.json | 6 +- .../entries/runtime-snapshot-v1.json | 6 +- .../scenario-satisfiability-evidence-v1.json | 6 +- .../entries/sdl-authoring-input-v1.json | 6 +- .../backend-manifest/backend-manifest-v2.json | 311 +++++- ...pant-behavior-history-event-stream-v1.json | 81 +- .../participant-history-view-v1.json | 81 +- ...articipant-execution-service-state-v1.json | 9 + .../participant-resource-budget-event-v1.json | 159 +++ ...participant-resource-budget-policy-v1.json | 303 ++++++ .../participant-resource-budget-state-v1.json | 184 ++++ ...participant-resource-pool-capacity-v1.json | 160 +++ .../processor-manifest-v2.json | 1 + .../schemas/profiles/backend-profile-v1.json | 4 + .../scenario-satisfiability-evidence-v1.json | 441 +++++++++ .../instantiated-scenario-snapshot-v1.json | 510 ++++++++++ .../schemas/sdl/instantiated-scenario-v1.json | 510 ++++++++++ .../schemas/sdl/sdl-authoring-input-v1.json | 441 +++++++++ .../snapshots/runtime-snapshot-v1.json | 693 ++++++++++++- docs/decisions/adrs/README.md | 2 + ...rce-budgets-and-shared-service-fairness.md | 406 ++++++++ docs/explain/reference/backend-conformance.md | 15 + docs/migration/README.md | 4 + docs/migration/autonomous-execution-v3.md | 109 ++ .../lineage/source-audit-2026-07-12.md | 45 + .../python/packages/raes/_mapping_scopes.py | 2 + .../python/packages/raes/composition.py | 34 + .../packages/raes/participant_execution.py | 31 +- .../raes/participant_resource_budgets.py | 227 +++++ .../raes/semantics/participant_behavior.py | 12 +- .../raes/validator/_content_objectives.py | 10 + .../_participant_resource_budget_owners.py | 83 ++ .../capability_admission.py | 12 +- .../participant_capabilities.py | 18 +- .../participant_execution_manifest.py | 14 + .../participant_resource_admission.py | 200 ++++ .../participant_resource_budgets.py | 131 +++ .../conformance/snapshot_semantics.py | 12 + .../raes_contracts/contracts/bundle.py | 10 + .../raes_contracts/contracts/manifests.py | 20 +- .../contracts/participant_execution.py | 2 + .../contracts/participant_resource_budgets.py | 496 ++++++++++ .../contracts/participant_resource_types.py | 125 +++ .../contracts/participant_runtime.py | 15 +- .../contracts/realization_plans.py | 44 + .../raes_contracts/manifest_authority.py | 5 + .../raes_contracts/participant_binding.py | 45 +- .../participant_binding_validation.py | 15 + .../packages/raes_contracts/runtime_state.py | 51 + .../participant_autonomous_execution.py | 150 ++- .../raes_processor/models/__init__.py | 6 + .../models/behavior_resources.py | 8 + .../models/participant_resources.py | 55 ++ .../raes_runtime/control_plane_api_models.py | 3 + .../raes_runtime/control_plane_store.py | 6 + .../participant_activity_support.py | 2 +- .../participant_execution_control.py | 5 + .../participant_execution_scheduler_state.py | 9 + .../participant_resource_accounting.py | 371 +++++++ .../participant_resource_budgets.py | 464 +++++++++ .../participant_resource_pool_ledger.py | 201 ++++ .../raes_runtime/participant_scheduler.py | 63 +- .../participant_scheduler_concurrency.py | 4 +- .../participant_scheduler_operations.py | 53 +- .../participant_scheduler_policy.py | 13 +- .../participant_scheduler_resources.py | 158 +++ .../participant_scheduler_time.py | 36 +- ...st_dsl_437_benign_participant_execution.py | 14 + ..._issue_899_participant_resource_budgets.py | 927 ++++++++++++++++++ .../autonomous-execution.md | 75 +- tools/generate_contract_schemas.py | 4 + tools/policy/historical_identity_records.json | 8 +- 89 files changed, 9147 insertions(+), 120 deletions(-) create mode 100644 contracts/fixtures/participant-runtime/participant-resource-budget-event-v1/valid/token-commit-event.json create mode 100644 contracts/fixtures/participant-runtime/participant-resource-budget-policy-v1/valid/complete-resource-vector.json create mode 100644 contracts/fixtures/participant-runtime/participant-resource-budget-state-v1/valid/token-budget-state.json create mode 100644 contracts/fixtures/participant-runtime/participant-resource-pool-capacity-v1/valid/configured-inference-pool.json create mode 100644 contracts/schema-publication/entries/participant-resource-budget-event-v1.json create mode 100644 contracts/schema-publication/entries/participant-resource-budget-policy-v1.json create mode 100644 contracts/schema-publication/entries/participant-resource-budget-state-v1.json create mode 100644 contracts/schema-publication/entries/participant-resource-pool-capacity-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-resource-budget-event-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-resource-budget-policy-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-resource-budget-state-v1.json create mode 100644 contracts/schemas/participant-runtime/participant-resource-pool-capacity-v1.json create mode 100644 docs/decisions/adrs/adr-097-scoped-participant-resource-budgets-and-shared-service-fairness.md create mode 100644 docs/migration/autonomous-execution-v3.md create mode 100644 implementations/python/packages/raes/participant_resource_budgets.py create mode 100644 implementations/python/packages/raes/validator/_participant_resource_budget_owners.py create mode 100644 implementations/python/packages/raes_backend_protocols/participant_resource_admission.py create mode 100644 implementations/python/packages/raes_backend_protocols/participant_resource_budgets.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_resource_types.py create mode 100644 implementations/python/packages/raes_processor/models/participant_resources.py create mode 100644 implementations/python/packages/raes_runtime/participant_resource_accounting.py create mode 100644 implementations/python/packages/raes_runtime/participant_resource_budgets.py create mode 100644 implementations/python/packages/raes_runtime/participant_resource_pool_ledger.py create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_resources.py create mode 100644 implementations/python/tests/test_issue_899_participant_resource_budgets.py 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 f08a2df5e..8c5f5d086 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -241,6 +241,7 @@ "white" ], "supported_execution_control_actions": [], + "resource_budgets": null, "supports_autonomous_execution": false, "supports_bounded_concurrency": false, "supports_execution_control": false @@ -395,6 +396,10 @@ "participant-execution-binding-v1", "participant-execution-control-v1", "participant-execution-service-state-v1", + "participant-resource-budget-policy-v1", + "participant-resource-pool-capacity-v1", + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", diff --git a/contracts/fixtures/participant-runtime/participant-resource-budget-event-v1/valid/token-commit-event.json b/contracts/fixtures/participant-runtime/participant-resource-budget-event-v1/valid/token-commit-event.json new file mode 100644 index 000000000..864d4c08b --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-resource-budget-event-v1/valid/token-commit-event.json @@ -0,0 +1,22 @@ +{ + "schema_version": "participant-resource-budget-event/v1", + "event_id": "action-1:participant.autonomous-execution.green.resource-budget-state.tokens:commit", + "operation_id": "action-1", + "budget_state_ref": "participant.autonomous-execution.green.resource-budget-state.tokens", + "budget_id": "tokens", + "policy_address": "participant.autonomous-execution.green", + "owner_ref": "provision.node.inference.service.http", + "pool_ref": "inference-pool", + "execution_generation": 3, + "transition": "commit", + "disposition": "committed", + "requested": 500, + "measured": 450, + "resource_kind": "inference_tokens", + "unit": "tokens", + "meter_profile_ref": "tokenizer.example/v1", + "predecessor_event_ref": "action-1:participant.autonomous-execution.green.resource-budget-state.tokens:reserve", + "evidence_refs": [ + "evidence.resource-meter.action-1" + ] +} diff --git a/contracts/fixtures/participant-runtime/participant-resource-budget-policy-v1/valid/complete-resource-vector.json b/contracts/fixtures/participant-runtime/participant-resource-budget-policy-v1/valid/complete-resource-vector.json new file mode 100644 index 000000000..bed396683 --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-resource-budget-policy-v1/valid/complete-resource-vector.json @@ -0,0 +1,156 @@ +{ + "schema_version": "participant-resource-budget-policy/v1", + "policy_id": "green-shared-capacity", + "policy_address": "participant.autonomous-execution.green", + "policy_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "owners": [ + { + "owner_id": "participant", + "kind": "participant", + "owner_ref": "participant.behavior.green" + }, + { + "owner_id": "range", + "kind": "deployment_tenant", + "owner_ref": "deployment.tenant.range-a" + }, + { + "owner_id": "inference", + "kind": "shared_service", + "owner_ref": "provision.node.inference.service.http" + }, + { + "owner_id": "fleet", + "kind": "fleet", + "owner_ref": "fleet.primary" + } + ], + "demands": [ + { + "budget_id": "actions", + "owner": { + "owner_id": "participant", + "kind": "participant", + "owner_ref": "participant.behavior.green" + }, + "pool_ref": "participant-pool", + "quantity": { + "resource_kind": "action_rate", + "unit": "actions", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "raes.action-attempt/v1", + "amount": 1 + }, + "limit": 24, + "reservation": 1, + "reset": "time_segment", + "window_ticks": 100 + }, + { + "budget_id": "concurrency", + "owner": { + "owner_id": "participant", + "kind": "participant", + "owner_ref": "participant.behavior.green" + }, + "pool_ref": "participant-pool", + "quantity": { + "resource_kind": "concurrent_actions", + "unit": "actions", + "accounting_mode": "reservable_gauge", + "meter_profile_ref": "raes.concurrent-action/v1", + "amount": 1 + }, + "limit": 2, + "reservation": 1, + "reset": "reconciled" + }, + { + "budget_id": "storage", + "owner": { + "owner_id": "range", + "kind": "deployment_tenant", + "owner_ref": "deployment.tenant.range-a" + }, + "pool_ref": "range-pool", + "quantity": { + "resource_kind": "storage_growth", + "unit": "bytes", + "accounting_mode": "growth_counter", + "meter_profile_ref": "raes.logical-byte/v1", + "amount": 4096 + }, + "limit": 1048576, + "reservation": 4096, + "reset": "reconciled" + }, + { + "budget_id": "tokens", + "owner": { + "owner_id": "inference", + "kind": "shared_service", + "owner_ref": "provision.node.inference.service.http" + }, + "pool_ref": "inference-pool", + "quantity": { + "resource_kind": "inference_tokens", + "unit": "tokens", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "tokenizer.example/v1", + "amount": 500 + }, + "limit": 20000, + "reservation": 500, + "reset": "time_segment", + "window_ticks": 100 + }, + { + "budget_id": "images", + "owner": { + "owner_id": "inference", + "kind": "shared_service", + "owner_ref": "provision.node.inference.service.http" + }, + "pool_ref": "inference-pool", + "quantity": { + "resource_kind": "image_generations", + "unit": "images", + "accounting_mode": "cumulative_counter", + "meter_profile_ref": "raes.image-generation/v1", + "amount": 1 + }, + "limit": 20, + "reservation": 1, + "reset": "run" + }, + { + "budget_id": "accelerator", + "owner": { + "owner_id": "fleet", + "kind": "fleet", + "owner_ref": "fleet.primary" + }, + "pool_ref": "accelerator-pool", + "quantity": { + "resource_kind": "accelerator", + "unit": "accelerator_milliseconds", + "accounting_mode": "lease", + "meter_profile_ref": "raes.accelerator-class.generic/v1", + "amount": 1000 + }, + "limit": 60000, + "reservation": 1000, + "reset": "reconciled" + } + ], + "fairness": { + "policy": "weighted_fair", + "priority_class": "background", + "weight": 1, + "protected": false, + "borrowing": "lendable_only", + "reclaim": "yield", + "max_queue_ticks": 20, + "starvation_bound_ticks": 100 + } +} diff --git a/contracts/fixtures/participant-runtime/participant-resource-budget-state-v1/valid/token-budget-state.json b/contracts/fixtures/participant-runtime/participant-resource-budget-state-v1/valid/token-budget-state.json new file mode 100644 index 000000000..55db429ef --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-resource-budget-state-v1/valid/token-budget-state.json @@ -0,0 +1,27 @@ +{ + "schema_version": "participant-resource-budget-state/v1", + "state_ref": "participant.autonomous-execution.green.resource-budget-state.tokens", + "budget_id": "tokens", + "policy_address": "participant.autonomous-execution.green", + "owner_kind": "shared_service", + "owner_ref": "provision.node.inference.service.http", + "pool_ref": "inference-pool", + "resource_kind": "inference_tokens", + "unit": "tokens", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "tokenizer.example/v1", + "reset": "time_segment", + "generation": 3, + "limit": 20000, + "configured_capacity": 20000, + "reserved": 0, + "current_use": 0, + "cumulative_use": 450, + "throttled": 0, + "rejected": 0, + "reconciliation_status": "reconciled", + "last_event_ref": "action-1:participant.autonomous-execution.green.resource-budget-state.tokens:commit", + "evidence_refs": [ + "evidence.resource-meter.action-1" + ] +} diff --git a/contracts/fixtures/participant-runtime/participant-resource-pool-capacity-v1/valid/configured-inference-pool.json b/contracts/fixtures/participant-runtime/participant-resource-pool-capacity-v1/valid/configured-inference-pool.json new file mode 100644 index 000000000..6d8c66b76 --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-resource-pool-capacity-v1/valid/configured-inference-pool.json @@ -0,0 +1,27 @@ +{ + "schema_version": "participant-resource-pool-capacity/v1", + "pool_ref": "inference-pool", + "owner_kind": "shared_service", + "owner_ref": "provision.node.inference.service.http", + "resource_kind": "inference_tokens", + "unit": "tokens", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "tokenizer.example/v1", + "capacity": 20000, + "tenant_isolation": "tenant_partitioned", + "configuration_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "fairness_policy": "weighted_fair", + "priority_classes": [ + "evaluated", + "standard", + "background" + ], + "borrowing": "lendable_only", + "reclaim": "yield", + "max_queue_ticks": 20, + "starvation_bound_ticks": 100, + "protected_capacity": 1000, + "evidence_contract_ids": [ + "participant-resource-budget-event-v1" + ] +} diff --git a/contracts/provenance/sdl-lineage-ledger-v1.json b/contracts/provenance/sdl-lineage-ledger-v1.json index 3739aa251..f770c7c5a 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-26", + "reviewed_on": "2026-07-27", "citations": [ { "citation_id": "ocr-sdl-v0.21.2", @@ -346,6 +346,100 @@ "canonical_url": "https://doi.org/10.1016/j.cose.2020.101837", "verified_on": "2026-07-12", "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#crack-publications" + }, + { + "citation_id": "kubernetes-resource-quota-2026", + "title": "Resource Quotas", + "authors_or_maintainer": [ + "Kubernetes Authors" + ], + "year": 2026, + "canonical_url": "https://kubernetes.io/docs/concepts/policy/resource-quotas/", + "verified_on": "2026-07-27", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-899-scoped-resource-governance-review" + }, + { + "citation_id": "kubernetes-api-priority-fairness-2026", + "title": "API Priority and Fairness", + "authors_or_maintainer": [ + "Kubernetes Authors" + ], + "year": 2026, + "canonical_url": "https://kubernetes.io/docs/concepts/cluster-administration/flow-control/", + "verified_on": "2026-07-27", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-899-scoped-resource-governance-review" + }, + { + "citation_id": "kueue-cluster-queue-2026", + "title": "Cluster Queue", + "authors_or_maintainer": [ + "Kueue Authors" + ], + "year": 2026, + "canonical_url": "https://kueue.sigs.k8s.io/docs/concepts/cluster_queue/", + "verified_on": "2026-07-27", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-899-scoped-resource-governance-review" + }, + { + "citation_id": "ghodsi-drf-2011", + "title": "Dominant Resource Fairness: Fair Allocation of Multiple Resource Types", + "authors_or_maintainer": [ + "Ali Ghodsi", + "Matei Zaharia", + "Benjamin Hindman", + "Andy Konwinski", + "Scott Shenker", + "Ion Stoica" + ], + "year": 2011, + "container_title": "8th USENIX Symposium on Networked Systems Design and Implementation", + "canonical_url": "https://www.usenix.org/conference/nsdi11/dominant-resource-fairness-fair-allocation-multiple-resource-types", + "verified_on": "2026-07-27", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-899-scoped-resource-governance-review" + }, + { + "citation_id": "oci-linux-container-configuration-2026", + "title": "Linux Container Configuration", + "authors_or_maintainer": [ + "Open Container Initiative" + ], + "year": 2026, + "canonical_url": "https://github.com/opencontainers/runtime-spec/blob/main/config-linux.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": "kubernetes-dynamic-resource-allocation-2026", + "title": "Dynamic Resource Allocation", + "authors_or_maintainer": [ + "Kubernetes Authors" + ], + "year": 2026, + "canonical_url": "https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/", + "verified_on": "2026-07-27", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-899-scoped-resource-governance-review" + }, + { + "citation_id": "opentelemetry-metrics-semconv-2026", + "title": "Metrics semantic conventions", + "authors_or_maintainer": [ + "OpenTelemetry Authors" + ], + "year": 2026, + "canonical_url": "https://opentelemetry.io/docs/specs/semconv/general/metrics/", + "verified_on": "2026-07-27", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-899-scoped-resource-governance-review" + }, + { + "citation_id": "opentelemetry-genai-metrics-2026", + "title": "Generative AI metrics", + "authors_or_maintainer": [ + "OpenTelemetry Authors" + ], + "year": 2026, + "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" } ], "sources": [ @@ -576,6 +670,73 @@ "version_or_edition": "version of record", "canonical_url": "https://doi.org/10.1145/1653662.1653673", "citation_ref": "bohannon-pierce-sjoberg-weirich-zdancewic-2009" + }, + { + "source_id": "kubernetes-resource-quota-2026", + "kind": "publication", + "title": "Kubernetes Resource Quotas", + "version_or_edition": "documentation reviewed 2026-07-27", + "canonical_url": "https://kubernetes.io/docs/concepts/policy/resource-quotas/", + "citation_ref": "kubernetes-resource-quota-2026" + }, + { + "source_id": "kubernetes-api-priority-fairness-2026", + "kind": "publication", + "title": "Kubernetes API Priority and Fairness", + "version_or_edition": "documentation reviewed 2026-07-27", + "canonical_url": "https://kubernetes.io/docs/concepts/cluster-administration/flow-control/", + "citation_ref": "kubernetes-api-priority-fairness-2026" + }, + { + "source_id": "kueue-cluster-queue-2026", + "kind": "publication", + "title": "Kueue Cluster Queue", + "version_or_edition": "documentation reviewed 2026-07-27", + "canonical_url": "https://kueue.sigs.k8s.io/docs/concepts/cluster_queue/", + "citation_ref": "kueue-cluster-queue-2026" + }, + { + "source_id": "ghodsi-drf-2011", + "kind": "publication", + "title": "Dominant Resource Fairness", + "version_or_edition": "NSDI 2011 publication", + "canonical_url": "https://www.usenix.org/conference/nsdi11/dominant-resource-fairness-fair-allocation-multiple-resource-types", + "citation_ref": "ghodsi-drf-2011" + }, + { + "source_id": "oci-linux-container-configuration-2026", + "kind": "standard", + "title": "OCI Linux Container Configuration", + "version_or_edition": "main documentation reviewed 2026-07-27", + "canonical_url": "https://github.com/opencontainers/runtime-spec/blob/main/config-linux.md", + "maintaining_body": "Open Container Initiative", + "citation_ref": "oci-linux-container-configuration-2026" + }, + { + "source_id": "kubernetes-dynamic-resource-allocation-2026", + "kind": "publication", + "title": "Kubernetes Dynamic Resource Allocation", + "version_or_edition": "documentation reviewed 2026-07-27", + "canonical_url": "https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/", + "citation_ref": "kubernetes-dynamic-resource-allocation-2026" + }, + { + "source_id": "opentelemetry-metrics-semconv-2026", + "kind": "standard", + "title": "OpenTelemetry Metrics Semantic Conventions", + "version_or_edition": "documentation reviewed 2026-07-27", + "canonical_url": "https://opentelemetry.io/docs/specs/semconv/general/metrics/", + "maintaining_body": "OpenTelemetry", + "citation_ref": "opentelemetry-metrics-semconv-2026" + }, + { + "source_id": "opentelemetry-genai-metrics-2026", + "kind": "standard", + "title": "OpenTelemetry Generative AI Metrics", + "version_or_edition": "main documentation reviewed 2026-07-27", + "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" } ], "subjects": [ @@ -936,7 +1097,15 @@ "fmi-3.0.2-2024", "ieee-1516.1-2010", "tena-overview-2024", - "openscenario-xml-1.3.0-2024" + "openscenario-xml-1.3.0-2024", + "kubernetes-resource-quota-2026", + "kubernetes-api-priority-fairness-2026", + "kueue-cluster-queue-2026", + "ghodsi-drf-2011", + "oci-linux-container-configuration-2026", + "kubernetes-dynamic-resource-allocation-2026", + "opentelemetry-metrics-semconv-2026", + "opentelemetry-genai-metrics-2026" ], "aces_boundaries": [ { @@ -980,9 +1149,29 @@ { "artifact": "ASAM OpenSCENARIO XML 1.3.0", "symbol_or_pointer": "sections 7.2 and 8.4: storyboard/entity/action/trigger lifecycle and execution" + }, + { + "artifact": "Kubernetes Resource Quotas and API Priority and Fairness", + "symbol_or_pointer": "scoped hard limits, request classification, priority levels, queues, and concurrency shares" + }, + { + "artifact": "Kueue Cluster Queue", + "symbol_or_pointer": "nominal quota, cohorts, borrowing/lending, and priority" + }, + { + "artifact": "Dominant Resource Fairness", + "symbol_or_pointer": "multi-resource demand vectors and dominant-share fairness" + }, + { + "artifact": "OCI Linux Container Configuration and Kubernetes Dynamic Resource Allocation", + "symbol_or_pointer": "logical resource/device demand separated from runtime enforcement and allocation" + }, + { + "artifact": "OpenTelemetry Metrics and Generative AI Metrics", + "symbol_or_pointer": "metric identity, units, bounded attributes, and token usage observation" } ], - "divergence": "ACES adapts only the participant-environment, multi-participant ordering, shared-time coordination, and clock-jump callback concerns. The nested autonomous-execution profiles, green-role restriction, evaluation-authority boundary, canonical references, reachable cadence and activity timing rules, work/pause window algebra, weighted candidate/dependency/retry/cooldown/burst policy, participant-occurrence random address, runtime-owned wall pacing, exact capability admission, and clock/episode/scheduler/provenance readback contracts are ACES-defined. V2 reuses ACES's separately governed random-stream engine and bounded-integer transform but remains within-run participant execution, not trial variation. Externally paced autonomous execution remains unsupported pending a portable transition-notification contract. ACES adopts no source syntax, actor ontology, scheduler algorithm, clock protocol, federation middleware, automotive hierarchy, RNG API, or wire compatibility.", + "divergence": "RAES adapts only participant-environment, multi-participant ordering, shared-time coordination, clock-jump, scoped quota, multi-resource fairness, logical isolation, and metric-identity concerns. The autonomous-execution profiles, authority boundaries, canonical references, v3 owner graph and complete resource vector, exact atomic admission, configured-pool contract, fairness obligations, generation-fenced accounting, reset reconciliation, and runtime evidence carriers are RAES-defined. V2 reuses RAES random streams; v3 does not implement the DRF allocator or adopt Kubernetes, Kueue, OCI, or OpenTelemetry syntax, APIs, scheduler algorithms, device/cgroup models, telemetry formats, or wire compatibility. Externally paced autonomous execution remains unsupported pending a portable transition-notification contract.", "compatibility": "none", "citation_refs": [ "cyborg-2021", @@ -993,12 +1182,21 @@ "fmi-3.0.2-2024", "ieee-1516.1-2010", "tena-overview-2024", - "openscenario-xml-1.3.0-2024" + "openscenario-xml-1.3.0-2024", + "kubernetes-resource-quota-2026", + "kubernetes-api-priority-fairness-2026", + "kueue-cluster-queue-2026", + "ghodsi-drf-2011", + "oci-linux-container-configuration-2026", + "kubernetes-dynamic-resource-allocation-2026", + "opentelemetry-metrics-semconv-2026", + "opentelemetry-genai-metrics-2026" ], "internal_authority_refs": [ "specs/sdl/sections.md", "docs/decisions/adrs/adr-084-scenario-variation-and-deterministic-trial-realization.md", "docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md", + "docs/decisions/adrs/adr-097-scoped-participant-resource-budgets-and-shared-service-fairness.md", "specs/formal/participant-semantics/autonomous-execution.md", "specs/formal/scenario-variation-trial-realization/README.md", "docs/research/participant-backend-contracts/prior-art-and-design-criteria.md", diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index 33455c47d..c61847761 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": "4d842923928a9e80e92fa31559fd557a846c4854ac41c30e4d88fa53288c2b22", + "content_hash": "fdce66f526907a096c90b6e9ef8e4141ad908a181a52670083e43f3471405f66", "last_change": { - "summary": "Combined current autonomous activity and policy capabilities with exact action-to-target bindings, lifecycle control, and bounded concurrent participant-service declarations for issue #898.", - "content_hash": "4d842923928a9e80e92fa31559fd557a846c4854ac41c30e4d88fa53288c2b22" + "summary": "Added versioned participant resource-budget support, configured pool capacity, fairness, isolation, and realization declarations for issue #899.", + "content_hash": "fdce66f526907a096c90b6e9ef8e4141ad908a181a52670083e43f3471405f66" } } diff --git a/contracts/schema-publication/entries/backend-profile-v1.json b/contracts/schema-publication/entries/backend-profile-v1.json index 1cd4e841b..4dd66c3dc 100644 --- a/contracts/schema-publication/entries/backend-profile-v1.json +++ b/contracts/schema-publication/entries/backend-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "736ef55570ccbfa97b966cfdf121eea0b75e42d8a1e94e0897371aa6a6502a47", + "content_hash": "ac32e06e7b7f8de2cfab4e6feda9bac56ae8b0768f8fdc2764fc24e4d335a913", "last_change": { - "summary": "Combined participant control and crossing occurrence contracts with execution binding, lifecycle control, and service-state contracts for issue #898.", - "content_hash": "736ef55570ccbfa97b966cfdf121eea0b75e42d8a1e94e0897371aa6a6502a47" + "summary": "Authorized backend profiles to require the participant resource-budget policy, pool-capacity, state, and event contracts for issue #899.", + "content_hash": "ac32e06e7b7f8de2cfab4e6feda9bac56ae8b0768f8fdc2764fc24e4d335a913" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json index 1b39345b9..77d677019 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": "812a22bf742131ea7c255c1305bafd8b695a2ad0ef50535361bd0dbde58c7e68", + "content_hash": "67e529931bccbc9d5aed61008b947f4e00ecf42e3c85ffabef06d94f85a214bb", "last_change": { - "summary": "Published the explicit autonomous activity v2 policy union in immutable instantiated scenario snapshots for issue #897.", - "content_hash": "812a22bf742131ea7c255c1305bafd8b695a2ad0ef50535361bd0dbde58c7e68" + "summary": "Published the v3 scoped participant resource-budget policy in immutable instantiated scenario snapshots for issue #899.", + "content_hash": "67e529931bccbc9d5aed61008b947f4e00ecf42e3c85ffabef06d94f85a214bb" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-v1.json b/contracts/schema-publication/entries/instantiated-scenario-v1.json index 8fb00be25..c39f442ef 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": "7c228bd1b4b1004c5f71c3e8b9b17dd63fdb146ac84fdc5ad5437d1c63075eda", + "content_hash": "5219c01dad11bc4f430e59d64c104de0911006724536d67d27ee271184bed020", "last_change": { - "summary": "Published the explicit autonomous activity v2 policy union in instantiated scenarios for issue #897.", - "content_hash": "7c228bd1b4b1004c5f71c3e8b9b17dd63fdb146ac84fdc5ad5437d1c63075eda" + "summary": "Published the v3 scoped participant resource-budget policy in instantiated scenarios for issue #899.", + "content_hash": "5219c01dad11bc4f430e59d64c104de0911006724536d67d27ee271184bed020" } } diff --git a/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json b/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json index 992c2b082..40d4e8c5f 100644 --- a/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json +++ b/contracts/schema-publication/entries/participant-behavior-history-event-stream-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-behavior-history-event-stream-v1", "schema_path": "contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json", "stability": "draft", - "content_hash": "d1baba6ec1967d08d28d8342ec526aa4041f8123e31d693cb744ad6a9351028a", + "content_hash": "46756a33b20550c709ecaacd41bb5621f291be9582c05b313b5076da0a9065f5", "last_change": { - "summary": "Added safe typed autonomous activity occurrence, attempt, timing, dependency, burst, and random-address provenance for issue #897.", - "content_hash": "d1baba6ec1967d08d28d8342ec526aa4041f8123e31d693cb744ad6a9351028a" + "summary": "Extended v3 activity history with native resource measurements and policy-scoped budget identity for issue #899.", + "content_hash": "46756a33b20550c709ecaacd41bb5621f291be9582c05b313b5076da0a9065f5" } } diff --git a/contracts/schema-publication/entries/participant-execution-service-state-v1.json b/contracts/schema-publication/entries/participant-execution-service-state-v1.json index eff9dd06d..0e235dbe8 100644 --- a/contracts/schema-publication/entries/participant-execution-service-state-v1.json +++ b/contracts/schema-publication/entries/participant-execution-service-state-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-execution-service-state-v1", "schema_path": "contracts/schemas/participant-runtime/participant-execution-service-state-v1.json", "stability": "draft", - "content_hash": "3cfd82795d44408fa86f3c89e221023a1924e596c8a72334b9f8ead01a33e9e8", + "content_hash": "9e1af40ed0e6c7be3766bb4219b23a3f5dd9f78d65d3faa82474f9b86299da46", "last_change": { - "summary": "Published participant execution lifecycle, generation, health, readiness, concurrency, shared-time provenance, and evidence readback for issue #898 under the RAES schema identity.", - "content_hash": "3cfd82795d44408fa86f3c89e221023a1924e596c8a72334b9f8ead01a33e9e8" + "summary": "Linked participant execution concurrency readback to authoritative resource-budget states for issue #899.", + "content_hash": "9e1af40ed0e6c7be3766bb4219b23a3f5dd9f78d65d3faa82474f9b86299da46" } } diff --git a/contracts/schema-publication/entries/participant-history-view-v1.json b/contracts/schema-publication/entries/participant-history-view-v1.json index 4a88322d5..3959fee02 100644 --- a/contracts/schema-publication/entries/participant-history-view-v1.json +++ b/contracts/schema-publication/entries/participant-history-view-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-history-view-v1", "schema_path": "contracts/schemas/control-plane/participant-history-view-v1.json", "stability": "draft", - "content_hash": "a76e3fabd050a42c46cb2459f93e02aa1b482b95c277fa166c4040a305b06320", + "content_hash": "483731937b2dbee8c5bc081f4348398a280a6ef28044c17395dcf635ba99c7c4", "last_change": { - "summary": "Projected safe autonomous activity occurrence provenance through API-408 participant history views for issue #897.", - "content_hash": "a76e3fabd050a42c46cb2459f93e02aa1b482b95c277fa166c4040a305b06320" + "summary": "Projected v3 native resource measurements and policy-scoped budget identity through history views for issue #899.", + "content_hash": "483731937b2dbee8c5bc081f4348398a280a6ef28044c17395dcf635ba99c7c4" } } diff --git a/contracts/schema-publication/entries/participant-resource-budget-event-v1.json b/contracts/schema-publication/entries/participant-resource-budget-event-v1.json new file mode 100644 index 000000000..ae0652722 --- /dev/null +++ b/contracts/schema-publication/entries/participant-resource-budget-event-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-resource-budget-event-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-resource-budget-event-v1.json", + "stability": "draft", + "content_hash": "b71b0880a3751dac2a51fa34621fdd1b8a1ff6e37ddd6c7a6c52b5ff3938d56b", + "last_change": { + "summary": "Published policy-scoped, transition-consistent resource evidence events with measured-use commits for issue #899.", + "content_hash": "b71b0880a3751dac2a51fa34621fdd1b8a1ff6e37ddd6c7a6c52b5ff3938d56b" + } +} diff --git a/contracts/schema-publication/entries/participant-resource-budget-policy-v1.json b/contracts/schema-publication/entries/participant-resource-budget-policy-v1.json new file mode 100644 index 000000000..ecec22e26 --- /dev/null +++ b/contracts/schema-publication/entries/participant-resource-budget-policy-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-resource-budget-policy-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-resource-budget-policy-v1.json", + "stability": "draft", + "content_hash": "40280cca88f0aa01c60861cea70aa5f6fff9ded2fd85473c45e339194317fe1d", + "last_change": { + "summary": "Published canonical participant resource-budget owners, typed demands, meters, reset policy, and fairness obligations for issue #899.", + "content_hash": "40280cca88f0aa01c60861cea70aa5f6fff9ded2fd85473c45e339194317fe1d" + } +} diff --git a/contracts/schema-publication/entries/participant-resource-budget-state-v1.json b/contracts/schema-publication/entries/participant-resource-budget-state-v1.json new file mode 100644 index 000000000..0459cd56d --- /dev/null +++ b/contracts/schema-publication/entries/participant-resource-budget-state-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-resource-budget-state-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-resource-budget-state-v1.json", + "stability": "draft", + "content_hash": "6fddd2ea39a2df27846c35e8c88e9985d64e12b78151f4fc23978d0820d49d8d", + "last_change": { + "summary": "Published canonical policy-scoped budget state for generation-fenced reservations, measured use, and reconciliation for issue #899.", + "content_hash": "6fddd2ea39a2df27846c35e8c88e9985d64e12b78151f4fc23978d0820d49d8d" + } +} diff --git a/contracts/schema-publication/entries/participant-resource-pool-capacity-v1.json b/contracts/schema-publication/entries/participant-resource-pool-capacity-v1.json new file mode 100644 index 000000000..e44381acf --- /dev/null +++ b/contracts/schema-publication/entries/participant-resource-pool-capacity-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-resource-pool-capacity-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-resource-pool-capacity-v1.json", + "stability": "draft", + "content_hash": "754e61686f6eab224e3ce605ff403e67797182b6e613deccdbbeaa0ca9d60022", + "last_change": { + "summary": "Published configuration-bound logical pool capacity, owner, meter, fairness, isolation, digest, and evidence claims for issue #899.", + "content_hash": "754e61686f6eab224e3ce605ff403e67797182b6e613deccdbbeaa0ca9d60022" + } +} diff --git a/contracts/schema-publication/entries/processor-manifest-v2.json b/contracts/schema-publication/entries/processor-manifest-v2.json index b7b189d4c..6c781b15b 100644 --- a/contracts/schema-publication/entries/processor-manifest-v2.json +++ b/contracts/schema-publication/entries/processor-manifest-v2.json @@ -2,9 +2,9 @@ "contract_id": "processor-manifest-v2", "schema_path": "contracts/schemas/processor-manifest/processor-manifest-v2.json", "stability": "draft", - "content_hash": "a1afaba381dfec1a3e05467baee34335a15c3e8c6f9e2b6cd20fe99f0484a174", + "content_hash": "17335ec9416d0ec5d0e3d7bee409c6d1caabd6250f8cf4159243e4483ebd8897", "last_change": { - "summary": "Completed the RAES identity hard cut for published schema URIs, extension keywords, and identity-bearing contract values under #908; validation semantics are unchanged apart from the intentional breaking name migration.", - "content_hash": "a1afaba381dfec1a3e05467baee34335a15c3e8c6f9e2b6cd20fe99f0484a174" + "summary": "Authorized processor declarations for the participant resource-budget policy contract introduced by issue #899.", + "content_hash": "17335ec9416d0ec5d0e3d7bee409c6d1caabd6250f8cf4159243e4483ebd8897" } } diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index acd7ee1c4..7ceedcc16 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": "eb09a73c33e562958bfb5dc606aa4ac52f8ffa9025cd6a4a0db206ffc3000f50", + "content_hash": "0a0504edc131b6f7b9d08fc47487d37223e8d389c7c1dcc11f4f76562b2cfe60", "last_change": { - "summary": "Combined autonomous activity v2 continuation and occurrence provenance with generation-fenced execution service health, readiness, capacity, shared-time provenance, and lifecycle evidence for issue #898.", - "content_hash": "eb09a73c33e562958bfb5dc606aa4ac52f8ffa9025cd6a4a0db206ffc3000f50" + "summary": "Added canonical participant budget states, authoritative physical-pool allocation ledgers, and evidence events for issue #899.", + "content_hash": "0a0504edc131b6f7b9d08fc47487d37223e8d389c7c1dcc11f4f76562b2cfe60" } } diff --git a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json index 6dc3a7166..20da4f811 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": "b67869d52a1c78da43d56d01465e85f09023e348ffd185b71e276b20f3798765", + "content_hash": "a7a2b5c03e42e64b499e21d902e1bfe1fcc6d7052df9119c45a252f5538d63df", "last_change": { - "summary": "Carried the explicit autonomous activity v2 policy union through satisfiability evidence scenario definitions for issue #897.", - "content_hash": "b67869d52a1c78da43d56d01465e85f09023e348ffd185b71e276b20f3798765" + "summary": "Carried the v3 scoped participant resource-budget policy through satisfiability evidence scenario definitions for issue #899.", + "content_hash": "a7a2b5c03e42e64b499e21d902e1bfe1fcc6d7052df9119c45a252f5538d63df" } } diff --git a/contracts/schema-publication/entries/sdl-authoring-input-v1.json b/contracts/schema-publication/entries/sdl-authoring-input-v1.json index 2a1865c5c..123225ac5 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": "f1bc1cfb91a66b739f94ee34ff5ee13070a9a062f4071a2693e8639e09a5ba92", + "content_hash": "cd3e58a6fb92f0de031d3f51dcad963ba43892d41721cd3e5dcb5e74e021d72c", "last_change": { - "summary": "Published the explicit autonomous activity v2 authoring profile with governed windows, timing, weighted candidates, retries, cooldowns, and bursts for issue #897.", - "content_hash": "f1bc1cfb91a66b739f94ee34ff5ee13070a9a062f4071a2693e8639e09a5ba92" + "summary": "Published the v3 autonomous profile with scoped owners, complete typed resource budgets, aggregation, fairness, and reset policy for issue #899.", + "content_hash": "cd3e58a6fb92f0de031d3f51dcad963ba43892d41721cd3e5dcb5e74e021d72c" } } diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index caa38bd18..57385b877 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -995,6 +995,299 @@ "title": "ParticipantFeatureSupportModel", "type": "object" }, + "ParticipantResourceBudgetCapabilitiesModel": { + "additionalProperties": false, + "description": "Declared support plus configuration-bound logical pool capacity.", + "properties": { + "configured_pools": { + "items": { + "$ref": "#/$defs/ParticipantResourcePoolCapacityModel" + }, + "minItems": 1, + "title": "Configured Pools", + "type": "array" + }, + "cross_range_pool_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Cross Range Pool Refs", + "type": "array" + }, + "realization_contract_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Realization Contract Ids", + "type": "array" + }, + "support_strength": { + "enum": [ + "unsupported", + "disclosed_weak", + "bounded", + "exact" + ], + "title": "Support Strength", + "type": "string" + }, + "supported_accounting_modes": { + "items": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "type": "string" + }, + "minItems": 1, + "title": "Supported Accounting Modes", + "type": "array" + }, + "supported_fairness_policies": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Fairness Policies", + "type": "array" + }, + "supported_isolation_strengths": { + "items": { + "enum": [ + "none", + "stateless", + "tenant_partitioned" + ], + "type": "string" + }, + "minItems": 1, + "title": "Supported Isolation Strengths", + "type": "array" + }, + "supported_owner_kinds": { + "items": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "type": "string" + }, + "minItems": 1, + "title": "Supported Owner Kinds", + "type": "array" + }, + "supported_reset_modes": { + "items": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "type": "string" + }, + "minItems": 1, + "title": "Supported Reset Modes", + "type": "array" + }, + "supported_resource_kinds": { + "items": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "type": "string" + }, + "minItems": 1, + "title": "Supported Resource Kinds", + "type": "array" + } + }, + "required": [ + "support_strength", + "supported_owner_kinds", + "supported_resource_kinds", + "supported_accounting_modes", + "supported_reset_modes", + "supported_fairness_policies", + "supported_isolation_strengths", + "configured_pools", + "realization_contract_ids" + ], + "title": "ParticipantResourceBudgetCapabilitiesModel", + "type": "object" + }, + "ParticipantResourcePoolCapacityModel": { + "additionalProperties": false, + "properties": { + "accounting_mode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "Accounting Mode", + "type": "string" + }, + "borrowing": { + "minLength": 1, + "title": "Borrowing", + "type": "string" + }, + "capacity": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Capacity", + "type": "integer" + }, + "configuration_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": "Configuration Digest", + "type": "string" + }, + "evidence_contract_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Contract Ids", + "type": "array" + }, + "fairness_policy": { + "minLength": 1, + "title": "Fairness Policy", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_kind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "Owner Kind", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "priority_classes": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Priority Classes", + "type": "array" + }, + "protected_capacity": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Protected Capacity", + "type": "integer" + }, + "reclaim": { + "minLength": 1, + "title": "Reclaim", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "schema_version": { + "const": "participant-resource-pool-capacity/v1", + "default": "participant-resource-pool-capacity/v1", + "title": "Schema Version", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "tenant_isolation": { + "enum": [ + "none", + "stateless", + "tenant_partitioned" + ], + "title": "Tenant Isolation", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "pool_ref", + "owner_kind", + "owner_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "capacity", + "tenant_isolation", + "configuration_digest", + "fairness_policy", + "priority_classes", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks", + "protected_capacity", + "evidence_contract_ids" + ], + "title": "ParticipantResourcePoolCapacityModel", + "type": "object" + }, "ParticipantRuntimeCapabilitiesModel": { "additionalProperties": false, "description": "Participant-episode lifecycle capability block (RUN-311).\n\nA backend that declares this block advertises that it implements\nthe full participant episode control surface on the\n``ParticipantRuntime`` protocol: ``initialize`` / ``reset`` /\n``restart`` / ``terminate`` plus ``status`` / ``results`` /\n``history``. Consumers of the manifest can infer the\n``FULL_REMOTE_CONTROL_PLANE`` conformance profile from this block.\n\nAPI-405 support dimensions live here because they are backend apparatus\nclaims: which participant roles, behavior features, and interaction\nfeatures this participant runtime can actually realize.", @@ -1129,6 +1422,17 @@ "title": "Name", "type": "string" }, + "resource_budgets": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantResourceBudgetCapabilitiesModel" + }, + { + "type": "null" + } + ], + "default": null + }, "supported_autonomous_action_contracts": { "items": { "minLength": 1, @@ -1169,7 +1473,8 @@ "items": { "enum": [ "participant-autonomous-execution/v1", - "participant-autonomous-execution/v2" + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3" ], "type": "string" }, @@ -1967,6 +2272,10 @@ "participant-execution-binding-v1", "participant-execution-control-v1", "participant-execution-service-state-v1", + "participant-resource-budget-policy-v1", + "participant-resource-pool-capacity-v1", + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", diff --git a/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json b/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json index f720e320d..738a1a1ce 100644 --- a/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json +++ b/contracts/schemas/control-plane/participant-behavior-history-event-stream-v1.json @@ -239,6 +239,13 @@ "title": "Preconditions", "type": "array" }, + "resource_measurements": { + "items": { + "$ref": "#/$defs/ParticipantResourceMeasurementModel" + }, + "title": "Resource Measurements", + "type": "array" + }, "status": { "enum": [ "accepted", @@ -302,7 +309,10 @@ "type": "string" }, "policy_profile": { - "const": "participant-autonomous-execution/v2", + "enum": [ + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3" + ], "title": "Policy Profile", "type": "string" }, @@ -982,6 +992,75 @@ "title": "ParticipantPreconditionClass", "type": "string" }, + "ParticipantResourceMeasurementModel": { + "additionalProperties": false, + "properties": { + "budget_state_ref": { + "minLength": 1, + "title": "Budget State Ref", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "execution_generation": { + "minimum": 0, + "title": "Execution Generation", + "type": "integer" + }, + "measured": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Measured", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "operation_id": { + "minLength": 1, + "title": "Operation Id", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "budget_state_ref", + "operation_id", + "execution_generation", + "resource_kind", + "unit", + "meter_profile_ref", + "measured", + "evidence_refs" + ], + "title": "ParticipantResourceMeasurementModel", + "type": "object" + }, "ParticipantRuntimeLifecyclePhase": { "description": "RUN-306 observable participant runtime lifecycle phases.", "enum": [ diff --git a/contracts/schemas/control-plane/participant-history-view-v1.json b/contracts/schemas/control-plane/participant-history-view-v1.json index acdb513d4..0f2e0f9b6 100644 --- a/contracts/schemas/control-plane/participant-history-view-v1.json +++ b/contracts/schemas/control-plane/participant-history-view-v1.json @@ -239,6 +239,13 @@ "title": "Preconditions", "type": "array" }, + "resource_measurements": { + "items": { + "$ref": "#/$defs/ParticipantResourceMeasurementModel" + }, + "title": "Resource Measurements", + "type": "array" + }, "status": { "enum": [ "accepted", @@ -302,7 +309,10 @@ "type": "string" }, "policy_profile": { - "const": "participant-autonomous-execution/v2", + "enum": [ + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3" + ], "title": "Policy Profile", "type": "string" }, @@ -1299,6 +1309,75 @@ "title": "ParticipantPreconditionClass", "type": "string" }, + "ParticipantResourceMeasurementModel": { + "additionalProperties": false, + "properties": { + "budget_state_ref": { + "minLength": 1, + "title": "Budget State Ref", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "execution_generation": { + "minimum": 0, + "title": "Execution Generation", + "type": "integer" + }, + "measured": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Measured", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "operation_id": { + "minLength": 1, + "title": "Operation Id", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "budget_state_ref", + "operation_id", + "execution_generation", + "resource_kind", + "unit", + "meter_profile_ref", + "measured", + "evidence_refs" + ], + "title": "ParticipantResourceMeasurementModel", + "type": "object" + }, "ParticipantRuntimeLifecyclePhase": { "description": "RUN-306 observable participant runtime lifecycle phases.", "enum": [ diff --git a/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json b/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json index 8c2aa607f..478224add 100644 --- a/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json +++ b/contracts/schemas/participant-runtime/participant-execution-service-state-v1.json @@ -139,6 +139,15 @@ "title": "Reserved", "type": "integer" }, + "resource_budget_state_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Resource Budget State Refs", + "type": "array" + }, "resources_released": { "title": "Resources Released", "type": "boolean" diff --git a/contracts/schemas/participant-runtime/participant-resource-budget-event-v1.json b/contracts/schemas/participant-runtime/participant-resource-budget-event-v1.json new file mode 100644 index 000000000..1dd418c76 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-resource-budget-event-v1.json @@ -0,0 +1,159 @@ +{ + "$id": "https://raes.dev/schemas/participant-resource-budget-event-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "budget_id": { + "minLength": 1, + "title": "Budget Id", + "type": "string" + }, + "budget_state_ref": { + "minLength": 1, + "title": "Budget State Ref", + "type": "string" + }, + "disposition": { + "enum": [ + "reserved", + "committed", + "released", + "throttled", + "rejected", + "reconciled" + ], + "title": "Disposition", + "type": "string" + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "evidence_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "execution_generation": { + "minimum": 0, + "title": "Execution Generation", + "type": "integer" + }, + "measured": { + "anyOf": [ + { + "maximum": 1000000000000000000, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Measured" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "operation_id": { + "minLength": 1, + "title": "Operation Id", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "predecessor_event_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Predecessor Event Ref" + }, + "requested": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Requested", + "type": "integer" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "schema_version": { + "const": "participant-resource-budget-event/v1", + "default": "participant-resource-budget-event/v1", + "title": "Schema Version", + "type": "string" + }, + "transition": { + "enum": [ + "reserve", + "commit", + "release", + "throttle", + "reject", + "reconcile" + ], + "title": "Transition", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "event_id", + "operation_id", + "budget_state_ref", + "budget_id", + "policy_address", + "owner_ref", + "pool_ref", + "execution_generation", + "transition", + "disposition", + "requested", + "resource_kind", + "unit", + "meter_profile_ref" + ], + "title": "ParticipantResourceBudgetEventModel", + "type": "object" +} diff --git a/contracts/schemas/participant-runtime/participant-resource-budget-policy-v1.json b/contracts/schemas/participant-runtime/participant-resource-budget-policy-v1.json new file mode 100644 index 000000000..d29cfd6bc --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-resource-budget-policy-v1.json @@ -0,0 +1,303 @@ +{ + "$defs": { + "ParticipantResourceBudgetDemandModel": { + "additionalProperties": false, + "properties": { + "budget_id": { + "minLength": 1, + "title": "Budget Id", + "type": "string" + }, + "evidence_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "limit": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/ParticipantResourceOwnerModel" + }, + "parent_budget_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Budget Ref" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "provenance": { + "default": "authored", + "enum": [ + "authored", + "legacy_maximum" + ], + "title": "Provenance", + "type": "string" + }, + "quantity": { + "$ref": "#/$defs/ParticipantResourceQuantityModel" + }, + "reservation": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Reservation", + "type": "integer" + }, + "reset": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "title": "Reset", + "type": "string" + }, + "window_ticks": { + "anyOf": [ + { + "maximum": 1000000000, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Window Ticks" + } + }, + "required": [ + "budget_id", + "owner", + "pool_ref", + "quantity", + "limit", + "reservation", + "reset" + ], + "title": "ParticipantResourceBudgetDemandModel", + "type": "object" + }, + "ParticipantResourceFairnessModel": { + "additionalProperties": false, + "properties": { + "borrowing": { + "minLength": 1, + "title": "Borrowing", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "policy": { + "minLength": 1, + "title": "Policy", + "type": "string" + }, + "priority_class": { + "minLength": 1, + "title": "Priority Class", + "type": "string" + }, + "protected": { + "title": "Protected", + "type": "boolean" + }, + "reclaim": { + "minLength": 1, + "title": "Reclaim", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "weight": { + "maximum": 1000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "policy", + "priority_class", + "weight", + "protected", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks" + ], + "title": "ParticipantResourceFairnessModel", + "type": "object" + }, + "ParticipantResourceOwnerModel": { + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "Kind", + "type": "string" + }, + "owner_id": { + "minLength": 1, + "title": "Owner Id", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + } + }, + "required": [ + "owner_id", + "kind", + "owner_ref" + ], + "title": "ParticipantResourceOwnerModel", + "type": "object" + }, + "ParticipantResourceQuantityModel": { + "additionalProperties": false, + "properties": { + "accounting_mode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "Accounting Mode", + "type": "string" + }, + "amount": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Amount", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "amount" + ], + "title": "ParticipantResourceQuantityModel", + "type": "object" + } + }, + "$id": "https://raes.dev/schemas/participant-resource-budget-policy-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "demands": { + "items": { + "$ref": "#/$defs/ParticipantResourceBudgetDemandModel" + }, + "minItems": 1, + "title": "Demands", + "type": "array" + }, + "fairness": { + "$ref": "#/$defs/ParticipantResourceFairnessModel" + }, + "owners": { + "items": { + "$ref": "#/$defs/ParticipantResourceOwnerModel" + }, + "minItems": 1, + "title": "Owners", + "type": "array" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "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" + }, + "policy_id": { + "minLength": 1, + "title": "Policy Id", + "type": "string" + }, + "schema_version": { + "const": "participant-resource-budget-policy/v1", + "default": "participant-resource-budget-policy/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "policy_id", + "policy_address", + "policy_digest", + "owners", + "demands", + "fairness" + ], + "title": "ParticipantResourceBudgetPolicyModel", + "type": "object" +} diff --git a/contracts/schemas/participant-runtime/participant-resource-budget-state-v1.json b/contracts/schemas/participant-runtime/participant-resource-budget-state-v1.json new file mode 100644 index 000000000..58279fbe2 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-resource-budget-state-v1.json @@ -0,0 +1,184 @@ +{ + "$id": "https://raes.dev/schemas/participant-resource-budget-state-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "accounting_mode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "Accounting Mode", + "type": "string" + }, + "budget_id": { + "minLength": 1, + "title": "Budget Id", + "type": "string" + }, + "configured_capacity": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Configured Capacity", + "type": "integer" + }, + "cumulative_use": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Cumulative Use", + "type": "integer" + }, + "current_use": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Current Use", + "type": "integer" + }, + "evidence_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "generation": { + "minimum": 0, + "title": "Generation", + "type": "integer" + }, + "last_event_ref": { + "minLength": 1, + "title": "Last Event Ref", + "type": "string" + }, + "limit": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_kind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "Owner Kind", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "reconciliation_status": { + "enum": [ + "reconciled", + "pending", + "unreconciled" + ], + "title": "Reconciliation Status", + "type": "string" + }, + "rejected": { + "minimum": 0, + "title": "Rejected", + "type": "integer" + }, + "reserved": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Reserved", + "type": "integer" + }, + "reset": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "title": "Reset", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "schema_version": { + "const": "participant-resource-budget-state/v1", + "default": "participant-resource-budget-state/v1", + "title": "Schema Version", + "type": "string" + }, + "state_ref": { + "minLength": 1, + "title": "State Ref", + "type": "string" + }, + "throttled": { + "minimum": 0, + "title": "Throttled", + "type": "integer" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "state_ref", + "budget_id", + "policy_address", + "owner_kind", + "owner_ref", + "pool_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "reset", + "generation", + "limit", + "configured_capacity", + "reserved", + "current_use", + "cumulative_use", + "throttled", + "rejected", + "reconciliation_status", + "last_event_ref" + ], + "title": "ParticipantResourceBudgetStateModel", + "type": "object" +} diff --git a/contracts/schemas/participant-runtime/participant-resource-pool-capacity-v1.json b/contracts/schemas/participant-runtime/participant-resource-pool-capacity-v1.json new file mode 100644 index 000000000..b944f2903 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-resource-pool-capacity-v1.json @@ -0,0 +1,160 @@ +{ + "$id": "https://raes.dev/schemas/participant-resource-pool-capacity-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "accounting_mode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "Accounting Mode", + "type": "string" + }, + "borrowing": { + "minLength": 1, + "title": "Borrowing", + "type": "string" + }, + "capacity": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Capacity", + "type": "integer" + }, + "configuration_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": "Configuration Digest", + "type": "string" + }, + "evidence_contract_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Contract Ids", + "type": "array" + }, + "fairness_policy": { + "minLength": 1, + "title": "Fairness Policy", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_kind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "Owner Kind", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "priority_classes": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Priority Classes", + "type": "array" + }, + "protected_capacity": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Protected Capacity", + "type": "integer" + }, + "reclaim": { + "minLength": 1, + "title": "Reclaim", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "schema_version": { + "const": "participant-resource-pool-capacity/v1", + "default": "participant-resource-pool-capacity/v1", + "title": "Schema Version", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "tenant_isolation": { + "enum": [ + "none", + "stateless", + "tenant_partitioned" + ], + "title": "Tenant Isolation", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "pool_ref", + "owner_kind", + "owner_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "capacity", + "tenant_isolation", + "configuration_digest", + "fairness_policy", + "priority_classes", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks", + "protected_capacity", + "evidence_contract_ids" + ], + "title": "ParticipantResourcePoolCapacityModel", + "type": "object" +} diff --git a/contracts/schemas/processor-manifest/processor-manifest-v2.json b/contracts/schemas/processor-manifest/processor-manifest-v2.json index 025dbf1ef..5aae39c9d 100644 --- a/contracts/schemas/processor-manifest/processor-manifest-v2.json +++ b/contracts/schemas/processor-manifest/processor-manifest-v2.json @@ -348,6 +348,7 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-resource-budget-policy-v1", "time-model-v1" ], "minLength": 1, diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index b3f263203..1af057238 100644 --- a/contracts/schemas/profiles/backend-profile-v1.json +++ b/contracts/schemas/profiles/backend-profile-v1.json @@ -32,6 +32,10 @@ "participant-execution-binding-v1", "participant-execution-control-v1", "participant-execution-service-state-v1", + "participant-resource-budget-policy-v1", + "participant-resource-pool-capacity-v1", + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", diff --git a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json index fb3988c24..2e44769c7 100644 --- a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json +++ b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json @@ -6153,6 +6153,157 @@ "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, + "ParticipantAutonomousExecutionPolicyV3": { + "additionalProperties": false, + "description": "V2 activity plus scoped multi-resource governance.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v3", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "title": "Progression Policy Ref", + "type": "string" + }, + "resource_budget": { + "$ref": "#/$defs/ParticipantResourceBudgetPolicy" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority", + "resource_budget" + ], + "title": "ParticipantAutonomousExecutionPolicyV3", + "type": "object" + }, "ParticipantBackendFailureMapping": { "additionalProperties": false, "description": "Mapping from a backend diagnostic code to a portable SEM-211 failure.", @@ -6266,6 +6417,9 @@ { "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV3" + }, { "type": "null" } @@ -7014,6 +7168,293 @@ "title": "ParticipantPreconditionClass", "type": "string" }, + "ParticipantResourceAccountingMode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "ParticipantResourceAccountingMode", + "type": "string" + }, + "ParticipantResourceBudgetDimension": { + "additionalProperties": false, + "properties": { + "accounting_mode": { + "$ref": "#/$defs/ParticipantResourceAccountingMode" + }, + "evidence_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "title": "Evidence Refs", + "type": "array" + }, + "limit": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_ref": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Owner Ref", + "type": "string" + }, + "parent_budget_ref": { + "anyOf": [ + { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Budget Ref" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "reservation": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Reservation", + "type": "integer" + }, + "reset": { + "$ref": "#/$defs/ParticipantResourceResetMode" + }, + "resource_kind": { + "$ref": "#/$defs/ParticipantResourceKind" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + }, + "window_ticks": { + "anyOf": [ + { + "maximum": 1000000000, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Window Ticks" + } + }, + "required": [ + "owner_ref", + "pool_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "limit", + "reservation", + "reset" + ], + "title": "ParticipantResourceBudgetDimension", + "type": "object" + }, + "ParticipantResourceBudgetPolicy": { + "additionalProperties": false, + "properties": { + "dimensions": { + "maxProperties": 4096, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceBudgetDimension" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Dimensions", + "type": "object" + }, + "fairness": { + "$ref": "#/$defs/ParticipantResourceFairness" + }, + "owners": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceOwner" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Owners", + "type": "object" + }, + "policy_id": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Policy Id", + "type": "string" + } + }, + "required": [ + "policy_id", + "owners", + "fairness", + "dimensions" + ], + "title": "ParticipantResourceBudgetPolicy", + "type": "object" + }, + "ParticipantResourceFairness": { + "additionalProperties": false, + "properties": { + "borrowing": { + "minLength": 1, + "title": "Borrowing", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "policy": { + "minLength": 1, + "title": "Policy", + "type": "string" + }, + "priority_class": { + "minLength": 1, + "title": "Priority Class", + "type": "string" + }, + "protected": { + "title": "Protected", + "type": "boolean" + }, + "reclaim": { + "minLength": 1, + "title": "Reclaim", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "weight": { + "maximum": 1000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "policy", + "priority_class", + "weight", + "protected", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks" + ], + "title": "ParticipantResourceFairness", + "type": "object" + }, + "ParticipantResourceKind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "ParticipantResourceKind", + "type": "string" + }, + "ParticipantResourceOwner": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/$defs/ParticipantResourceOwnerKind" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + } + }, + "required": [ + "kind", + "ref" + ], + "title": "ParticipantResourceOwner", + "type": "object" + }, + "ParticipantResourceOwnerKind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "ParticipantResourceOwnerKind", + "type": "string" + }, + "ParticipantResourceResetMode": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "title": "ParticipantResourceResetMode", + "type": "string" + }, "ParticipantTemporalContract": { "additionalProperties": false, "description": "Typed SEM-213 temporal contract for a participant action.", diff --git a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json index f6dc83966..31aceba71 100644 --- a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -7416,6 +7416,178 @@ "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, + "ParticipantAutonomousExecutionPolicyV3": { + "additionalProperties": false, + "description": "V2 activity plus scoped multi-resource governance.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v3", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Progression Policy Ref", + "type": "string" + }, + "resource_budget": { + "$ref": "#/$defs/ParticipantResourceBudgetPolicy" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority", + "resource_budget" + ], + "title": "ParticipantAutonomousExecutionPolicyV3", + "type": "object" + }, "ParticipantBackendFailureMapping": { "additionalProperties": false, "description": "Mapping from a backend diagnostic code to a portable SEM-211 failure.", @@ -7556,6 +7728,9 @@ { "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV3" + }, { "type": "null" } @@ -8446,6 +8621,341 @@ "title": "ParticipantPreconditionClass", "type": "string" }, + "ParticipantResourceAccountingMode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "ParticipantResourceAccountingMode", + "type": "string" + }, + "ParticipantResourceBudgetDimension": { + "additionalProperties": false, + "properties": { + "accounting_mode": { + "$ref": "#/$defs/ParticipantResourceAccountingMode" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "title": "Evidence Refs", + "type": "array" + }, + "limit": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_ref": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Owner Ref", + "type": "string" + }, + "parent_budget_ref": { + "anyOf": [ + { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Budget Ref" + }, + "pool_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Pool Ref", + "type": "string" + }, + "reservation": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Reservation", + "type": "integer" + }, + "reset": { + "$ref": "#/$defs/ParticipantResourceResetMode" + }, + "resource_kind": { + "$ref": "#/$defs/ParticipantResourceKind" + }, + "unit": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Unit", + "type": "string" + }, + "window_ticks": { + "anyOf": [ + { + "maximum": 1000000000, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Window Ticks" + } + }, + "required": [ + "owner_ref", + "pool_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "limit", + "reservation", + "reset" + ], + "title": "ParticipantResourceBudgetDimension", + "type": "object" + }, + "ParticipantResourceBudgetPolicy": { + "additionalProperties": false, + "properties": { + "dimensions": { + "maxProperties": 4096, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceBudgetDimension" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Dimensions", + "type": "object" + }, + "fairness": { + "$ref": "#/$defs/ParticipantResourceFairness" + }, + "owners": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceOwner" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Owners", + "type": "object" + }, + "policy_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Policy Id", + "type": "string" + } + }, + "required": [ + "policy_id", + "owners", + "fairness", + "dimensions" + ], + "title": "ParticipantResourceBudgetPolicy", + "type": "object" + }, + "ParticipantResourceFairness": { + "additionalProperties": false, + "properties": { + "borrowing": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Borrowing", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "policy": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Policy", + "type": "string" + }, + "priority_class": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Priority Class", + "type": "string" + }, + "protected": { + "title": "Protected", + "type": "boolean" + }, + "reclaim": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Reclaim", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "weight": { + "maximum": 1000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "policy", + "priority_class", + "weight", + "protected", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks" + ], + "title": "ParticipantResourceFairness", + "type": "object" + }, + "ParticipantResourceKind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "ParticipantResourceKind", + "type": "string" + }, + "ParticipantResourceOwner": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/$defs/ParticipantResourceOwnerKind" + }, + "ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ref", + "type": "string" + } + }, + "required": [ + "kind", + "ref" + ], + "title": "ParticipantResourceOwner", + "type": "object" + }, + "ParticipantResourceOwnerKind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "ParticipantResourceOwnerKind", + "type": "string" + }, + "ParticipantResourceResetMode": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "title": "ParticipantResourceResetMode", + "type": "string" + }, "ParticipantTemporalContract": { "additionalProperties": false, "description": "Typed SEM-213 temporal contract for a participant action.", diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index b5bd8a815..ff3c624a6 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -6773,6 +6773,178 @@ "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, + "ParticipantAutonomousExecutionPolicyV3": { + "additionalProperties": false, + "description": "V2 activity plus scoped multi-resource governance.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v3", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Progression Policy Ref", + "type": "string" + }, + "resource_budget": { + "$ref": "#/$defs/ParticipantResourceBudgetPolicy" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority", + "resource_budget" + ], + "title": "ParticipantAutonomousExecutionPolicyV3", + "type": "object" + }, "ParticipantBackendFailureMapping": { "additionalProperties": false, "description": "Mapping from a backend diagnostic code to a portable SEM-211 failure.", @@ -6913,6 +7085,9 @@ { "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV3" + }, { "type": "null" } @@ -7803,6 +7978,341 @@ "title": "ParticipantPreconditionClass", "type": "string" }, + "ParticipantResourceAccountingMode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "ParticipantResourceAccountingMode", + "type": "string" + }, + "ParticipantResourceBudgetDimension": { + "additionalProperties": false, + "properties": { + "accounting_mode": { + "$ref": "#/$defs/ParticipantResourceAccountingMode" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "maxItems": 1024, + "title": "Evidence Refs", + "type": "array" + }, + "limit": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_ref": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Owner Ref", + "type": "string" + }, + "parent_budget_ref": { + "anyOf": [ + { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Budget Ref" + }, + "pool_ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Pool Ref", + "type": "string" + }, + "reservation": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Reservation", + "type": "integer" + }, + "reset": { + "$ref": "#/$defs/ParticipantResourceResetMode" + }, + "resource_kind": { + "$ref": "#/$defs/ParticipantResourceKind" + }, + "unit": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Unit", + "type": "string" + }, + "window_ticks": { + "anyOf": [ + { + "maximum": 1000000000, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Window Ticks" + } + }, + "required": [ + "owner_ref", + "pool_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "limit", + "reservation", + "reset" + ], + "title": "ParticipantResourceBudgetDimension", + "type": "object" + }, + "ParticipantResourceBudgetPolicy": { + "additionalProperties": false, + "properties": { + "dimensions": { + "maxProperties": 4096, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceBudgetDimension" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Dimensions", + "type": "object" + }, + "fairness": { + "$ref": "#/$defs/ParticipantResourceFairness" + }, + "owners": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceOwner" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Owners", + "type": "object" + }, + "policy_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Policy Id", + "type": "string" + } + }, + "required": [ + "policy_id", + "owners", + "fairness", + "dimensions" + ], + "title": "ParticipantResourceBudgetPolicy", + "type": "object" + }, + "ParticipantResourceFairness": { + "additionalProperties": false, + "properties": { + "borrowing": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Borrowing", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "policy": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Policy", + "type": "string" + }, + "priority_class": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Priority Class", + "type": "string" + }, + "protected": { + "title": "Protected", + "type": "boolean" + }, + "reclaim": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Reclaim", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "weight": { + "maximum": 1000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "policy", + "priority_class", + "weight", + "protected", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks" + ], + "title": "ParticipantResourceFairness", + "type": "object" + }, + "ParticipantResourceKind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "ParticipantResourceKind", + "type": "string" + }, + "ParticipantResourceOwner": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/$defs/ParticipantResourceOwnerKind" + }, + "ref": { + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ref", + "type": "string" + } + }, + "required": [ + "kind", + "ref" + ], + "title": "ParticipantResourceOwner", + "type": "object" + }, + "ParticipantResourceOwnerKind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "ParticipantResourceOwnerKind", + "type": "string" + }, + "ParticipantResourceResetMode": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "title": "ParticipantResourceResetMode", + "type": "string" + }, "ParticipantTemporalContract": { "additionalProperties": false, "description": "Typed SEM-213 temporal contract for a participant action.", diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index 1b4e6ca82..0e0d5e3ca 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -6009,6 +6009,157 @@ "title": "ParticipantAutonomousExecutionPolicyV2", "type": "object" }, + "ParticipantAutonomousExecutionPolicyV3": { + "additionalProperties": false, + "description": "V2 activity plus scoped multi-resource governance.", + "properties": { + "action_candidates": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantActivityActionCandidate" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Action Candidates", + "type": "object" + }, + "clock_ref": { + "minLength": 1, + "title": "Clock Ref", + "type": "string" + }, + "empty_eligible_disposition": { + "enum": [ + "complete", + "wait" + ], + "title": "Empty Eligible Disposition", + "type": "string" + }, + "evaluation_authority": { + "$ref": "#/$defs/ParticipantEvaluationAuthority" + }, + "failure_policy": { + "$ref": "#/$defs/ParticipantExecutionFailurePolicy", + "default": "stop" + }, + "max_action_attempts": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Action Attempts", + "type": "integer" + }, + "max_burst_size": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max Burst Size", + "type": "integer" + }, + "max_in_flight": { + "default": 1, + "maximum": 1024, + "minimum": 1, + "title": "Max In Flight", + "type": "integer" + }, + "max_occurrences": { + "maximum": 1000000, + "minimum": 1, + "title": "Max Occurrences", + "type": "integer" + }, + "observation_boundary_ref": { + "minLength": 1, + "title": "Observation Boundary Ref", + "type": "string" + }, + "outside_window_disposition": { + "enum": [ + "next_opening", + "skip" + ], + "title": "Outside Window Disposition", + "type": "string" + }, + "participant_implementation_ref": { + "minLength": 1, + "title": "Participant Implementation Ref", + "type": "string" + }, + "pause_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "title": "Pause Window Refs", + "type": "array" + }, + "profile": { + "const": "participant-autonomous-execution/v3", + "title": "Profile", + "type": "string" + }, + "progression_policy_ref": { + "minLength": 1, + "title": "Progression Policy Ref", + "type": "string" + }, + "resource_budget": { + "$ref": "#/$defs/ParticipantResourceBudgetPolicy" + }, + "selection_strategy": { + "const": "weighted", + "title": "Selection Strategy", + "type": "string" + }, + "stochastic_control_ref": { + "minLength": 1, + "title": "Stochastic Control Ref", + "type": "string" + }, + "timing": { + "$ref": "#/$defs/ParticipantActivityTiming" + }, + "work_window_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "minItems": 1, + "title": "Work Window Refs", + "type": "array" + } + }, + "required": [ + "profile", + "participant_implementation_ref", + "clock_ref", + "progression_policy_ref", + "work_window_refs", + "observation_boundary_ref", + "stochastic_control_ref", + "selection_strategy", + "timing", + "outside_window_disposition", + "empty_eligible_disposition", + "action_candidates", + "max_occurrences", + "max_action_attempts", + "evaluation_authority", + "resource_budget" + ], + "title": "ParticipantAutonomousExecutionPolicyV3", + "type": "object" + }, "ParticipantBackendFailureMapping": { "additionalProperties": false, "description": "Mapping from a backend diagnostic code to a portable SEM-211 failure.", @@ -6122,6 +6273,9 @@ { "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV2" }, + { + "$ref": "#/$defs/ParticipantAutonomousExecutionPolicyV3" + }, { "type": "null" } @@ -6870,6 +7024,293 @@ "title": "ParticipantPreconditionClass", "type": "string" }, + "ParticipantResourceAccountingMode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "ParticipantResourceAccountingMode", + "type": "string" + }, + "ParticipantResourceBudgetDimension": { + "additionalProperties": false, + "properties": { + "accounting_mode": { + "$ref": "#/$defs/ParticipantResourceAccountingMode" + }, + "evidence_refs": { + "items": { + "type": "string" + }, + "maxItems": 1024, + "title": "Evidence Refs", + "type": "array" + }, + "limit": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_ref": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Owner Ref", + "type": "string" + }, + "parent_budget_ref": { + "anyOf": [ + { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Budget Ref" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "reservation": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Reservation", + "type": "integer" + }, + "reset": { + "$ref": "#/$defs/ParticipantResourceResetMode" + }, + "resource_kind": { + "$ref": "#/$defs/ParticipantResourceKind" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + }, + "window_ticks": { + "anyOf": [ + { + "maximum": 1000000000, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Window Ticks" + } + }, + "required": [ + "owner_ref", + "pool_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "limit", + "reservation", + "reset" + ], + "title": "ParticipantResourceBudgetDimension", + "type": "object" + }, + "ParticipantResourceBudgetPolicy": { + "additionalProperties": false, + "properties": { + "dimensions": { + "maxProperties": 4096, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceBudgetDimension" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Dimensions", + "type": "object" + }, + "fairness": { + "$ref": "#/$defs/ParticipantResourceFairness" + }, + "owners": { + "maxProperties": 1024, + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/ParticipantResourceOwner" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Owners", + "type": "object" + }, + "policy_id": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Policy Id", + "type": "string" + } + }, + "required": [ + "policy_id", + "owners", + "fairness", + "dimensions" + ], + "title": "ParticipantResourceBudgetPolicy", + "type": "object" + }, + "ParticipantResourceFairness": { + "additionalProperties": false, + "properties": { + "borrowing": { + "minLength": 1, + "title": "Borrowing", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "policy": { + "minLength": 1, + "title": "Policy", + "type": "string" + }, + "priority_class": { + "minLength": 1, + "title": "Priority Class", + "type": "string" + }, + "protected": { + "title": "Protected", + "type": "boolean" + }, + "reclaim": { + "minLength": 1, + "title": "Reclaim", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "weight": { + "maximum": 1000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "policy", + "priority_class", + "weight", + "protected", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks" + ], + "title": "ParticipantResourceFairness", + "type": "object" + }, + "ParticipantResourceKind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "ParticipantResourceKind", + "type": "string" + }, + "ParticipantResourceOwner": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/$defs/ParticipantResourceOwnerKind" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + } + }, + "required": [ + "kind", + "ref" + ], + "title": "ParticipantResourceOwner", + "type": "object" + }, + "ParticipantResourceOwnerKind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "ParticipantResourceOwnerKind", + "type": "string" + }, + "ParticipantResourceResetMode": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "title": "ParticipantResourceResetMode", + "type": "string" + }, "ParticipantTemporalContract": { "additionalProperties": false, "description": "Typed SEM-213 temporal contract for a participant action.", diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index e3273f292..f61a2ef13 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -562,6 +562,13 @@ "title": "Preconditions", "type": "array" }, + "resource_measurements": { + "items": { + "$ref": "#/$defs/ParticipantResourceMeasurementModel" + }, + "title": "Resource Measurements", + "type": "array" + }, "status": { "enum": [ "accepted", @@ -625,7 +632,10 @@ "type": "string" }, "policy_profile": { - "const": "participant-autonomous-execution/v2", + "enum": [ + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3" + ], "title": "Policy Profile", "type": "string" }, @@ -1267,7 +1277,8 @@ "default": "participant-autonomous-execution/v1", "enum": [ "participant-autonomous-execution/v1", - "participant-autonomous-execution/v2" + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3" ], "title": "Profile", "type": "string" @@ -2686,6 +2697,15 @@ "title": "Reserved", "type": "integer" }, + "resource_budget_state_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Resource Budget State Refs", + "type": "array" + }, "resources_released": { "title": "Resources Released", "type": "boolean" @@ -4567,6 +4587,654 @@ "title": "ParticipantProposalOccurrenceModel", "type": "object" }, + "ParticipantResourceBudgetEventModel": { + "additionalProperties": false, + "properties": { + "budget_id": { + "minLength": 1, + "title": "Budget Id", + "type": "string" + }, + "budget_state_ref": { + "minLength": 1, + "title": "Budget State Ref", + "type": "string" + }, + "disposition": { + "enum": [ + "reserved", + "committed", + "released", + "throttled", + "rejected", + "reconciled" + ], + "title": "Disposition", + "type": "string" + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "evidence_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "execution_generation": { + "minimum": 0, + "title": "Execution Generation", + "type": "integer" + }, + "measured": { + "anyOf": [ + { + "maximum": 1000000000000000000, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Measured" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "operation_id": { + "minLength": 1, + "title": "Operation Id", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "predecessor_event_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Predecessor Event Ref" + }, + "requested": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Requested", + "type": "integer" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "schema_version": { + "const": "participant-resource-budget-event/v1", + "default": "participant-resource-budget-event/v1", + "title": "Schema Version", + "type": "string" + }, + "transition": { + "enum": [ + "reserve", + "commit", + "release", + "throttle", + "reject", + "reconcile" + ], + "title": "Transition", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "event_id", + "operation_id", + "budget_state_ref", + "budget_id", + "policy_address", + "owner_ref", + "pool_ref", + "execution_generation", + "transition", + "disposition", + "requested", + "resource_kind", + "unit", + "meter_profile_ref" + ], + "title": "ParticipantResourceBudgetEventModel", + "type": "object" + }, + "ParticipantResourceBudgetStateModel": { + "additionalProperties": false, + "properties": { + "accounting_mode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "Accounting Mode", + "type": "string" + }, + "budget_id": { + "minLength": 1, + "title": "Budget Id", + "type": "string" + }, + "configured_capacity": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Configured Capacity", + "type": "integer" + }, + "cumulative_use": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Cumulative Use", + "type": "integer" + }, + "current_use": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Current Use", + "type": "integer" + }, + "evidence_refs": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "generation": { + "minimum": 0, + "title": "Generation", + "type": "integer" + }, + "last_event_ref": { + "minLength": 1, + "title": "Last Event Ref", + "type": "string" + }, + "limit": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_kind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "Owner Kind", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "reconciliation_status": { + "enum": [ + "reconciled", + "pending", + "unreconciled" + ], + "title": "Reconciliation Status", + "type": "string" + }, + "rejected": { + "minimum": 0, + "title": "Rejected", + "type": "integer" + }, + "reserved": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Reserved", + "type": "integer" + }, + "reset": { + "enum": [ + "episode", + "time_segment", + "run", + "reconciled" + ], + "title": "Reset", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "schema_version": { + "const": "participant-resource-budget-state/v1", + "default": "participant-resource-budget-state/v1", + "title": "Schema Version", + "type": "string" + }, + "state_ref": { + "minLength": 1, + "title": "State Ref", + "type": "string" + }, + "throttled": { + "minimum": 0, + "title": "Throttled", + "type": "integer" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "state_ref", + "budget_id", + "policy_address", + "owner_kind", + "owner_ref", + "pool_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "reset", + "generation", + "limit", + "configured_capacity", + "reserved", + "current_use", + "cumulative_use", + "throttled", + "rejected", + "reconciliation_status", + "last_event_ref" + ], + "title": "ParticipantResourceBudgetStateModel", + "type": "object" + }, + "ParticipantResourceMeasurementModel": { + "additionalProperties": false, + "properties": { + "budget_state_ref": { + "minLength": 1, + "title": "Budget State Ref", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "execution_generation": { + "minimum": 0, + "title": "Execution Generation", + "type": "integer" + }, + "measured": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Measured", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "operation_id": { + "minLength": 1, + "title": "Operation Id", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "budget_state_ref", + "operation_id", + "execution_generation", + "resource_kind", + "unit", + "meter_profile_ref", + "measured", + "evidence_refs" + ], + "title": "ParticipantResourceMeasurementModel", + "type": "object" + }, + "ParticipantResourcePoolAllocationModel": { + "additionalProperties": false, + "properties": { + "borrowing": { + "minLength": 1, + "title": "Borrowing", + "type": "string" + }, + "budget_id": { + "minLength": 1, + "title": "Budget Id", + "type": "string" + }, + "budget_state_ref": { + "minLength": 1, + "title": "Budget State Ref", + "type": "string" + }, + "cumulative_use": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Cumulative Use", + "type": "integer" + }, + "current_use": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Current Use", + "type": "integer" + }, + "generation": { + "minimum": 0, + "title": "Generation", + "type": "integer" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "policy_address": { + "minLength": 1, + "title": "Policy Address", + "type": "string" + }, + "priority_class": { + "minLength": 1, + "title": "Priority Class", + "type": "string" + }, + "protected": { + "title": "Protected", + "type": "boolean" + }, + "reclaim": { + "minLength": 1, + "title": "Reclaim", + "type": "string" + }, + "reserved": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Reserved", + "type": "integer" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "weight": { + "maximum": 1000000, + "minimum": 1, + "title": "Weight", + "type": "integer" + } + }, + "required": [ + "budget_state_ref", + "policy_address", + "budget_id", + "generation", + "priority_class", + "weight", + "protected", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks", + "reserved", + "current_use", + "cumulative_use" + ], + "title": "ParticipantResourcePoolAllocationModel", + "type": "object" + }, + "ParticipantResourcePoolStateModel": { + "additionalProperties": false, + "description": "Authoritative allocation ledger for one exact physical resource pool.", + "properties": { + "accounting_mode": { + "enum": [ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease" + ], + "title": "Accounting Mode", + "type": "string" + }, + "allocations": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantResourcePoolAllocationModel" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Allocations", + "type": "object" + }, + "borrowing": { + "minLength": 1, + "title": "Borrowing", + "type": "string" + }, + "capacity": { + "maximum": 1000000000000000000, + "minimum": 1, + "title": "Capacity", + "type": "integer" + }, + "fairness_policy": { + "minLength": 1, + "title": "Fairness Policy", + "type": "string" + }, + "max_queue_ticks": { + "maximum": 1000000000, + "minimum": 0, + "title": "Max Queue Ticks", + "type": "integer" + }, + "meter_profile_ref": { + "minLength": 1, + "title": "Meter Profile Ref", + "type": "string" + }, + "owner_kind": { + "enum": [ + "participant", + "deployment_tenant", + "shared_service", + "fleet" + ], + "title": "Owner Kind", + "type": "string" + }, + "owner_ref": { + "minLength": 1, + "title": "Owner Ref", + "type": "string" + }, + "pool_ref": { + "minLength": 1, + "title": "Pool Ref", + "type": "string" + }, + "pool_state_ref": { + "minLength": 1, + "title": "Pool State Ref", + "type": "string" + }, + "priority_classes": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Priority Classes", + "type": "array" + }, + "protected_capacity": { + "maximum": 1000000000000000000, + "minimum": 0, + "title": "Protected Capacity", + "type": "integer" + }, + "reclaim": { + "minLength": 1, + "title": "Reclaim", + "type": "string" + }, + "resource_kind": { + "enum": [ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator" + ], + "title": "Resource Kind", + "type": "string" + }, + "starvation_bound_ticks": { + "maximum": 1000000000, + "minimum": 1, + "title": "Starvation Bound Ticks", + "type": "integer" + }, + "unit": { + "minLength": 1, + "title": "Unit", + "type": "string" + } + }, + "required": [ + "pool_state_ref", + "pool_ref", + "owner_kind", + "owner_ref", + "resource_kind", + "unit", + "accounting_mode", + "meter_profile_ref", + "capacity", + "protected_capacity", + "fairness_policy", + "priority_classes", + "borrowing", + "reclaim", + "max_queue_ticks", + "starvation_bound_ticks" + ], + "title": "ParticipantResourcePoolStateModel", + "type": "object" + }, "ParticipantRuntimeLifecyclePhase": { "description": "RUN-306 observable participant runtime lifecycle phases.", "enum": [ @@ -7321,6 +7989,27 @@ "title": "Participant Execution Services", "type": "object" }, + "participant_resource_budget_events": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantResourceBudgetEventModel" + }, + "title": "Participant Resource Budget Events", + "type": "object" + }, + "participant_resource_budget_states": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantResourceBudgetStateModel" + }, + "title": "Participant Resource Budget States", + "type": "object" + }, + "participant_resource_pool_states": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantResourcePoolStateModel" + }, + "title": "Participant Resource Pool States", + "type": "object" + }, "proposition_truth_results": { "additionalProperties": { "$ref": "#/$defs/PropositionTruthResultModel" diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index 6a733c128..4eea152d2 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -140,6 +140,7 @@ adr-093-raes-rename-and-compatibility-boundaries adr-094-authoritative-cross-plane-experiment-bindings adr-095-participant-decision-epoch-state-cut-and-delivery-semantics adr-096-identity-cutover-and-historical-record-boundary +adr-097-scoped-participant-resource-budgets-and-shared-service-fairness ``` | ADR | Title | Status | Date | @@ -240,3 +241,4 @@ adr-096-identity-cutover-and-historical-record-boundary | [094](adr-094-authoritative-cross-plane-experiment-bindings.md) | Authoritative Cross-Plane Experiment Bindings | accepted | 2026-07-26 | | [095](adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md) | Participant Decision Epoch, State-Cut, And Delivery Semantics | accepted | 2026-07-26 | | [096](adr-096-identity-cutover-and-historical-record-boundary.md) | Identity Cutover and Historical-Record Boundary | accepted | 2026-07-26 | +| [097](adr-097-scoped-participant-resource-budgets-and-shared-service-fairness.md) | Scoped Participant Resource Budgets And Shared-Service Fairness | proposed | 2026-07-27 | diff --git a/docs/decisions/adrs/adr-097-scoped-participant-resource-budgets-and-shared-service-fairness.md b/docs/decisions/adrs/adr-097-scoped-participant-resource-budgets-and-shared-service-fairness.md new file mode 100644 index 000000000..a67e9f078 --- /dev/null +++ b/docs/decisions/adrs/adr-097-scoped-participant-resource-budgets-and-shared-service-fairness.md @@ -0,0 +1,406 @@ +# ADR-097: Scoped Participant Resource Budgets And Shared-Service Fairness + +## Status + +proposed + +## Date + +2026-07-27 + +## Classification + +Classification: FM3 + +Required artifacts: authority-boundary decision, formal state and aggregation +invariants, closed authored and runtime contracts, semantic and instantiated +admission, canonical compilation, atomic backend-capacity admission, runtime +accounting and reset reconciliation, negative and race tests, cross-range +isolation probes, evidence-bearing conformance, lineage, migration, and +bidirectional issue/test/release traceability. + +Waivers: none. + +## Context + +Issue #899 requires participant activity to be governed across action rate, +concurrency, storage growth, inference tokens, image generation, and +accelerator use. Limits can be owned by a participant, deployment tenant/range, +shared service, or fleet pool. Admission must protect evaluated participants +and range reliability when autonomous participants contend for shared +inference and accelerator capacity. + +The repository already has adjacent authority: + +- ADR-022 and the participant-semantics specification reserve participant + budget and quota/exhaustion meaning for the participant semantic family. +- ADR-092 and + `ParticipantBehaviorSpecification.autonomous_execution` own autonomous + participant activity. V1 and v2 already carry finite attempt, occurrence, + burst, retry, and in-flight limits. +- ADR-087 owns deployment-tenant identity, shared-service use, tenant + isolation, mutable-state ownership, and reset-generation ownership. It + explicitly does not make deployment cells cloud projects or quota + boundaries. +- ADR-060 and `backend-manifest-v2.capabilities.participant_runtime` own + participant-runtime capability and evidence claims. +- `participant-execution-binding-v1`, + `participant-execution-service-state-v1`, planner capability admission, and + the participant scheduler already own exact native bindings, bounded + concurrency, generation fencing, and typed capacity/readback. +- ADR-054, ADR-066, `RuntimeSnapshot`, `ControlPlaneStore`, `Diagnostic`, + operation status, audit events, and conformance reports own runtime state, + durable evidence, public errors, and observability/evidence separation. + +The current maxima express only policy-local counts and backend-declared +ceilings. They cannot express multi-resource accounting, aggregation, +configured shared-pool capacity, tenant isolation, fairness, reset +reconciliation, or measured throttling. Extending them as more unrelated +`max_*` fields would conflate authored demand, backend support, configured +capacity, current availability, and measured use. + +Relevant precedent is deliberately used as design guidance, not as a wire +format: + +- Kubernetes + [ResourceQuota](https://kubernetes.io/docs/concepts/policy/resource-quotas/) + separates scoped quota from workload objects, while + [API Priority and Fairness](https://kubernetes.io/docs/concepts/cluster-administration/flow-control/) + separates priority classes, queues, and concurrency control. +- Kueue + [ClusterQueues and cohorts](https://kueue.sigs.k8s.io/docs/concepts/cluster_queue/) + distinguish nominal quota, borrowing/lending, priority, and cohort-wide + capacity. +- Dominant Resource Fairness + ([Ghodsi et al., NSDI 2011](https://www.usenix.org/conference/nsdi11/dominant-resource-fairness-fair-allocation-multiple-resource-types)) + demonstrates why multi-resource fairness cannot be reduced to one scalar + quota. +- The OCI runtime + [Linux resource model](https://github.com/opencontainers/runtime-spec/blob/main/config-linux.md) + and Kubernetes + [Dynamic Resource Allocation](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/) + separate logical demand from host CPU, memory, device, and accelerator + enforcement. +- OpenTelemetry's + [metric guidance](https://opentelemetry.io/docs/specs/semconv/general/metrics/) + and + [GenAI token metric](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-metrics.md) + reinforce explicit units, meter identity, and bounded-cardinality + attributes. Telemetry remains an observation projection, not RAES policy or + evidence authority. + +## Decision + +### 1. Extend participant semantics; do not create another actor or scheduler + +Resource governance is part of ordinary participant execution and shared +service admission. Autonomous policies continue to compile and execute through +the ADR-092 participant scheduler, execution bindings, lifecycle control, and +backend protocol. + +V1 and v2 autonomous profiles keep their existing meaning. A richer authored +profile must be versioned and reference governed resource-budget policy rather +than silently changing v2. Existing attempt, occurrence, retry, burst, and +in-flight fields compile into the same canonical budget demand used by the new +profile, with legacy provenance. There is one planner/runtime enforcement path, +not a legacy validator beside a new budget service. + +The portable budget models are reusable by ordinary evaluated or +non-evaluated participants and shared services. They contain no provider, +product, model-vendor, or KeplerOps-specific field. + +### 2. One contract family, with separate intent, capacity, and observation carriers + +Adopt one versioned participant-resource-budget contract family with shared +identity, scope, resource-dimension, quantity, meter, reset, aggregation, +priority, and evidence value models. The family has distinct carriers: + +1. **Budget policy/demand** records authored or admitted limits and resource + demand. +2. **Configured pool capacity** records the configuration-bound capacity and + isolation posture available from one logical backend/shared pool. +3. **Budget runtime state and events** use a canonical policy-scoped state + identity and record logical reservations, use, throttling/rejection, reset + reconciliation, and evidence. +4. **Physical-pool runtime state** is the single allocation authority for an + exact owner/pool/resource/unit/accounting/meter identity across every + admitted policy. + +These carriers must not copy one another's authority. They join by exact +policy-scoped budget state, owner, canonical pool, generation, +contract-version, and digest identities. +Actual measurements never become manifest capability truth, and a manifest +declaration never becomes evidence that capacity was configured or realized. + +The existing participant execution-service `capacity`, `reserved`, and +`in_flight` fields remain the service-local concurrency projection. When a +resource-budget state governs that concurrency, the service state references +the authoritative budget state and equality is validated. Two independently +mutable concurrency counters are forbidden. + +### 3. Scope is typed ownership plus an explicit aggregation graph + +Every budget and pool has one typed owner and stable owner reference. Initial +portable owner kinds are: + +- participant; +- deployment tenant/range; +- shared service; and +- fleet. + +Participant refs resolve to compiled participant addresses. Deployment-tenant +and shared-service refs reuse ADR-087 identities and exact +`uses_shared_service` bindings; budget ownership does not grant cross-tenant +access. Fleet refs are configuration-bound apparatus identities and are not +invented as SDL tenants or nodes. + +Aggregation is a finite acyclic parent relation over resolved owners and pools. +Every child names at most one parent for a resource dimension. A child limit +does not expand its parent, unused capacity is not borrowable unless the +configured policy says so, and the same usage event cannot be counted twice +through aliases or multiple parents. Compilation canonicalizes owner and pool +refs before duplicate and cycle checks. + +### 4. Resource dimensions are governed typed entries, not a generic quota map + +Each dimension declares: + +- a governed resource kind and unit; +- an accounting mode: windowed counter, cumulative counter, reservable gauge, + growth counter, or lease; +- a governed meter/profile reference and measurement basis; +- finite bounds and, where applicable, window/clock and burst; +- reservation, commit, release, exhaustion, and reset behavior; and +- evidence and limitation requirements. + +The initial kinds cover action rate, concurrent actions, storage growth, +inference token use, image generations, and accelerator allocation/time. +Inference tokens distinguish input, output, and billable accounting and name a +compatible tokenizer/meter profile; counts from incompatible profiles do not +aggregate. Image-generation and accelerator quantities likewise name a +portable meter/resource class rather than assuming every image or accelerator +unit has equal cost. + +The extension seam is a governed resource-kind plus meter/profile entry. +Adding a future network, CPU, memory, or evidence-ingest dimension extends that +catalog and the accounting-mode conformance table; it does not add another +budget root, free-form unit, provider schema, or scheduler. + +### 5. Admission is atomic across the complete scope and resource vector + +Structural and semantic admission resolve all owners, pools, parent relations, +meters, clocks, reset owners, and shared-service isolation bindings. +Compilation emits one canonical demand vector and aggregation graph. + +Planner admission compares the entire aggregated demand against: + +- manifest-declared support and guarantee strength; +- configuration-bound pool capacity; +- current pool ownership and tenant-isolation capability; +- the required fairness/priority policy; and +- exact action-to-target execution bindings. + +Admission fails before a plan is emitted if any dimension, ancestor scope, +isolation obligation, meter, reset behavior, or fairness guarantee is +unsupported. It is all-or-nothing: accepting concurrency while silently +dropping token or accelerator limits is invalid. + +Admission aggregates every policy limit by canonical pool identity and rejects +aliases or aggregate overcommit before runtime. Runtime admission reserves the +complete vector against one authoritative pool ledger before native work. +Completion commits only an exact, bounded native measurement vector whose +operation, generation, resource, unit, meter, and evidence match the +reservation; +failure, cancellation, timeout, stale generation, and teardown release or +reconcile it exactly once. Backend success cannot bypass serialized RAES +accounting, and RAES accounting cannot claim that a backend enforced an OS or +service limit without backend evidence. + +### 6. Fairness and priority are explicit obligations, not booleans + +Fairness policy is independent of participant role, evaluation authority, and +resource ownership. A `green` role is not automatically low priority, and +evaluation authority does not by itself grant unlimited capacity. + +Every contended shared pool declares a governed priority/fairness policy with: + +- explicit protected evaluated-participant capacity or latency obligation; +- priority classes and tie-breaking basis; +- weighted share or other bounded allocation rule; +- borrowing, lending, reclaim, and preemption posture; +- queue and starvation bounds or an explicit unsupported disclosure; and +- evidence needed to falsify the claim. + +Autonomous/background work may use only residual or explicitly lendable +capacity and must yield according to the admitted reclaim rule. A fairness, +latency, or starvation-free claim is invalid without bounded queue/deviation +evidence. Multi-resource fairness may use a governed weighted max-min/DRF-like +policy only when demand and capacity vectors are comparable under their meter +profiles; one scalar "cost" is not an acceptable substitute. + +### 7. Backend manifests separate support, configuration, and realization + +`backend-manifest-v2.capabilities.participant_runtime` remains the only +participant backend capability root. It is extended with: + +- declared supported owner scopes, resource kinds, accounting/reset modes, + fairness policies, isolation strengths, and evidence contract ids; +- configuration-bound logical pool capacities and a secret-free material + configuration digest; and +- explicit contract/evidence identities promised for measured realization. + +The existing `unsupported < disclosed_weak < bounded < exact` feature-support +scale remains the guarantee vocabulary. More `supports_*` booleans or unrelated +`max_*` fields are not added as the primary model. + +Actual utilization, throttle decisions, latency deviations, and reconciliation +events remain typed runtime/evidence carriers in `RuntimeSnapshot`, +participant histories, operation results, and conformance reports. They are +not mutable samples embedded in a capability manifest or prose in +`constraints`. + +### 8. Runtime accounting is append-only, generation-fenced, and reset-aware + +The runtime snapshot gets first-class typed budget-state, physical-pool-state, +and event surfaces. +Budget data must not be stored in `RuntimeSnapshot.metadata`, generic +`details`, audit text, or backend-private dictionaries. + +The accounting transition is reserve, admit/reject, commit/release, and +reconcile. Every transition carries canonical budget-state identity, owner, +pool, participant/episode +when applicable, execution generation, order point, resource quantity/meter, +disposition, predecessor, and evidence refs. Reservation and settlement are +idempotent under stable operation/action identity. + +Episode reset does not reset a tenant, shared-service, or fleet counter. +Reset behavior names its clock, generation owner, and accounting owner. +Persistent storage growth remains until evidence-backed reclamation; token and +image windows reset only at their declared boundary; accelerator leases are +released or marked unreconciled. Coordinated reset drains or reconciles +outstanding reservations before advancing generation and appends evidence +instead of deleting prior use. + +### 9. Reuse existing security, persistence, diagnostics, and observability gates + +- SDL and contract payloads remain closed (`SDLModel` / `ContractModel`) and + pass parser shape checks, semantic validation, instantiated-artifact + admission, compiler diagnostics, planner admission, runtime snapshot + validation, and conformance validation. +- Any control-plane mutation uses existing backend/operator authentication, + target authorization, request-size bounds, idempotency, request + fingerprints, atomic store transitions, and audit recording. Readback uses + existing read authorization and applies tenant/audience markings before + publication. +- Portable artifacts carry logical refs, units, counts, digests, markings, + and evidence only. They never carry credentials, bearer tokens, prompts, + generated images, model inputs/outputs, hidden truth, environment dumps, + host paths, device nodes, backend-native ids, or raw tracebacks. +- Backend capacity configuration enters through a typed, closed + configuration-bound manifest model. It is not accepted from unchecked + `**config`, environment-variable-only parsing, free-form manifest + `constraints`, or process argv. Secrets remain outside this capability. +- Public failures use existing `Diagnostic`, `ApplyResult`, operation status, + and redacted HTTP error envelopes. No resource-budget exception hierarchy is + introduced. Limit errors name safe logical ids, kinds, quantities, and + dispositions, never private payloads or backend objects. +- `ControlPlaneStore` and its atomic local-store pattern remain the durable + owner. Security audit records who attempted a control action; typed budget + events and experiment evidence record resource behavior. Logs or + OpenTelemetry may mirror those records but are not the evidence authority. +- Host enforcement is backend evidence. OCI/cgroup limits, accelerator device + claims, service-side inference quotas, filesystem quotas, and tenant + partitions must be validated against the selected backend's declared + isolation strength. Portable success never follows from a process flag, + environment value, device path, or provider response alone. + +### 10. Publication, lineage, compatibility, and traceability are end-to-end + +Contract models generate schemas through `schema_bundle()` and +`tools/generate_contract_schemas.py`; generated schemas are never edited by +hand. Publication entries, contract authority lists, controlled vocabularies, +concept bindings, fixtures, profiles, backend adapters, and schema +compatibility records advance together. + +If an SDL authoring field is added, module composition, symbol/reference +rewriting, source/instantiated schema parity, semantic admission, examples, +agent guidance, and the exact bidirectional SDL lineage ledger are updated in +the same change. External precedent is recorded as semantic influence, not +code/schema derivation or compatibility. + +Issue #899 has no formal Ground Control requirement. Do not invent one. +Traceability binds issue #899 to the ADR, normative clauses, implementation +surfaces, tests, conformance evidence, migration/release notes, and released +contract ids. Requirement reconciliation uses the repository's +requirement-free/orphan-link path. + +## Consequences + +### Positive + +- Existing participant, tenant, shared-service, manifest, scheduler, runtime, + persistence, diagnostic, and conformance authorities remain intact. +- Authored demand, configured capacity, current availability, measured use, + and evidence cannot silently collapse into one number. +- New resource dimensions and backend pool types have governed extension + seams without provider fields or parallel schedulers. +- Evaluated-participant protection and cross-range isolation become + falsifiable contract obligations. + +### Negative + +- The contract family and runtime accounting state machine are larger than + extending the existing `max_*` fields. +- Existing v1/v2 autonomous limit fields need canonical legacy projection and + migration guidance. +- Backends cannot make exact capacity, fairness, isolation, or enforcement + claims from manifest shape alone; they need configuration and measured + evidence. + +### Risks + +- Ambiguous meters can make token, image, accelerator, or storage quantities + appear comparable when they are not. +- A non-atomic multi-scope reservation can leak capacity or oversubscribe a + parent pool. +- Participant reset can incorrectly erase tenant/fleet use or release + still-active shared-service resources. +- High-cardinality participant/prompt/model attributes can leak information + and exhaust telemetry systems. +- Strict priority without bounded reclaim and starvation evidence can protect + evaluated work while making an unsupported fairness claim. + +## Rejected Alternatives + +- A second autonomous-activity scheduler or background-actor service. +- More unrelated optional `max_*` fields on SDL policies or backend manifests. +- A free-form `dict[str, number]` quota/capacity/usage map. +- Treating deployment cells as cloud projects, quota pools, or realized + isolation. +- Inferring priority from participant color, evaluated status, or source + order. +- One global fleet counter with no participant/range/shared-service ownership. +- Storing budget state in snapshot metadata, audit logs, telemetry labels, or + backend-private objects. +- Treating declared support, configured capacity, current availability, and + measured realization as interchangeable. +- Provider/model/device-specific portable fields or arbitrary policy + callbacks. +- Resetting aggregate usage on participant episode reset or deleting history + during reconciliation. + +## Non-Goals + +- Selecting a provider, inference product, model, tokenizer vendor, + accelerator vendor/profile, cloud project, cluster scheduler, or storage + implementation. +- Defining billing, currency, procurement, chargeback, or cost optimization. +- Proving model quality, human realism, throughput, latency, fairness, + isolation, or OS enforcement from a declaration alone. +- Replacing deployment tenancy, shared-time, participant lifecycle, + execution-binding, experiment apparatus, observability/evidence, or + realization-envelope semantics. +- Exposing prompts, completions, images, hidden evaluator state, credentials, + or backend-native resource identifiers. +- Making OpenTelemetry, Kubernetes, Kueue, OCI, or DRF a RAES wire format or + mandatory backend implementation. diff --git a/docs/explain/reference/backend-conformance.md b/docs/explain/reference/backend-conformance.md index 7908d6be7..9ecd42c35 100644 --- a/docs/explain/reference/backend-conformance.md +++ b/docs/explain/reference/backend-conformance.md @@ -177,6 +177,21 @@ experiment-selection `blake3-xof-v1` address/profile, silently drop occurrence provenance, or treat an apparatus stochastic-control declaration as scenario variation. Missing exact support fails planning. +The `participant-autonomous-execution/v3` profile additionally requires a +closed `resource_budgets` capability under the existing participant-runtime +root. Admission matches the complete compiled resource vector against +declared owner/resource/accounting/reset/fairness support and +configuration-bound pool entries. Owner, unit, meter, accounting mode, and +capacity must match exactly; declared cross-range pools require +`tenant_partitioned` isolation. Admission is atomic, so a backend cannot accept +action/concurrency bounds while dropping storage, token, image, accelerator, +ancestor, or fairness obligations. Runtime conformance then requires typed, +generation-fenced, policy-scoped budget state; one canonical cross-policy pool +allocation ledger; and append-only reserve, measured commit, release, throttle, +and reconcile events. Native commits require a complete matching measurement +vector and evidence. Manifest support and configured capacity do not count as +measured-realization evidence. + ## Gotchas And Anti-Patterns Avoid: diff --git a/docs/migration/README.md b/docs/migration/README.md index 89f9b35a4..e82b53126 100644 --- a/docs/migration/README.md +++ b/docs/migration/README.md @@ -13,6 +13,10 @@ The explicit opt-in from fixed-cadence autonomous participant execution to the governed activity profile is documented in [Autonomous Execution V2 Migration](autonomous-execution-v2.md). +Scoped participant, tenant, shared-service, and fleet resource governance is +an explicit v3 opt-in documented in +[Autonomous Execution V3 Migration](autonomous-execution-v3.md). + Backends that declare autonomous participant execution must adopt the portable execution-service surface described in [Participant Execution Control Migration](participant-execution-control.md). diff --git a/docs/migration/autonomous-execution-v3.md b/docs/migration/autonomous-execution-v3.md new file mode 100644 index 000000000..c87b0896a --- /dev/null +++ b/docs/migration/autonomous-execution-v3.md @@ -0,0 +1,109 @@ +# Autonomous Execution V3 Migration + +`participant-autonomous-execution/v3` adds scoped participant resource budgets +to the v2 activity profile. Existing v1 and v2 documents keep their meaning +and require no edits. Their `max_action_attempts` and `max_in_flight` values +compile into canonical legacy demand records, but they do not opt into shared +pool capacity, fairness, isolation, or measured resource accounting. + +## Opting In + +Start with a valid v2 policy, change its profile to v3, and add one complete +`resource_budget`: + +```yaml +autonomous_execution: + profile: participant-autonomous-execution/v3 + # All v2 fields remain required. + max_in_flight: 2 + resource_budget: + policy_id: green-shared-capacity + owners: + participant: + kind: participant + ref: participant-agent + range: + kind: deployment_tenant + ref: range-a + inference: + kind: shared_service + ref: nodes.inference.services.http + fleet: + kind: fleet + ref: fleet.primary + fairness: + policy: weighted_fair + priority_class: background + weight: 1 + protected: false + borrowing: lendable_only + reclaim: yield + max_queue_ticks: 20 + starvation_bound_ticks: 100 + dimensions: + participant-actions: + owner_ref: participant + pool_ref: participant-pool + resource_kind: action_rate + unit: actions + accounting_mode: windowed_counter + meter_profile_ref: raes.action-attempt/v1 + limit: 24 + reservation: 1 + reset: time_segment + window_ticks: 100 + concurrency: + owner_ref: participant + pool_ref: participant-pool + resource_kind: concurrent_actions + unit: actions + accounting_mode: reservable_gauge + meter_profile_ref: raes.concurrent-action/v1 + limit: 2 + reservation: 1 + reset: reconciled + # Also declare storage_growth, inference_tokens, image_generations, + # and accelerator entries with their governed units and meters. +``` + +The vector must contain all six initial resource kinds. The participant-owned +`concurrent_actions` limit must equal `max_in_flight`. Parent budgets may +aggregate the same kind, unit, accounting mode, and meter across participant, +tenant, shared-service, and fleet owners, but the graph must remain acyclic and +a child cannot exceed its parent. + +## Backend Changes + +A backend admitting v3 extends +`capabilities.participant_runtime.resource_budgets`. It declares support +strength and supported terms separately from `configured_pools`. Every demand +must match one configuration-bound pool entry by owner, pool, resource kind, +unit, accounting mode, and meter. The pool capacity must cover the authored +limit. Cross-range pools require `tenant_partitioned` isolation. + +The backend also declares +`participant-resource-budget-state-v1` and +`participant-resource-budget-event-v1` as realization contracts. Those +runtime carriers report reservations, measured use, throttling, and reset +reconciliation; they are not mutable utilization fields in the manifest. + +## Runtime and Consumer Changes + +Runtime snapshots add `participant_resource_budget_states`, +`participant_resource_pool_states`, and +`participant_resource_budget_events`. Budget states are keyed by canonical +policy-scoped state reference; pool states are keyed by exact physical-pool +identity and contain the cross-policy allocation ledger. Consumers that +deserialize the current closed snapshot schema must regenerate against the +updated `runtime-snapshot-v1` schema. Execution-service state now includes +`resource_budget_state_refs`; for v3 its concurrency projection is validated +against the referenced authoritative budget. + +Reservation is atomic across the full vector and occurs before native work. +The native result must return one measurement per reservation with matching +operation, generation, resource, unit, meter, and evidence; absent or +contradictory measurements release or roll back the reservation rather than +committing its estimate. Commit is generation-fenced and idempotent by action +identity. A shared-time +reset advances every state generation, clears only `time_segment` dimensions, +and preserves persistent storage and other independently owned counters. diff --git a/docs/research/lineage/source-audit-2026-07-12.md b/docs/research/lineage/source-audit-2026-07-12.md index 822b56967..bfee5eb5a 100644 --- a/docs/research/lineage/source-audit-2026-07-12.md +++ b/docs/research/lineage/source-audit-2026-07-12.md @@ -309,6 +309,51 @@ Bounded concurrent native participant execution, coordinated drain/reset, and action-to-target evidence remain RAES-defined obligations under ADR-092, ADR-054/RUN-308, ADR-091, and issue #898. +### Issue 899 scoped resource-governance review + +The participant execution and deployment-tenancy sources were re-reviewed on +2026-07-27 for issue #899. The following primary sources add design criteria: + +- Kubernetes, *Resource Quotas*, + , separates + namespace-scoped aggregate limits from individual workload declarations. +- Kubernetes, *API Priority and Fairness*, + , + separates classification, priority, queueing, and concurrency allocation. +- Kueue, *Cluster Queue*, + , distinguishes + nominal quota, cohorts, borrowing/lending, and priority. +- Ghodsi et al., *Dominant Resource Fairness: Fair Allocation of Multiple + Resource Types* (NSDI 2011), + , + establishes a multi-resource fairness precedent rather than a single + scalar-cost reduction. +- Open Container Initiative, *Linux Container Configuration*, + , + separates logical container configuration from cgroup and device + enforcement. +- Kubernetes, *Dynamic Resource Allocation*, + , + separates workload demand, device classes, claims, allocation, and driver + realization. +- OpenTelemetry, *Metrics semantic conventions* and *Generative AI metrics*, + and + , + require explicit metric identity, units, and bounded attribute sets and + provide token-usage observation precedent. + +RAES adapts only the separation of authored demand, configured capacity, +multi-resource fairness obligations, logical isolation claims, and measured +realization. The v3 SDL shape, owner graph, resource kinds and units, meter +profiles, exact atomic admission, generation-fenced accounting, reset rules, +manifest carriers, runtime events, and evidence obligations are RAES-defined +under ADR-097. RAES does not adopt Kubernetes, Kueue, OCI, or OpenTelemetry +syntax, APIs, object identity, scheduler algorithms, device models, cgroup +configuration, telemetry wire formats, or compatibility. Dominant Resource +Fairness is precedent for keeping vectors comparable; v3 does not claim to +implement the paper's allocator. No source code or schema was copied, and no +additional license notice is required. + ## CRACK Publications Two related works by Russo, Costa, and Armando are distinct and must not share diff --git a/implementations/python/packages/raes/_mapping_scopes.py b/implementations/python/packages/raes/_mapping_scopes.py index 09fae7329..10bb10cfa 100644 --- a/implementations/python/packages/raes/_mapping_scopes.py +++ b/implementations/python/packages/raes/_mapping_scopes.py @@ -77,6 +77,8 @@ class MappingScope(str, Enum): "fixed_positions", "tool_affordances", "participant_inject_deliveries", + "owners", + "dimensions", } ) diff --git a/implementations/python/packages/raes/composition.py b/implementations/python/packages/raes/composition.py index a9f667e23..b6ca3a8fe 100644 --- a/implementations/python/packages/raes/composition.py +++ b/implementations/python/packages/raes/composition.py @@ -105,6 +105,34 @@ def _rewrite_node_or_service_ref(name: str, node_map: Mapping[str, str]) -> str: return _rewrite_section_ref(name, "nodes", node_map) +def _rewrite_participant_resource_budget( + payload: object, + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + """Rewrite kind-specific owner references in an autonomous budget policy.""" + + if not isinstance(payload, dict): + return + owners = payload.get("owners") + if not isinstance(owners, dict): + return + for owner in owners.values(): + if not isinstance(owner, dict) or not owner.get("ref"): + continue + reference = str(owner["ref"]) + owner_kind = owner.get("kind") + if owner_kind == "participant": + owner["ref"] = _rewrite_section_ref(reference, "agents", symbols["agents"]) + elif owner_kind == "deployment_tenant": + owner["ref"] = _rewrite_section_ref( + reference, + "deployment_tenants", + symbols["deployment_tenants"], + ) + elif owner_kind == "shared_service": + owner["ref"] = _rewrite_node_or_service_ref(reference, symbols["nodes"]) + + def _rewrite_stateful_dependency_ref( reference: str, symbols: dict[str, dict[str, str] | set[str]], @@ -820,6 +848,12 @@ def _namespace_payload( 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): diff --git a/implementations/python/packages/raes/participant_execution.py b/implementations/python/packages/raes/participant_execution.py index b0b0ea052..f2693ce21 100644 --- a/implementations/python/packages/raes/participant_execution.py +++ b/implementations/python/packages/raes/participant_execution.py @@ -10,6 +10,7 @@ from ._base import SDLModel from ._identifiers import PortableIdentifier from .participant_action_semantics import ParticipantFailureClass +from .participant_resource_budgets import ParticipantResourceBudgetPolicy class ParticipantExecutionFailurePolicy(str, Enum): @@ -200,7 +201,34 @@ def visit(candidate_id: str) -> None: return self -ParticipantAutonomousExecutionPolicy = ParticipantAutonomousExecutionPolicyV1 | ParticipantAutonomousExecutionPolicyV2 +class ParticipantAutonomousExecutionPolicyV3(ParticipantAutonomousExecutionPolicyV2): + """V2 activity plus scoped multi-resource governance.""" + + profile: Literal["participant-autonomous-execution/v3"] + resource_budget: ParticipantResourceBudgetPolicy + + @model_validator(mode="after") + def _validate_concurrency_projection(self) -> ParticipantAutonomousExecutionPolicyV3: + participant_concurrency = [ + dimension + for dimension in self.resource_budget.dimensions.values() + if dimension.resource_kind.value == "concurrent_actions" + and self.resource_budget.owners[dimension.owner_ref].kind.value == "participant" + ] + if len(participant_concurrency) != 1: + raise ValueError( + "participant-autonomous-execution/v3 requires exactly one participant-owned concurrent_actions budget" + ) + if participant_concurrency[0].limit != self.max_in_flight: + raise ValueError("participant concurrent_actions budget limit must equal max_in_flight") + return self + + +ParticipantAutonomousExecutionPolicy = ( + ParticipantAutonomousExecutionPolicyV1 + | ParticipantAutonomousExecutionPolicyV2 + | ParticipantAutonomousExecutionPolicyV3 +) __all__ = [ @@ -209,6 +237,7 @@ def visit(candidate_id: str) -> None: "ParticipantAutonomousExecutionPolicy", "ParticipantAutonomousExecutionPolicyV1", "ParticipantAutonomousExecutionPolicyV2", + "ParticipantAutonomousExecutionPolicyV3", "ParticipantEvaluationAuthority", "ParticipantEvaluationAuthorityMode", "ParticipantExecutionFailurePolicy", diff --git a/implementations/python/packages/raes/participant_resource_budgets.py b/implementations/python/packages/raes/participant_resource_budgets.py new file mode 100644 index 000000000..d40a17087 --- /dev/null +++ b/implementations/python/packages/raes/participant_resource_budgets.py @@ -0,0 +1,227 @@ +"""Authored participant resource-budget and fairness policy.""" + +from __future__ import annotations + +from enum import Enum + +from pydantic import Field, field_validator, model_validator + +from ._base import SDLModel +from ._identifiers import PortableIdentifier + + +class ParticipantResourceOwnerKind(str, Enum): + PARTICIPANT = "participant" + DEPLOYMENT_TENANT = "deployment_tenant" + SHARED_SERVICE = "shared_service" + FLEET = "fleet" + + +class ParticipantResourceKind(str, Enum): + ACTION_RATE = "action_rate" + CONCURRENT_ACTIONS = "concurrent_actions" + STORAGE_GROWTH = "storage_growth" + INFERENCE_TOKENS = "inference_tokens" + IMAGE_GENERATIONS = "image_generations" + ACCELERATOR = "accelerator" + + +class ParticipantResourceAccountingMode(str, Enum): + WINDOWED_COUNTER = "windowed_counter" + CUMULATIVE_COUNTER = "cumulative_counter" + RESERVABLE_GAUGE = "reservable_gauge" + GROWTH_COUNTER = "growth_counter" + LEASE = "lease" + + +class ParticipantResourceResetMode(str, Enum): + EPISODE = "episode" + TIME_SEGMENT = "time_segment" + RUN = "run" + RECONCILED = "reconciled" + + +class ParticipantResourceOwner(SDLModel): + kind: ParticipantResourceOwnerKind + ref: str = Field(min_length=1) + + +class ParticipantResourceFairness(SDLModel): + policy: str = Field(min_length=1) + priority_class: str = Field(min_length=1) + weight: int = Field(ge=1, le=1_000_000) + protected: bool + borrowing: str = Field(min_length=1) + reclaim: str = Field(min_length=1) + max_queue_ticks: int = Field(ge=0, le=1_000_000_000) + starvation_bound_ticks: int = Field(ge=1, le=1_000_000_000) + + +_RESOURCE_UNITS = { + ParticipantResourceKind.ACTION_RATE: "actions", + ParticipantResourceKind.CONCURRENT_ACTIONS: "actions", + ParticipantResourceKind.STORAGE_GROWTH: "bytes", + ParticipantResourceKind.INFERENCE_TOKENS: "tokens", + ParticipantResourceKind.IMAGE_GENERATIONS: "images", + ParticipantResourceKind.ACCELERATOR: "accelerator_milliseconds", +} + +_RESOURCE_ACCOUNTING = { + ParticipantResourceKind.ACTION_RATE: {ParticipantResourceAccountingMode.WINDOWED_COUNTER}, + ParticipantResourceKind.CONCURRENT_ACTIONS: {ParticipantResourceAccountingMode.RESERVABLE_GAUGE}, + ParticipantResourceKind.STORAGE_GROWTH: {ParticipantResourceAccountingMode.GROWTH_COUNTER}, + ParticipantResourceKind.INFERENCE_TOKENS: { + ParticipantResourceAccountingMode.WINDOWED_COUNTER, + ParticipantResourceAccountingMode.CUMULATIVE_COUNTER, + }, + ParticipantResourceKind.IMAGE_GENERATIONS: { + ParticipantResourceAccountingMode.WINDOWED_COUNTER, + ParticipantResourceAccountingMode.CUMULATIVE_COUNTER, + }, + ParticipantResourceKind.ACCELERATOR: {ParticipantResourceAccountingMode.LEASE}, +} + + +class ParticipantResourceBudgetDimension(SDLModel): + owner_ref: PortableIdentifier + pool_ref: str = Field(min_length=1) + resource_kind: ParticipantResourceKind + unit: str = Field(min_length=1) + accounting_mode: ParticipantResourceAccountingMode + meter_profile_ref: str = Field(min_length=1) + limit: int = Field(ge=1, le=10**18) + reservation: int = Field(ge=1, le=10**18) + reset: ParticipantResourceResetMode + window_ticks: int | None = Field(default=None, ge=1, le=1_000_000_000) + parent_budget_ref: PortableIdentifier | None = None + evidence_refs: list[str] = Field(default_factory=list, max_length=1024) + + @field_validator("evidence_refs") + @classmethod + def _unique_evidence_refs(cls, values: list[str]) -> list[str]: + if any(not value.strip() for value in values): + raise ValueError("resource-budget evidence refs must be non-empty") + if len(values) != len(set(values)): + raise ValueError("resource-budget evidence refs must be unique") + return values + + @model_validator(mode="after") + def _validate_dimension(self) -> ParticipantResourceBudgetDimension: + expected_unit = _RESOURCE_UNITS[self.resource_kind] + if self.unit != expected_unit: + raise ValueError(f"{self.resource_kind.value} resource budget requires unit {expected_unit!r}") + if self.accounting_mode not in _RESOURCE_ACCOUNTING[self.resource_kind]: + raise ValueError( + f"{self.resource_kind.value} resource budget does not support accounting mode " + f"{self.accounting_mode.value!r}" + ) + if self.reservation > self.limit: + raise ValueError("resource-budget reservation cannot exceed limit") + windowed = self.accounting_mode == ParticipantResourceAccountingMode.WINDOWED_COUNTER + if windowed != (self.window_ticks is not None): + raise ValueError("windowed resource budgets require window_ticks and other modes forbid it") + if self.resource_kind == ParticipantResourceKind.STORAGE_GROWTH and ( + self.reset != ParticipantResourceResetMode.RECONCILED + ): + raise ValueError("storage_growth resource budget requires reconciled reset") + return self + + +class ParticipantResourceBudgetPolicy(SDLModel): + policy_id: PortableIdentifier + owners: dict[PortableIdentifier, ParticipantResourceOwner] = Field(min_length=1, max_length=1024) + fairness: ParticipantResourceFairness + dimensions: dict[PortableIdentifier, ParticipantResourceBudgetDimension] = Field( + min_length=1, + max_length=4096, + ) + + @model_validator(mode="after") + def _validate_policy(self) -> ParticipantResourceBudgetPolicy: + required_kinds = set(ParticipantResourceKind) + actual_kinds = {dimension.resource_kind for dimension in self.dimensions.values()} + missing = sorted(kind.value for kind in required_kinds - actual_kinds) + if missing: + raise ValueError("resource budget requires complete resource vector: " + ", ".join(missing)) + for budget_id, dimension in self.dimensions.items(): + if dimension.owner_ref not in self.owners: + raise ValueError(f"resource budget {budget_id!r} has unknown owner_ref") + if dimension.parent_budget_ref is not None and dimension.parent_budget_ref not in self.dimensions: + raise ValueError(f"resource budget {budget_id!r} has unknown parent_budget_ref") + owner = self.owners[dimension.owner_ref] + if owner.kind != ParticipantResourceOwnerKind.PARTICIPANT and ( + dimension.reset == ParticipantResourceResetMode.EPISODE + ): + raise ValueError("only participant-owned resource budgets may reset with an episode") + self._validate_parent_graph() + pool_keys = [ + ( + dimension.pool_ref, + self.owners[dimension.owner_ref].kind, + self.owners[dimension.owner_ref].ref, + dimension.resource_kind, + dimension.unit, + dimension.accounting_mode, + dimension.meter_profile_ref, + ) + for dimension in self.dimensions.values() + ] + if len(pool_keys) != len(set(pool_keys)): + raise ValueError("resource-budget dimensions cannot alias the same canonical resource pool") + return self + + def _validate_parent_graph(self) -> None: + visiting: set[str] = set() + visited: set[str] = set() + + def visit(budget_id: str) -> None: + if budget_id in visiting: + raise ValueError("resource-budget parent aggregation graph must be acyclic") + if budget_id in visited: + return + visiting.add(budget_id) + dimension = self.dimensions[budget_id] + parent_ref = dimension.parent_budget_ref + if parent_ref is not None: + parent = self.dimensions[parent_ref] + comparable = ( + dimension.resource_kind, + dimension.unit, + dimension.accounting_mode, + dimension.meter_profile_ref, + ) + parent_comparable = ( + parent.resource_kind, + parent.unit, + parent.accounting_mode, + parent.meter_profile_ref, + ) + if comparable != parent_comparable: + raise ValueError("resource-budget parent must use the same resource, unit, mode, and meter") + if dimension.limit > parent.limit: + raise ValueError("resource-budget child limit cannot exceed its parent") + visit(str(parent_ref)) + visiting.remove(budget_id) + visited.add(budget_id) + + for budget_id in self.dimensions: + visit(str(budget_id)) + children_by_parent: dict[str, list[ParticipantResourceBudgetDimension]] = {} + for dimension in self.dimensions.values(): + if dimension.parent_budget_ref is not None: + children_by_parent.setdefault(str(dimension.parent_budget_ref), []).append(dimension) + for parent_id, children in children_by_parent.items(): + if sum(child.limit for child in children) > self.dimensions[parent_id].limit: + raise ValueError("resource-budget sibling limits cannot exceed their parent limit") + + +__all__ = [ + "ParticipantResourceAccountingMode", + "ParticipantResourceBudgetDimension", + "ParticipantResourceBudgetPolicy", + "ParticipantResourceFairness", + "ParticipantResourceKind", + "ParticipantResourceOwner", + "ParticipantResourceOwnerKind", + "ParticipantResourceResetMode", +] diff --git a/implementations/python/packages/raes/semantics/participant_behavior.py b/implementations/python/packages/raes/semantics/participant_behavior.py index 3c8e4f8ea..76ae04795 100644 --- a/implementations/python/packages/raes/semantics/participant_behavior.py +++ b/implementations/python/packages/raes/semantics/participant_behavior.py @@ -566,7 +566,10 @@ def _autonomous_constraint_issues( cadence = None cadence_count = 0 profile = getattr(context.policy, "profile", "participant-autonomous-execution/v1") - activity_policy = profile == "participant-autonomous-execution/v2" + activity_policy = profile in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + } constraint_refs = ( [*context.policy.work_window_refs, *context.policy.pause_window_refs] if activity_policy @@ -678,9 +681,10 @@ def _autonomous_stepped_cadence_issues( if progression_mode != "stepped": return [] step_ticks = getattr(bindings.progression, "step_ticks", None) - if getattr(context.policy, "profile", "participant-autonomous-execution/v1") == ( - "participant-autonomous-execution/v2" - ): + if getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + }: minimum_ticks = context.policy.timing.minimum_ticks maximum_ticks = context.policy.timing.maximum_ticks if isinstance(step_ticks, int) and not minimum_ticks % step_ticks and not maximum_ticks % step_ticks: diff --git a/implementations/python/packages/raes/validator/_content_objectives.py b/implementations/python/packages/raes/validator/_content_objectives.py index 61ffff261..f9a40de89 100644 --- a/implementations/python/packages/raes/validator/_content_objectives.py +++ b/implementations/python/packages/raes/validator/_content_objectives.py @@ -22,6 +22,7 @@ analyze_participant_outcome_interpretations, ) from ._participant_execution_renderers import AUTONOMOUS_PARTICIPANT_ISSUE_RENDERERS +from ._participant_resource_budget_owners import participant_resource_budget_owner_errors # Renders an objective-semantics issue (machine-readable code from # ``raes.semantics.objective_semantics``) into the authoring-error string @@ -381,6 +382,15 @@ def _verify_participant_behavior(self) -> None: ) for issue in analysis.issues: self._err(self._format_participant_behavior_issue(issue)) + for error in participant_resource_budget_owner_errors( + self._s.behavior_specifications, + self._s.action_contracts, + self._s.deployment_tenants, + self._s.deployment_cells, + self._s.relationships, + self._split_node_service_ref, + ): + self._err(error) self._verify_tool_affordance_tool_refs() self._verify_participant_inject_deliveries() self._verify_participant_interaction_refs() diff --git a/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py b/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py new file mode 100644 index 000000000..36c535f90 --- /dev/null +++ b/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py @@ -0,0 +1,83 @@ +"""Semantic validation for authored participant resource-budget owners.""" + +from collections.abc import Callable, Mapping + + +def participant_resource_budget_owner_errors( + behavior_specifications: Mapping[str, object], + action_contracts: Mapping[str, object], + deployment_tenants: Mapping[str, object], + deployment_cells: Mapping[str, object], + relationships: Mapping[str, object], + split_node_service_ref: Callable[[str], object | None], +) -> tuple[str, ...]: + """Return errors for resource owners outside their declared SDL scope.""" + + errors: list[str] = [] + for spec_name, behavior_spec in behavior_specifications.items(): + policy = getattr(behavior_spec, "autonomous_execution", None) + budget = getattr(policy, "resource_budget", None) + if budget is None: + continue + participant_refs = {str(ref).removeprefix("agents.") for ref in getattr(behavior_spec, "participant_refs", ())} + action_refs = tuple(getattr(policy, "action_order", ())) or tuple( + getattr(candidate, "action_ref", "") for candidate in getattr(policy, "action_candidates", {}).values() + ) + action_targets = { + str(target) + for action_ref in action_refs + for effect in getattr( + action_contracts.get(str(action_ref).removeprefix("action_contracts.")), "effects", () + ) + for target in getattr(effect, "target_refs", ()) + } + target_nodes = { + str(target).removeprefix("nodes.").split(".services.", 1)[0] + for target in action_targets + if str(target).startswith("nodes.") + } + target_tenants = { + str(getattr(cell, "tenant_ref", "")).removeprefix("deployment_tenants.") + for cell in deployment_cells.values() + if target_nodes & {str(node_ref).removeprefix("nodes.") for node_ref in getattr(cell, "node_refs", ())} + } + declared_tenants = { + str(owner.ref).removeprefix("deployment_tenants.") + for owner in budget.owners.values() + if getattr(owner.kind, "value", owner.kind) == "deployment_tenant" + } + shared_permissions = { + ( + str(getattr(relationship, "source", "")).removeprefix("deployment_tenants."), + str(getattr(relationship, "target", "")), + ) + for relationship in relationships.values() + if getattr(getattr(relationship, "type", ""), "value", getattr(relationship, "type", "")) + == "uses_shared_service" + } + target_tenants.update(tenant for tenant, target in shared_permissions if target in action_targets) + for owner_id, owner in budget.owners.items(): + kind = getattr(owner.kind, "value", owner.kind) + ref = str(owner.ref) + label = f"Behavior specification '{spec_name}' resource-budget owner '{owner_id}'" + if kind == "participant" and ref.removeprefix("agents.") not in participant_refs: + errors.append(f"{label} participant ref '{ref}' is outside the policy participant scope") + elif kind == "deployment_tenant": + tenant_ref = ref.removeprefix("deployment_tenants.") + if tenant_ref not in deployment_tenants: + errors.append(f"{label} deployment tenant ref '{ref}' is undefined") + elif tenant_ref not in target_tenants: + errors.append(f"{label} deployment tenant ref '{ref}' does not own an authorized action target") + elif kind == "shared_service": + if split_node_service_ref(ref) is None: + errors.append(f"{label} shared service ref '{ref}' is undefined") + elif ref not in action_targets: + errors.append(f"{label} shared service ref '{ref}' is not an exact execution target") + elif not any((tenant, ref) in shared_permissions for tenant in declared_tenants): + errors.append( + f"{label} shared service ref '{ref}' lacks an authorized tenant uses_shared_service edge" + ) + return tuple(errors) + + +__all__ = ["participant_resource_budget_owner_errors"] diff --git a/implementations/python/packages/raes_backend_protocols/capability_admission.py b/implementations/python/packages/raes_backend_protocols/capability_admission.py index 32e11c867..ce1f99b61 100644 --- a/implementations/python/packages/raes_backend_protocols/capability_admission.py +++ b/implementations/python/packages/raes_backend_protocols/capability_admission.py @@ -16,6 +16,10 @@ from .participant_feature_admission import ( resolve_participant_feature_support as resolve_participant_feature_support, ) +from .participant_resource_admission import ( + ResourceGovernedPolicy, + participant_resource_budget_gaps, +) if TYPE_CHECKING: from raes_contracts.contracts.time_model import TimeModelDeclarationModel @@ -25,7 +29,7 @@ from .capabilities import ParticipantRuntimeCapabilities, TimeCapabilities -class AutonomousExecutionPolicy(Protocol): +class AutonomousExecutionPolicy(ResourceGovernedPolicy, Protocol): profile: str participant_addresses: tuple[str, ...] action_contract_addresses: tuple[str, ...] @@ -156,7 +160,10 @@ def _unsupported_autonomous_value_gaps( for label, required, supported in requirements if (unsupported := sorted(required - supported)) ] - if any(policy.profile == "participant-autonomous-execution/v2" for policy in policies): + if any( + policy.profile in {"participant-autonomous-execution/v2", "participant-autonomous-execution/v3"} + for policy in policies + ): missing_features = sorted(_V2_ACTIVITY_FEATURES - capability.supported_autonomous_activity_features) if missing_features: gaps.append(f"unsupported autonomous activity features: {', '.join(missing_features)}") @@ -245,6 +252,7 @@ def participant_autonomous_execution_capability_gaps( normalized_policies, ) ) + gaps.extend(participant_resource_budget_gaps(manifest, capability, normalized_policies)) gaps.extend(_autonomous_reset_gaps(manifest, normalized_policies, time_model)) return tuple(gaps) diff --git a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py index 99196a11f..2ff818160 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -13,6 +13,8 @@ ) from raes_contracts.vocabulary import ParticipantFeatureSupportLevel +from .participant_resource_budgets import ParticipantResourceBudgetCapabilities + PARTICIPANT_EXECUTION_CONTROL_ACTIONS = frozenset({"start", "pause", "resume", "drain", "reset", "teardown"}) @@ -175,6 +177,7 @@ class ParticipantRuntimeCapabilities: supports_bounded_concurrency: bool = False max_execution_services: int | None = None max_concurrent_actions: int | None = None + resource_budgets: ParticipantResourceBudgetCapabilities | None = None constraints: dict[str, str] = field(default_factory=dict) def __post_init__(self) -> None: @@ -271,11 +274,19 @@ def _validate_enabled_autonomous_execution(self) -> None: raise ValueError("autonomous execution requires exact supported observation boundaries") if not self.supported_autonomous_policy_profiles: raise ValueError("autonomous execution requires exact supported policy profiles") - if "participant-autonomous-execution/v2" in self.supported_autonomous_policy_profiles: + if { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + }.intersection(self.supported_autonomous_policy_profiles): if not self.supported_autonomous_activity_features: - raise ValueError("autonomous execution v2 requires exact supported activity features") + raise ValueError("autonomous execution v2/v3 requires exact supported activity features") if not self.supported_autonomous_random_stream_profiles: - raise ValueError("autonomous execution v2 requires exact supported random-stream profiles") + raise ValueError("autonomous execution v2/v3 requires exact supported random-stream profiles") + if ( + "participant-autonomous-execution/v3" in self.supported_autonomous_policy_profiles + and self.resource_budgets is None + ): + raise ValueError("autonomous execution v3 requires participant resource-budget capabilities") self._validate_autonomous_addresses() self._validate_execution_control() for label, value in self._autonomous_limits(): @@ -355,6 +366,7 @@ def _has_autonomous_configuration(self) -> bool: or self.supports_bounded_concurrency or self.max_execution_services is not None or self.max_concurrent_actions is not None + or self.resource_budgets is not None or any(value is not None for _, value in self._autonomous_limits()) ) diff --git a/implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py b/implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py index 0420a3ac6..d62432752 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py +++ b/implementations/python/packages/raes_backend_protocols/participant_execution_manifest.py @@ -3,6 +3,10 @@ from raes_contracts.contracts.participant_execution import ParticipantExecutionBindingModel from .participant_capabilities import ParticipantExecutionBinding, ParticipantRuntimeCapabilities +from .participant_resource_budgets import ( + participant_resource_budget_capability_from_model, + participant_resource_budget_capability_payload, +) def participant_execution_capability_payload( @@ -31,6 +35,11 @@ def participant_execution_capability_payload( "supports_bounded_concurrency": capability.supports_bounded_concurrency, "max_execution_services": capability.max_execution_services, "max_concurrent_actions": capability.max_concurrent_actions, + "resource_budgets": ( + participant_resource_budget_capability_payload(capability.resource_budgets) + if capability.resource_budgets is not None + else None + ), } @@ -58,6 +67,11 @@ def participant_execution_capability_kwargs(model: object) -> dict[str, object]: "supports_bounded_concurrency": model.supports_bounded_concurrency, "max_execution_services": model.max_execution_services, "max_concurrent_actions": model.max_concurrent_actions, + "resource_budgets": ( + participant_resource_budget_capability_from_model(model.resource_budgets) + if model.resource_budgets is not None + else None + ), } diff --git a/implementations/python/packages/raes_backend_protocols/participant_resource_admission.py b/implementations/python/packages/raes_backend_protocols/participant_resource_admission.py new file mode 100644 index 000000000..fe48b18b3 --- /dev/null +++ b/implementations/python/packages/raes_backend_protocols/participant_resource_admission.py @@ -0,0 +1,200 @@ +"""Admission checks for participant resource-budget vectors.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol + +if TYPE_CHECKING: + from .backend_manifest import BackendManifest + from .capabilities import ParticipantRuntimeCapabilities + + +class ResourceDemand(Protocol): + budget_id: str + owner_kind: str + owner_address: str + pool_ref: str + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + limit: int + reservation: int + reset: str + parent_budget_ref: str | None + + +class ResourceFairness(Protocol): + policy: str + priority_class: str + protected: bool + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + + +class ResourceGovernedPolicy(Protocol): + address: str + profile: str + resource_demands: tuple[ResourceDemand, ...] + resource_fairness: ResourceFairness + + +def participant_resource_budget_gaps( + manifest: BackendManifest, + capability: ParticipantRuntimeCapabilities, + policies: tuple[ResourceGovernedPolicy, ...], +) -> list[str]: + """Return atomic capacity, accounting, isolation, and fairness gaps.""" + + governed = tuple(policy for policy in policies if policy.profile == "participant-autonomous-execution/v3") + if not governed: + return [] + budgets = capability.resource_budgets + if budgets is None: + return ["participant-autonomous-execution/v3 requires resource-budget capabilities"] + gaps: list[str] = [] + if budgets.support_strength not in {"bounded", "exact"}: + gaps.append( + "participant resource budgets require bounded or exact support; " + f"backend declares {budgets.support_strength}" + ) + required_manifest_contracts = { + "participant-resource-budget-policy-v1", + "participant-resource-pool-capacity-v1", + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", + } + missing_manifest_contracts = sorted(required_manifest_contracts - manifest.supported_contract_versions) + if missing_manifest_contracts: + gaps.append("participant resource budgets missing manifest contracts: " + ", ".join(missing_manifest_contracts)) + required_contracts = { + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", + } + missing_contracts = sorted(required_contracts - budgets.realization_contract_ids) + if missing_contracts: + gaps.append("participant resource budgets missing realization contracts: " + ", ".join(missing_contracts)) + aggregate_limits: dict[tuple[str, ...], int] = {} + aggregate_protected_limits: dict[tuple[str, ...], int] = {} + pools_by_key: dict[tuple[str, ...], object] = {} + for policy in governed: + fairness = policy.resource_fairness + if fairness.policy not in budgets.supported_fairness_policies: + gaps.append(f"unsupported participant resource fairness policy: {fairness.policy}") + policy_pool_keys: set[tuple[str, ...]] = set() + demands_by_id = {demand.budget_id: demand for demand in policy.resource_demands} + children_by_parent: dict[str, list[ResourceDemand]] = {} + for demand in policy.resource_demands: + if demand.parent_budget_ref is not None: + children_by_parent.setdefault(demand.parent_budget_ref, []).append(demand) + for parent_id, children in children_by_parent.items(): + parent = demands_by_id.get(parent_id) + if parent is None or sum(child.limit for child in children) > parent.limit: + gaps.append( + f"participant resource budget parent {parent_id} is missing or overcommitted by child limits" + ) + for demand in policy.resource_demands: + unsupported: list[str] = [] + if demand.owner_kind not in budgets.supported_owner_kinds: + unsupported.append(f"owner kind {demand.owner_kind}") + if demand.resource_kind not in budgets.supported_resource_kinds: + unsupported.append(f"resource kind {demand.resource_kind}") + if demand.accounting_mode not in budgets.supported_accounting_modes: + unsupported.append(f"accounting mode {demand.accounting_mode}") + if demand.reset not in budgets.supported_reset_modes: + unsupported.append(f"reset mode {demand.reset}") + if unsupported: + gaps.append(f"participant resource budget {demand.budget_id} unsupported: " + ", ".join(unsupported)) + continue + exact = tuple( + pool + for pool in budgets.configured_pools + if pool.pool_ref == demand.pool_ref + and pool.owner_kind == demand.owner_kind + and pool.owner_ref == demand.owner_address + and pool.resource_kind == demand.resource_kind + and pool.unit == demand.unit + and pool.accounting_mode == demand.accounting_mode + and pool.meter_profile_ref == demand.meter_profile_ref + ) + if not exact: + gaps.append( + "participant resource budget " + f"{demand.budget_id} ({demand.resource_kind}) has no exact configured " + "owner/unit/accounting/meter capacity" + ) + continue + pool = exact[0] + pool_key = ( + pool.pool_ref, + pool.owner_kind, + pool.owner_ref, + pool.resource_kind, + pool.unit, + pool.accounting_mode, + pool.meter_profile_ref, + ) + if pool_key in policy_pool_keys: + gaps.append(f"participant policy {policy.address} aliases canonical resource pool {pool.pool_ref}") + continue + policy_pool_keys.add(pool_key) + pools_by_key[pool_key] = pool + aggregate_limits[pool_key] = aggregate_limits.get(pool_key, 0) + demand.limit + if fairness.protected: + aggregate_protected_limits[pool_key] = aggregate_protected_limits.get(pool_key, 0) + demand.limit + if pool.capacity < demand.limit: + gaps.append( + "participant resource budget " + f"{demand.budget_id} ({demand.resource_kind}) requires capacity " + f"{demand.limit}; configured capacity is {pool.capacity}" + ) + if pool.fairness_policy != fairness.policy: + gaps.append( + "participant resource budget " + f"{demand.budget_id} ({demand.resource_kind}) requires fairness " + f"{fairness.policy}; configured pool declares {pool.fairness_policy}" + ) + if fairness.priority_class not in pool.priority_classes: + gaps.append( + "participant resource budget " + f"{demand.budget_id} ({demand.resource_kind}) requires priority class " + f"{fairness.priority_class}" + ) + if pool.borrowing != fairness.borrowing or pool.reclaim != fairness.reclaim: + gaps.append( + "participant resource budget " + f"{demand.budget_id} ({demand.resource_kind}) fairness borrowing/reclaim " + "does not match configured pool" + ) + if ( + pool.max_queue_ticks > fairness.max_queue_ticks + or pool.starvation_bound_ticks > fairness.starvation_bound_ticks + ): + gaps.append( + "participant resource budget " + f"{demand.budget_id} ({demand.resource_kind}) configured fairness " + "queue/starvation bounds are weaker than required" + ) + if fairness.protected and pool.protected_capacity < demand.reservation: + gaps.append( + f"participant resource budget {demand.budget_id} ({demand.resource_kind}) lacks protected capacity" + ) + for pool_key, required in aggregate_limits.items(): + pool = pools_by_key[pool_key] + if required > pool.capacity: + gaps.append( + f"participant resource pool {pool.pool_ref} aggregate policy limits require " + f"{required}; configured capacity is {pool.capacity}" + ) + protected = aggregate_protected_limits.get(pool_key, 0) + if protected > pool.protected_capacity: + gaps.append( + f"participant resource pool {pool.pool_ref} protected policy limits require " + f"{protected}; configured protected capacity is {pool.protected_capacity}" + ) + return gaps + + +__all__ = ["participant_resource_budget_gaps"] diff --git a/implementations/python/packages/raes_backend_protocols/participant_resource_budgets.py b/implementations/python/packages/raes_backend_protocols/participant_resource_budgets.py new file mode 100644 index 000000000..126e91cfc --- /dev/null +++ b/implementations/python/packages/raes_backend_protocols/participant_resource_budgets.py @@ -0,0 +1,131 @@ +"""Backend participant resource-budget capability declarations.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceBudgetCapabilitiesModel, + ParticipantResourcePoolCapacityModel, +) + + +@dataclass(frozen=True) +class ParticipantResourcePoolCapacity: + pool_ref: str + owner_kind: str + owner_ref: str + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + capacity: int + tenant_isolation: str + configuration_digest: str + fairness_policy: str + priority_classes: tuple[str, ...] + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + protected_capacity: int + evidence_contract_ids: tuple[str, ...] + + def __post_init__(self) -> None: + ParticipantResourcePoolCapacityModel.model_validate(self.__dict__) + + +@dataclass(frozen=True) +class ParticipantResourceBudgetCapabilities: + support_strength: str + supported_owner_kinds: frozenset[str] + supported_resource_kinds: frozenset[str] + supported_accounting_modes: frozenset[str] + supported_reset_modes: frozenset[str] + supported_fairness_policies: frozenset[str] + supported_isolation_strengths: frozenset[str] + configured_pools: tuple[ParticipantResourcePoolCapacity, ...] + realization_contract_ids: frozenset[str] + cross_range_pool_refs: frozenset[str] = frozenset() + + def __post_init__(self) -> None: + ParticipantResourceBudgetCapabilitiesModel.model_validate( + { + "support_strength": self.support_strength, + "supported_owner_kinds": sorted(self.supported_owner_kinds), + "supported_resource_kinds": sorted(self.supported_resource_kinds), + "supported_accounting_modes": sorted(self.supported_accounting_modes), + "supported_reset_modes": sorted(self.supported_reset_modes), + "supported_fairness_policies": sorted(self.supported_fairness_policies), + "supported_isolation_strengths": sorted(self.supported_isolation_strengths), + "configured_pools": [pool.__dict__ for pool in self.configured_pools], + "realization_contract_ids": sorted(self.realization_contract_ids), + "cross_range_pool_refs": sorted(self.cross_range_pool_refs), + } + ) + + +def participant_resource_budget_capability_payload( + capability: ParticipantResourceBudgetCapabilities, +) -> ParticipantResourceBudgetCapabilitiesModel: + return ParticipantResourceBudgetCapabilitiesModel.model_validate( + { + "support_strength": capability.support_strength, + "supported_owner_kinds": sorted(capability.supported_owner_kinds), + "supported_resource_kinds": sorted(capability.supported_resource_kinds), + "supported_accounting_modes": sorted(capability.supported_accounting_modes), + "supported_reset_modes": sorted(capability.supported_reset_modes), + "supported_fairness_policies": sorted(capability.supported_fairness_policies), + "supported_isolation_strengths": sorted(capability.supported_isolation_strengths), + "configured_pools": [pool.__dict__ for pool in capability.configured_pools], + "realization_contract_ids": sorted(capability.realization_contract_ids), + "cross_range_pool_refs": sorted(capability.cross_range_pool_refs), + } + ) + + +def participant_resource_budget_capability_from_model( + model: ParticipantResourceBudgetCapabilitiesModel, +) -> ParticipantResourceBudgetCapabilities: + return ParticipantResourceBudgetCapabilities( + support_strength=model.support_strength, + supported_owner_kinds=frozenset(model.supported_owner_kinds), + supported_resource_kinds=frozenset(model.supported_resource_kinds), + supported_accounting_modes=frozenset(model.supported_accounting_modes), + supported_reset_modes=frozenset(model.supported_reset_modes), + supported_fairness_policies=frozenset(model.supported_fairness_policies), + supported_isolation_strengths=frozenset(model.supported_isolation_strengths), + configured_pools=tuple( + ParticipantResourcePoolCapacity( + pool_ref=pool.pool_ref, + owner_kind=pool.owner_kind, + owner_ref=pool.owner_ref, + resource_kind=pool.resource_kind, + unit=pool.unit, + accounting_mode=pool.accounting_mode, + meter_profile_ref=pool.meter_profile_ref, + capacity=pool.capacity, + tenant_isolation=pool.tenant_isolation, + configuration_digest=pool.configuration_digest, + fairness_policy=pool.fairness_policy, + priority_classes=tuple(pool.priority_classes), + borrowing=pool.borrowing, + reclaim=pool.reclaim, + max_queue_ticks=pool.max_queue_ticks, + starvation_bound_ticks=pool.starvation_bound_ticks, + protected_capacity=pool.protected_capacity, + evidence_contract_ids=tuple(pool.evidence_contract_ids), + ) + for pool in model.configured_pools + ), + realization_contract_ids=frozenset(model.realization_contract_ids), + cross_range_pool_refs=frozenset(model.cross_range_pool_refs), + ) + + +__all__ = [ + "ParticipantResourceBudgetCapabilities", + "ParticipantResourcePoolCapacity", + "participant_resource_budget_capability_from_model", + "participant_resource_budget_capability_payload", +] diff --git a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py index 9c3defc8a..89dd9bf44 100644 --- a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py +++ b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py @@ -80,6 +80,18 @@ def _snapshot_from_envelope(payload: dict[str, Any]) -> RuntimeSnapshot: participant_execution_services={ scope: state.model_dump(mode="json") for scope, state in validated.participant_execution_services.items() }, + participant_resource_budget_states={ + state_ref: state.model_dump(mode="json") + for state_ref, state in validated.participant_resource_budget_states.items() + }, + participant_resource_pool_states={ + pool_state_ref: state.model_dump(mode="json") + for pool_state_ref, state in validated.participant_resource_pool_states.items() + }, + participant_resource_budget_events={ + event_id: event.model_dump(mode="json") + for event_id, event in validated.participant_resource_budget_events.items() + }, shared_state_records={ state_address: record.model_dump(mode="json") for state_address, record in validated.shared_state_records.items() diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index 94ec48ddd..2e6d1a2fa 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -56,6 +56,12 @@ ParticipantImplementationManifestModel, ParticipantImplementationProvenanceModel, ) +from .participant_resource_budgets import ( + ParticipantResourceBudgetEventModel, + ParticipantResourceBudgetPolicyModel, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolCapacityModel, +) from .participant_runtime import ( ParticipantBehaviorHistoryEventModel, ParticipantEpisodeHistoryEventModel, @@ -195,6 +201,10 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "participant-execution-binding-v1": ParticipantExecutionBindingModel.model_json_schema(), "participant-execution-control-v1": ParticipantExecutionControlRequestModel.model_json_schema(), "participant-execution-service-state-v1": ParticipantExecutionServiceStateModel.model_json_schema(), + "participant-resource-budget-policy-v1": ParticipantResourceBudgetPolicyModel.model_json_schema(), + "participant-resource-pool-capacity-v1": ParticipantResourcePoolCapacityModel.model_json_schema(), + "participant-resource-budget-state-v1": ParticipantResourceBudgetStateModel.model_json_schema(), + "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-shared-state-record-v1": ParticipantSharedStateRecordModel.model_json_schema(), diff --git a/implementations/python/packages/raes_contracts/contracts/manifests.py b/implementations/python/packages/raes_contracts/contracts/manifests.py index 7bb12a0c4..a5f53a886 100644 --- a/implementations/python/packages/raes_contracts/contracts/manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/manifests.py @@ -30,6 +30,7 @@ from .experiment_bindings import ConfigurationTargetRegistryModel from .feature_support import ParticipantFeatureSupportModel from .participant_execution import ParticipantExecutionBindingModel +from .participant_resource_budgets import ParticipantResourceBudgetCapabilitiesModel from .time_manifest_capabilities import TimeCapabilitiesModel from .trial_cleanup import CleanupActionKind from .validators import ( @@ -117,7 +118,11 @@ class ParticipantRuntimeCapabilitiesModel(ContractModel): json_schema_extra={"uniqueItems": True}, ) supported_autonomous_policy_profiles: list[ - Literal["participant-autonomous-execution/v1", "participant-autonomous-execution/v2"] + Literal[ + "participant-autonomous-execution/v1", + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + ] ] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) supported_autonomous_activity_features: list[ Literal[ @@ -149,6 +154,7 @@ class ParticipantRuntimeCapabilitiesModel(ContractModel): supports_bounded_concurrency: bool = False max_execution_services: int | None = Field(default=None, ge=1) max_concurrent_actions: int | None = Field(default=None, ge=2) + resource_budgets: ParticipantResourceBudgetCapabilitiesModel | None = None constraints: dict[str, str] = Field(default_factory=dict) @model_validator(mode="after") @@ -218,7 +224,10 @@ def _validate_autonomous_configuration(self) -> None: ) if ( self.supports_autonomous_execution - and "participant-autonomous-execution/v2" in self.supported_autonomous_policy_profiles + and { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + }.intersection(self.supported_autonomous_policy_profiles) and ( not self.supported_autonomous_activity_features or not self.supported_autonomous_random_stream_profiles ) @@ -226,6 +235,12 @@ def _validate_autonomous_configuration(self) -> None: raise ValueError( "autonomous execution v2 requires exact activity-feature and random-stream-profile support" ) + if ( + self.supports_autonomous_execution + and "participant-autonomous-execution/v3" in self.supported_autonomous_policy_profiles + and self.resource_budgets is None + ): + raise ValueError("autonomous execution v3 requires participant resource-budget capabilities") if not self.supports_autonomous_execution and self._has_any_autonomous_configuration(): raise ValueError("autonomous execution limits require autonomous execution support") @@ -259,6 +274,7 @@ def _has_any_autonomous_configuration(self) -> bool: or self.supports_bounded_concurrency or self.max_execution_services is not None or self.max_concurrent_actions is not None + or self.resource_budgets is not None ) def _validate_execution_control(self) -> None: diff --git a/implementations/python/packages/raes_contracts/contracts/participant_execution.py b/implementations/python/packages/raes_contracts/contracts/participant_execution.py index 05bb35320..57fde11e3 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_execution.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_execution.py @@ -101,6 +101,7 @@ class ParticipantExecutionServiceStateModel(ContractModel): binding_digest: PrefixedDigestString time_declaration_digest: PrefixedDigestString scheduler_state_refs: tuple[NonEmptyString, ...] = () + resource_budget_state_refs: tuple[NonEmptyString, ...] = () capacity: int = Field(ge=1) reserved: int = Field(ge=0) in_flight: int = Field(ge=0) @@ -143,6 +144,7 @@ def _validate_lifecycle_readback(self) -> None: def _validate_unique_references(self) -> None: for field_name in ( "scheduler_state_refs", + "resource_budget_state_refs", "pacing_deviation_refs", "evidence_refs", ): diff --git a/implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py b/implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py new file mode 100644 index 000000000..ef2465cbc --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py @@ -0,0 +1,496 @@ +"""Portable participant resource-budget intent, capacity, and runtime carriers.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import Field, model_validator + +from .base import ContractModel, NonEmptyString, PrefixedDigestString +from .participant_resource_types import ( + EVENT_DISPOSITION as _EVENT_DISPOSITION, +) +from .participant_resource_types import ( + PARTICIPANT_RESOURCE_BUDGET_EVENT_SCHEMA_VERSION, + PARTICIPANT_RESOURCE_BUDGET_POLICY_SCHEMA_VERSION, + PARTICIPANT_RESOURCE_BUDGET_STATE_SCHEMA_VERSION, + PARTICIPANT_RESOURCE_POOL_CAPACITY_SCHEMA_VERSION, + ParticipantResourceAccountingMode, + ParticipantResourceIsolationStrength, + ParticipantResourceKind, + ParticipantResourceOwnerKind, + ParticipantResourceResetMode, + participant_resource_budget_state_ref, + participant_resource_pool_state_ref, +) +from .participant_resource_types import ( + RESOURCE_ACCOUNTING as _RESOURCE_ACCOUNTING, +) +from .participant_resource_types import ( + RESOURCE_UNIT as _RESOURCE_UNIT, +) +from .participant_resource_types import ( + require_quantity_semantics as _require_quantity_semantics, +) + + +class ParticipantResourceOwnerModel(ContractModel): + owner_id: NonEmptyString + kind: ParticipantResourceOwnerKind + owner_ref: NonEmptyString + + +class ParticipantResourceQuantityModel(ContractModel): + resource_kind: ParticipantResourceKind + unit: NonEmptyString + accounting_mode: ParticipantResourceAccountingMode + meter_profile_ref: NonEmptyString + amount: int = Field(ge=0, le=10**18) + + @model_validator(mode="after") + def _validate_quantity(self) -> ParticipantResourceQuantityModel: + _require_quantity_semantics(self.resource_kind, self.unit, self.accounting_mode) + return self + + +class ParticipantResourceFairnessModel(ContractModel): + policy: NonEmptyString + priority_class: NonEmptyString + weight: int = Field(ge=1, le=1_000_000) + protected: bool + borrowing: NonEmptyString + reclaim: NonEmptyString + max_queue_ticks: int = Field(ge=0, le=1_000_000_000) + starvation_bound_ticks: int = Field(ge=1, le=1_000_000_000) + + +class ParticipantResourceBudgetDemandModel(ContractModel): + budget_id: NonEmptyString + owner: ParticipantResourceOwnerModel + pool_ref: NonEmptyString + quantity: ParticipantResourceQuantityModel + limit: int = Field(ge=1, le=10**18) + reservation: int = Field(ge=1, le=10**18) + reset: ParticipantResourceResetMode + window_ticks: int | None = Field(default=None, ge=1, le=1_000_000_000) + parent_budget_ref: NonEmptyString | None = None + evidence_refs: tuple[NonEmptyString, ...] = () + provenance: Literal["authored", "legacy_maximum"] = "authored" + + @model_validator(mode="after") + def _validate_demand(self) -> ParticipantResourceBudgetDemandModel: + if self.reservation > self.limit: + raise ValueError("resource reservation cannot exceed its limit") + windowed = self.quantity.accounting_mode == "windowed_counter" + if windowed != (self.window_ticks is not None): + raise ValueError("windowed resource demands require window_ticks and other modes forbid it") + if len(self.evidence_refs) != len(set(self.evidence_refs)): + raise ValueError("resource demand evidence_refs must be unique") + if self.quantity.resource_kind == "storage_growth" and self.reset != "reconciled": + raise ValueError("storage_growth resource demand requires reconciled reset") + return self + + +class ParticipantResourceBudgetPolicyModel(ContractModel): + schema_version: Literal[PARTICIPANT_RESOURCE_BUDGET_POLICY_SCHEMA_VERSION] = ( + PARTICIPANT_RESOURCE_BUDGET_POLICY_SCHEMA_VERSION + ) + policy_id: NonEmptyString + policy_address: NonEmptyString + policy_digest: PrefixedDigestString + owners: tuple[ParticipantResourceOwnerModel, ...] = Field(min_length=1) + demands: tuple[ParticipantResourceBudgetDemandModel, ...] = Field(min_length=1) + fairness: ParticipantResourceFairnessModel + + @model_validator(mode="after") + def _validate_policy(self) -> ParticipantResourceBudgetPolicyModel: + owner_ids = [owner.owner_id for owner in self.owners] + budget_ids = [demand.budget_id for demand in self.demands] + if len(owner_ids) != len(set(owner_ids)): + raise ValueError("resource-budget policy owner ids must be unique") + if len(budget_ids) != len(set(budget_ids)): + raise ValueError("resource-budget policy budget ids must be unique") + owners = {owner.owner_id: owner for owner in self.owners} + demands = {demand.budget_id: demand for demand in self.demands} + required_kinds = set(_RESOURCE_UNIT) + actual_kinds = {demand.quantity.resource_kind for demand in self.demands} + missing = sorted(required_kinds - actual_kinds) + if missing: + raise ValueError("resource-budget policy requires complete resource vector: " + ", ".join(missing)) + for demand in self.demands: + if demand.owner.owner_id not in owners or owners[demand.owner.owner_id] != demand.owner: + raise ValueError("resource-budget demand owner must resolve exactly in policy owners") + if demand.parent_budget_ref is not None and demand.parent_budget_ref not in demands: + raise ValueError("resource-budget demand parent must resolve in policy demands") + visiting: set[str] = set() + visited: set[str] = set() + + def visit(budget_id: str) -> None: + if budget_id in visiting: + raise ValueError("resource-budget policy parent graph must be acyclic") + if budget_id in visited: + return + visiting.add(budget_id) + demand = demands[budget_id] + if demand.parent_budget_ref is not None: + parent = demands[demand.parent_budget_ref] + if ( + demand.quantity.resource_kind, + demand.quantity.unit, + demand.quantity.accounting_mode, + demand.quantity.meter_profile_ref, + ) != ( + parent.quantity.resource_kind, + parent.quantity.unit, + parent.quantity.accounting_mode, + parent.quantity.meter_profile_ref, + ): + raise ValueError("resource-budget parent must use the same resource, unit, mode, and meter") + if demand.limit > parent.limit: + raise ValueError("resource-budget child limit cannot exceed its parent") + visit(demand.parent_budget_ref) + visiting.remove(budget_id) + visited.add(budget_id) + + for budget_id in demands: + visit(budget_id) + children_by_parent: dict[str, list[ParticipantResourceBudgetDemandModel]] = {} + for demand in self.demands: + if demand.parent_budget_ref is not None: + children_by_parent.setdefault(demand.parent_budget_ref, []).append(demand) + for parent_id, children in children_by_parent.items(): + if sum(child.limit for child in children) > demands[parent_id].limit: + raise ValueError("resource-budget sibling limits cannot exceed their parent limit") + pool_keys = [ + ( + demand.pool_ref, + demand.owner.kind, + demand.owner.owner_ref, + demand.quantity.resource_kind, + demand.quantity.unit, + demand.quantity.accounting_mode, + demand.quantity.meter_profile_ref, + ) + for demand in self.demands + ] + if len(pool_keys) != len(set(pool_keys)): + raise ValueError("resource-budget demands cannot alias the same canonical resource pool") + return self + + +class ParticipantResourcePoolCapacityModel(ContractModel): + schema_version: Literal[PARTICIPANT_RESOURCE_POOL_CAPACITY_SCHEMA_VERSION] = ( + PARTICIPANT_RESOURCE_POOL_CAPACITY_SCHEMA_VERSION + ) + pool_ref: NonEmptyString + owner_kind: ParticipantResourceOwnerKind + owner_ref: NonEmptyString + resource_kind: ParticipantResourceKind + unit: NonEmptyString + accounting_mode: ParticipantResourceAccountingMode + meter_profile_ref: NonEmptyString + capacity: int = Field(ge=1, le=10**18) + tenant_isolation: ParticipantResourceIsolationStrength + configuration_digest: PrefixedDigestString + fairness_policy: NonEmptyString + priority_classes: tuple[NonEmptyString, ...] = Field(min_length=1) + borrowing: NonEmptyString + reclaim: NonEmptyString + max_queue_ticks: int = Field(ge=0, le=1_000_000_000) + starvation_bound_ticks: int = Field(ge=1, le=1_000_000_000) + protected_capacity: int = Field(ge=0, le=10**18) + evidence_contract_ids: tuple[NonEmptyString, ...] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_capacity(self) -> ParticipantResourcePoolCapacityModel: + _require_quantity_semantics(self.resource_kind, self.unit, self.accounting_mode) + if self.protected_capacity > self.capacity: + raise ValueError("protected capacity cannot exceed configured capacity") + if len(self.priority_classes) != len(set(self.priority_classes)): + raise ValueError("pool priority classes must be unique") + if len(self.evidence_contract_ids) != len(set(self.evidence_contract_ids)): + raise ValueError("pool evidence contract ids must be unique") + return self + + +class ParticipantResourceBudgetCapabilitiesModel(ContractModel): + """Declared support plus configuration-bound logical pool capacity.""" + + support_strength: Literal["unsupported", "disclosed_weak", "bounded", "exact"] + supported_owner_kinds: list[ParticipantResourceOwnerKind] = Field(min_length=1) + supported_resource_kinds: list[ParticipantResourceKind] = Field(min_length=1) + supported_accounting_modes: list[ParticipantResourceAccountingMode] = Field(min_length=1) + supported_reset_modes: list[ParticipantResourceResetMode] = Field(min_length=1) + supported_fairness_policies: list[NonEmptyString] = Field(min_length=1) + supported_isolation_strengths: list[ParticipantResourceIsolationStrength] = Field(min_length=1) + configured_pools: list[ParticipantResourcePoolCapacityModel] = Field(min_length=1) + realization_contract_ids: list[NonEmptyString] = Field(min_length=1) + cross_range_pool_refs: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_capabilities(self) -> ParticipantResourceBudgetCapabilitiesModel: + for field_name in ( + "supported_owner_kinds", + "supported_resource_kinds", + "supported_accounting_modes", + "supported_reset_modes", + "supported_fairness_policies", + "supported_isolation_strengths", + "realization_contract_ids", + "cross_range_pool_refs", + ): + values = getattr(self, field_name) + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must be unique") + keys = [(pool.pool_ref, pool.resource_kind, pool.meter_profile_ref) for pool in self.configured_pools] + if len(keys) != len(set(keys)): + raise ValueError("configured pool resource entries must be unique") + pools_by_ref = { + pool_ref: tuple(pool for pool in self.configured_pools if pool.pool_ref == pool_ref) + for pool_ref in self.cross_range_pool_refs + } + for _pool_ref, pools in pools_by_ref.items(): + if not pools: + raise ValueError("cross-range pool ref must resolve") + if any(pool.tenant_isolation != "tenant_partitioned" for pool in pools): + raise ValueError("cross-range shared pools require tenant_partitioned isolation") + for pool in self.configured_pools: + if pool.owner_kind not in self.supported_owner_kinds: + raise ValueError("configured pool owner kind is not declared supported") + if pool.resource_kind not in self.supported_resource_kinds: + raise ValueError("configured pool resource kind is not declared supported") + if pool.accounting_mode not in self.supported_accounting_modes: + raise ValueError("configured pool accounting mode is not declared supported") + if pool.fairness_policy not in self.supported_fairness_policies: + raise ValueError("configured pool fairness policy is not declared supported") + if pool.tenant_isolation not in self.supported_isolation_strengths: + raise ValueError("configured pool isolation strength is not declared supported") + return self + + +class ParticipantResourceBudgetStateModel(ContractModel): + schema_version: Literal[PARTICIPANT_RESOURCE_BUDGET_STATE_SCHEMA_VERSION] = ( + PARTICIPANT_RESOURCE_BUDGET_STATE_SCHEMA_VERSION + ) + state_ref: NonEmptyString + budget_id: NonEmptyString + policy_address: NonEmptyString + owner_kind: ParticipantResourceOwnerKind + owner_ref: NonEmptyString + pool_ref: NonEmptyString + resource_kind: ParticipantResourceKind + unit: NonEmptyString + accounting_mode: ParticipantResourceAccountingMode + meter_profile_ref: NonEmptyString + reset: ParticipantResourceResetMode + generation: int = Field(ge=0) + limit: int = Field(ge=1, le=10**18) + configured_capacity: int = Field(ge=1, le=10**18) + reserved: int = Field(ge=0, le=10**18) + current_use: int = Field(ge=0, le=10**18) + cumulative_use: int = Field(ge=0, le=10**18) + throttled: int = Field(ge=0) + rejected: int = Field(ge=0) + reconciliation_status: Literal["reconciled", "pending", "unreconciled"] + last_event_ref: NonEmptyString + evidence_refs: tuple[NonEmptyString, ...] = () + + @model_validator(mode="after") + def _validate_state(self) -> ParticipantResourceBudgetStateModel: + _require_quantity_semantics(self.resource_kind, self.unit, self.accounting_mode) + expected_ref = participant_resource_budget_state_ref(self.policy_address, self.budget_id) + if self.state_ref != expected_ref: + raise ValueError("resource budget state_ref must equal its canonical policy-scoped identity") + if self.reserved + self.current_use > self.limit: + raise ValueError("resource budget reserved and current use cannot exceed limit") + if self.reserved + self.current_use > self.configured_capacity: + raise ValueError("resource budget reserved and current use cannot exceed configured capacity") + if len(self.evidence_refs) != len(set(self.evidence_refs)): + raise ValueError("resource budget evidence refs must be unique") + return self + + +class ParticipantResourceMeasurementRequirementModel(ContractModel): + budget_state_ref: NonEmptyString + resource_kind: ParticipantResourceKind + unit: NonEmptyString + meter_profile_ref: NonEmptyString + reserved: int = Field(ge=0, le=10**18) + + @model_validator(mode="after") + def _validate_requirement(self) -> ParticipantResourceMeasurementRequirementModel: + expected_modes = _RESOURCE_ACCOUNTING[self.resource_kind] + if not expected_modes: + raise ValueError("resource measurement requires supported quantity semantics") + if self.unit != _RESOURCE_UNIT[self.resource_kind]: + raise ValueError("resource measurement requirement unit must match its resource kind") + return self + + +class ParticipantResourceMeasurementModel(ContractModel): + budget_state_ref: NonEmptyString + operation_id: NonEmptyString + execution_generation: int = Field(ge=0) + resource_kind: ParticipantResourceKind + unit: NonEmptyString + meter_profile_ref: NonEmptyString + measured: int = Field(ge=0, le=10**18) + evidence_refs: tuple[NonEmptyString, ...] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_measurement(self) -> ParticipantResourceMeasurementModel: + if self.unit != _RESOURCE_UNIT[self.resource_kind]: + raise ValueError("resource measurement unit must match its resource kind") + if len(self.evidence_refs) != len(set(self.evidence_refs)): + raise ValueError("resource measurement evidence refs must be unique") + return self + + +class ParticipantResourcePoolAllocationModel(ContractModel): + budget_state_ref: NonEmptyString + policy_address: NonEmptyString + budget_id: NonEmptyString + generation: int = Field(ge=0) + priority_class: NonEmptyString + weight: int = Field(ge=1, le=1_000_000) + protected: bool + borrowing: NonEmptyString + reclaim: NonEmptyString + max_queue_ticks: int = Field(ge=0, le=1_000_000_000) + starvation_bound_ticks: int = Field(ge=1, le=1_000_000_000) + reserved: int = Field(ge=0, le=10**18) + current_use: int = Field(ge=0, le=10**18) + cumulative_use: int = Field(ge=0, le=10**18) + + @model_validator(mode="after") + def _validate_allocation(self) -> ParticipantResourcePoolAllocationModel: + expected_ref = participant_resource_budget_state_ref(self.policy_address, self.budget_id) + if self.budget_state_ref != expected_ref: + raise ValueError("pool allocation must reference its canonical policy-scoped budget state") + return self + + +class ParticipantResourcePoolStateModel(ContractModel): + """Authoritative allocation ledger for one exact physical resource pool.""" + + pool_state_ref: NonEmptyString + pool_ref: NonEmptyString + owner_kind: ParticipantResourceOwnerKind + owner_ref: NonEmptyString + resource_kind: ParticipantResourceKind + unit: NonEmptyString + accounting_mode: ParticipantResourceAccountingMode + meter_profile_ref: NonEmptyString + capacity: int = Field(ge=1, le=10**18) + protected_capacity: int = Field(ge=0, le=10**18) + fairness_policy: NonEmptyString + priority_classes: tuple[NonEmptyString, ...] = Field(min_length=1) + borrowing: NonEmptyString + reclaim: NonEmptyString + max_queue_ticks: int = Field(ge=0, le=1_000_000_000) + starvation_bound_ticks: int = Field(ge=1, le=1_000_000_000) + allocations: dict[NonEmptyString, ParticipantResourcePoolAllocationModel] = Field(default_factory=dict) + + @model_validator(mode="after") + def _validate_pool_state(self) -> ParticipantResourcePoolStateModel: + _require_quantity_semantics(self.resource_kind, self.unit, self.accounting_mode) + expected_ref = participant_resource_pool_state_ref( + pool_ref=self.pool_ref, + owner_kind=self.owner_kind, + owner_ref=self.owner_ref, + resource_kind=self.resource_kind, + unit=self.unit, + accounting_mode=self.accounting_mode, + meter_profile_ref=self.meter_profile_ref, + ) + if self.pool_state_ref != expected_ref: + raise ValueError("pool_state_ref must equal the canonical exact-pool identity") + if self.protected_capacity > self.capacity: + raise ValueError("protected capacity cannot exceed physical pool capacity") + for allocation_ref, allocation in self.allocations.items(): + if allocation_ref != allocation.budget_state_ref: + raise ValueError("pool allocation map key must equal budget_state_ref") + if allocation.priority_class not in self.priority_classes: + raise ValueError("pool allocation priority class must be configured") + if allocation.borrowing != self.borrowing or allocation.reclaim != self.reclaim: + raise ValueError("pool allocation borrowing and reclaim must match pool authority") + if ( + self.max_queue_ticks > allocation.max_queue_ticks + or self.starvation_bound_ticks > allocation.starvation_bound_ticks + ): + raise ValueError("pool allocation fairness bounds are weaker than required") + total = sum( + ( + allocation.current_use + allocation.reserved + if self.accounting_mode in {"reservable_gauge", "lease"} + else allocation.cumulative_use + allocation.reserved + ) + for allocation in self.allocations.values() + ) + if total > self.capacity: + raise ValueError("physical pool allocations cannot exceed capacity") + return self + + +class ParticipantResourceBudgetEventModel(ContractModel): + schema_version: Literal[PARTICIPANT_RESOURCE_BUDGET_EVENT_SCHEMA_VERSION] = ( + PARTICIPANT_RESOURCE_BUDGET_EVENT_SCHEMA_VERSION + ) + event_id: NonEmptyString + operation_id: NonEmptyString + budget_state_ref: NonEmptyString + budget_id: NonEmptyString + policy_address: NonEmptyString + owner_ref: NonEmptyString + pool_ref: NonEmptyString + execution_generation: int = Field(ge=0) + transition: Literal["reserve", "commit", "release", "throttle", "reject", "reconcile"] + disposition: Literal["reserved", "committed", "released", "throttled", "rejected", "reconciled"] + requested: int = Field(ge=0, le=10**18) + measured: int | None = Field(default=None, ge=0, le=10**18) + resource_kind: ParticipantResourceKind + unit: NonEmptyString + meter_profile_ref: NonEmptyString + predecessor_event_ref: NonEmptyString | None = None + evidence_refs: tuple[NonEmptyString, ...] = () + + @model_validator(mode="after") + def _validate_event(self) -> ParticipantResourceBudgetEventModel: + expected_ref = participant_resource_budget_state_ref(self.policy_address, self.budget_id) + if self.budget_state_ref != expected_ref: + raise ValueError("resource budget event must reference its canonical policy-scoped state") + if self.disposition != _EVENT_DISPOSITION[self.transition]: + raise ValueError("resource budget event disposition must match its transition") + if self.transition == "commit" and self.measured is None: + raise ValueError("commit events require measured resource use") + if len(self.evidence_refs) != len(set(self.evidence_refs)): + raise ValueError("resource budget event evidence refs must be unique") + return self + + +__all__ = [ + "PARTICIPANT_RESOURCE_BUDGET_EVENT_SCHEMA_VERSION", + "PARTICIPANT_RESOURCE_BUDGET_POLICY_SCHEMA_VERSION", + "PARTICIPANT_RESOURCE_BUDGET_STATE_SCHEMA_VERSION", + "PARTICIPANT_RESOURCE_POOL_CAPACITY_SCHEMA_VERSION", + "ParticipantResourceAccountingMode", + "ParticipantResourceBudgetDemandModel", + "ParticipantResourceBudgetCapabilitiesModel", + "ParticipantResourceBudgetEventModel", + "ParticipantResourceBudgetPolicyModel", + "ParticipantResourceBudgetStateModel", + "ParticipantResourceFairnessModel", + "ParticipantResourceIsolationStrength", + "ParticipantResourceKind", + "ParticipantResourceOwnerKind", + "ParticipantResourceOwnerModel", + "ParticipantResourcePoolCapacityModel", + "ParticipantResourcePoolAllocationModel", + "ParticipantResourcePoolStateModel", + "ParticipantResourceMeasurementModel", + "ParticipantResourceMeasurementRequirementModel", + "ParticipantResourceQuantityModel", + "ParticipantResourceResetMode", + "participant_resource_budget_state_ref", + "participant_resource_pool_state_ref", +] diff --git a/implementations/python/packages/raes_contracts/contracts/participant_resource_types.py b/implementations/python/packages/raes_contracts/contracts/participant_resource_types.py new file mode 100644 index 000000000..68bcfd27f --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_resource_types.py @@ -0,0 +1,125 @@ +"""Shared participant resource-budget literals, identities, and quantity rules.""" + +from __future__ import annotations + +import hashlib +import json +from typing import Literal + +PARTICIPANT_RESOURCE_BUDGET_POLICY_SCHEMA_VERSION = "participant-resource-budget-policy/v1" +PARTICIPANT_RESOURCE_POOL_CAPACITY_SCHEMA_VERSION = "participant-resource-pool-capacity/v1" +PARTICIPANT_RESOURCE_BUDGET_STATE_SCHEMA_VERSION = "participant-resource-budget-state/v1" +PARTICIPANT_RESOURCE_BUDGET_EVENT_SCHEMA_VERSION = "participant-resource-budget-event/v1" + +ParticipantResourceOwnerKind = Literal[ + "participant", + "deployment_tenant", + "shared_service", + "fleet", +] +ParticipantResourceKind = Literal[ + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator", +] +ParticipantResourceAccountingMode = Literal[ + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease", +] +ParticipantResourceResetMode = Literal["episode", "time_segment", "run", "reconciled"] +ParticipantResourceIsolationStrength = Literal["none", "stateless", "tenant_partitioned"] + +RESOURCE_UNIT = { + "action_rate": "actions", + "concurrent_actions": "actions", + "storage_growth": "bytes", + "inference_tokens": "tokens", + "image_generations": "images", + "accelerator": "accelerator_milliseconds", +} +RESOURCE_ACCOUNTING = { + "action_rate": {"windowed_counter"}, + "concurrent_actions": {"reservable_gauge"}, + "storage_growth": {"growth_counter"}, + "inference_tokens": {"windowed_counter", "cumulative_counter"}, + "image_generations": {"windowed_counter", "cumulative_counter"}, + "accelerator": {"lease"}, +} +EVENT_DISPOSITION = { + "reserve": "reserved", + "commit": "committed", + "release": "released", + "throttle": "throttled", + "reject": "rejected", + "reconcile": "reconciled", +} + + +def require_quantity_semantics( + resource_kind: str, + unit: str, + accounting_mode: str, +) -> None: + expected_unit = RESOURCE_UNIT[resource_kind] + if unit != expected_unit: + raise ValueError(f"{resource_kind} resource quantity requires unit {expected_unit!r}") + if accounting_mode not in RESOURCE_ACCOUNTING[resource_kind]: + raise ValueError(f"{resource_kind} resource quantity does not support accounting mode {accounting_mode!r}") + + +def participant_resource_budget_state_ref(policy_address: str, budget_id: str) -> str: + """Return the globally stable identity of one policy-local budget state.""" + + return f"{policy_address}.resource-budget-state.{budget_id}" + + +def participant_resource_pool_state_ref( + *, + pool_ref: str, + owner_kind: str, + owner_ref: str, + resource_kind: str, + unit: str, + accounting_mode: str, + meter_profile_ref: str, +) -> str: + """Return the stable identity of one exact physical accounting pool.""" + + canonical = json.dumps( + ( + pool_ref, + owner_kind, + owner_ref, + resource_kind, + unit, + accounting_mode, + meter_profile_ref, + ), + separators=(",", ":"), + ) + return "participant-resource-pool:sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +__all__ = [ + "EVENT_DISPOSITION", + "PARTICIPANT_RESOURCE_BUDGET_EVENT_SCHEMA_VERSION", + "PARTICIPANT_RESOURCE_BUDGET_POLICY_SCHEMA_VERSION", + "PARTICIPANT_RESOURCE_BUDGET_STATE_SCHEMA_VERSION", + "PARTICIPANT_RESOURCE_POOL_CAPACITY_SCHEMA_VERSION", + "ParticipantResourceAccountingMode", + "ParticipantResourceIsolationStrength", + "ParticipantResourceKind", + "ParticipantResourceOwnerKind", + "ParticipantResourceResetMode", + "RESOURCE_ACCOUNTING", + "RESOURCE_UNIT", + "participant_resource_budget_state_ref", + "participant_resource_pool_state_ref", + "require_quantity_semantics", +] diff --git a/implementations/python/packages/raes_contracts/contracts/participant_runtime.py b/implementations/python/packages/raes_contracts/contracts/participant_runtime.py index 681d58a5f..5fdf334bc 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_runtime.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_runtime.py @@ -36,6 +36,7 @@ ) from ..versions import PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION from .base import ContractModel, NonEmptyString +from .participant_resource_budgets import ParticipantResourceMeasurementModel from .random_stream import ParticipantStreamAddressModel @@ -103,9 +104,17 @@ class ParticipantActionResultModel(ContractModel): effects: list[ParticipantActionEffectResultModel] = Field(default_factory=list) failure_class: ParticipantFailureClass | None = None observations: list[NonEmptyString] = Field(default_factory=list) + resource_measurements: list[ParticipantResourceMeasurementModel] = Field(default_factory=list) evidence_refs: list[NonEmptyString] = Field(default_factory=list) diagnostics: list[NonEmptyString] = Field(default_factory=list) + @model_validator(mode="after") + def _validate_resource_measurements(self) -> ParticipantActionResultModel: + state_refs = [measurement.budget_state_ref for measurement in self.resource_measurements] + if len(state_refs) != len(set(state_refs)): + raise ValueError("participant action resource measurements must have unique budget_state_ref values") + return self + class ParticipantTemporalRuntimeContextModel(ContractModel): temporal_contract_id: NonEmptyString @@ -194,7 +203,10 @@ class ParticipantActivityOccurrenceProvenanceModel(ContractModel): """Safe within-run scheduler provenance for one native action attempt.""" policy_address: NonEmptyString - policy_profile: Literal["participant-autonomous-execution/v2"] + policy_profile: Literal[ + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + ] occurrence_id: NonEmptyString attempt_id: NonEmptyString predecessor_attempt_id: NonEmptyString | None = None @@ -274,6 +286,7 @@ class ParticipantAutonomousExecutionStateModel(ContractModel): profile: Literal[ "participant-autonomous-execution/v1", "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", ] = "participant-autonomous-execution/v1" occurrence_ordinal: StrictInt = Field(default=0, ge=0) current_retry: StrictInt = Field(default=0, ge=0) diff --git a/implementations/python/packages/raes_contracts/contracts/realization_plans.py b/implementations/python/packages/raes_contracts/contracts/realization_plans.py index 9ab633e10..9321bd0cb 100644 --- a/implementations/python/packages/raes_contracts/contracts/realization_plans.py +++ b/implementations/python/packages/raes_contracts/contracts/realization_plans.py @@ -25,6 +25,11 @@ ParticipantTimeManagementContextModel, ) from .participant_execution import ParticipantExecutionServiceStateModel +from .participant_resource_budgets import ( + ParticipantResourceBudgetEventModel, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolStateModel, +) from .participant_runtime import ( ParticipantAutonomousExecutionStateModel, ParticipantBehaviorHistoryEventModel, @@ -172,6 +177,9 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): default_factory=dict ) participant_execution_services: dict[str, ParticipantExecutionServiceStateModel] = Field(default_factory=dict) + participant_resource_budget_states: dict[str, ParticipantResourceBudgetStateModel] = Field(default_factory=dict) + participant_resource_pool_states: dict[str, ParticipantResourcePoolStateModel] = Field(default_factory=dict) + participant_resource_budget_events: dict[str, ParticipantResourceBudgetEventModel] = Field(default_factory=dict) shared_state_records: dict[str, ParticipantSharedStateRecordModel] = Field(default_factory=dict) shared_state_history: dict[str, list[ParticipantSharedStateRecordModel]] = Field(default_factory=dict) joint_action_records: dict[str, ParticipantJointActionRecordModel] = Field(default_factory=dict) @@ -195,6 +203,42 @@ def _validate_entry_addresses(self) -> RuntimeSnapshotEnvelopeModel: for map_key, state in self.participant_execution_services.items(): if map_key != state.execution_scope_ref: raise ValueError("Participant execution service map key must equal execution_scope_ref") + for map_key, state in self.participant_resource_budget_states.items(): + if map_key != state.state_ref: + raise ValueError("Participant resource-budget state map key must equal state_ref") + for map_key, state in self.participant_resource_pool_states.items(): + if map_key != state.pool_state_ref: + raise ValueError("Participant resource-pool state map key must equal pool_state_ref") + for map_key, event in self.participant_resource_budget_events.items(): + if map_key != event.event_id: + raise ValueError("Participant resource-budget event map key must equal event_id") + budget_refs = set(self.participant_resource_budget_states) + for service in self.participant_execution_services.values(): + missing = sorted(set(service.resource_budget_state_refs) - budget_refs) + if missing: + raise ValueError( + "Participant execution service references missing resource-budget states: " + ", ".join(missing) + ) + concurrency = [ + self.participant_resource_budget_states[budget_id] + for budget_id in service.resource_budget_state_refs + if self.participant_resource_budget_states[budget_id].resource_kind == "concurrent_actions" + ] + if concurrency: + if len(concurrency) != 1: + raise ValueError( + "Participant execution service must reference exactly one authoritative concurrency budget" + ) + authoritative = concurrency[0] + if ( + service.capacity != authoritative.limit + or service.reserved != authoritative.reserved + or service.in_flight != authoritative.current_use + ): + raise ValueError( + "Participant execution service concurrency projection must " + "equal its authoritative resource-budget state" + ) return self diff --git a/implementations/python/packages/raes_contracts/manifest_authority.py b/implementations/python/packages/raes_contracts/manifest_authority.py index a8a9226b9..7ae084967 100644 --- a/implementations/python/packages/raes_contracts/manifest_authority.py +++ b/implementations/python/packages/raes_contracts/manifest_authority.py @@ -28,6 +28,7 @@ "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", "participant-behavior-history-event-stream-v1", + "participant-resource-budget-policy-v1", "time-model-v1", ) @@ -56,6 +57,10 @@ "participant-execution-binding-v1", "participant-execution-control-v1", "participant-execution-service-state-v1", + "participant-resource-budget-policy-v1", + "participant-resource-pool-capacity-v1", + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", "participant-control-occurrence-v1", "participant-crossing-occurrence-v1", "participant-lifecycle-event-v1", diff --git a/implementations/python/packages/raes_contracts/participant_binding.py b/implementations/python/packages/raes_contracts/participant_binding.py index 126d5eeaf..ab756c6cf 100644 --- a/implementations/python/packages/raes_contracts/participant_binding.py +++ b/implementations/python/packages/raes_contracts/participant_binding.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, replace from typing import Protocol, cast +from . import participant_binding_validation as _binding_validation from .contracts import ( ParticipantActionResultModel, ParticipantBehaviorHistoryEventModel, @@ -20,6 +21,7 @@ ParticipantObservationDetailsModel, ParticipantTemporalRuntimeContextModel, ) +from .contracts.participant_resource_budgets import ParticipantResourceMeasurementRequirementModel from .participant_action_arguments import ( ParticipantActionArgumentScalar, ParticipantActionArgumentValue, @@ -39,21 +41,6 @@ participant_behavior_event_payload, participant_implementation_actor_provenance, ) -from .participant_binding_validation import ( - ACTION_CONTRACT_PREFIX as _ACTION_CONTRACT_PREFIX, -) -from .participant_binding_validation import ( - OBSERVATION_BOUNDARY_PREFIX as _OBSERVATION_BOUNDARY_PREFIX, -) -from .participant_binding_validation import ( - require_non_empty as _require_non_empty, -) -from .participant_binding_validation import ( - require_prefixed as _require_prefixed, -) -from .participant_binding_validation import ( - string_tuple as _string_tuple, -) from .participant_native_execution import ParticipantNativeActionExecution from .runtime_state import ApplyResult @@ -113,6 +100,7 @@ class ParticipantActionAdmissionRequest: target_addresses: tuple[str, ...] = () execution_scope_ref: str | None = None execution_generation: int | None = None + resource_measurement_requirements: tuple[ParticipantResourceMeasurementRequirementModel, ...] = () def __post_init__(self) -> None: _validate_admission_request_basics(self) @@ -124,17 +112,21 @@ def __post_init__(self) -> None: def _validate_admission_request_basics(request: ParticipantActionAdmissionRequest) -> None: - _require_non_empty(request.participant_address, "participant_address") - _require_prefixed(request.action_contract_address, _ACTION_CONTRACT_PREFIX, "action_contract_address") - _require_prefixed( + _binding_validation.require_non_empty(request.participant_address, "participant_address") + _binding_validation.require_prefixed( + request.action_contract_address, + _binding_validation.ACTION_CONTRACT_PREFIX, + "action_contract_address", + ) + _binding_validation.require_prefixed( request.observation_boundary_address, - _OBSERVATION_BOUNDARY_PREFIX, + _binding_validation.OBSERVATION_BOUNDARY_PREFIX, "observation_boundary_address", ) - _require_non_empty(request.action_instance_id, "action_instance_id") - _require_non_empty(request.state_transition_kind, "state_transition_kind") + _binding_validation.require_non_empty(request.action_instance_id, "action_instance_id") + _binding_validation.require_non_empty(request.state_transition_kind, "state_transition_kind") if request.post_state_digest is not None: - _require_non_empty(request.post_state_digest, "post_state_digest") + _binding_validation.require_non_empty(request.post_state_digest, "post_state_digest") if not isinstance(request.implementation_manifest, ParticipantImplementationManifestModel): raise TypeError("implementation_manifest must be a ParticipantImplementationManifestModel") if not isinstance(request.implementation_selection, ParticipantImplementationSelectionModel): @@ -155,11 +147,12 @@ def _validate_admission_request_selection_and_execution(request: ParticipantActi if (request.execution_scope_ref is None) != (request.execution_generation is None): raise ValueError("execution_scope_ref and execution_generation must be provided together") if request.execution_scope_ref is not None: - _require_non_empty(request.execution_scope_ref, "execution_scope_ref") + _binding_validation.require_non_empty(request.execution_scope_ref, "execution_scope_ref") if request.execution_generation is None or request.execution_generation < 0: raise ValueError("execution_generation must be non-negative") if not request.target_addresses: raise ValueError("generation-bound participant actions require target_addresses") + _binding_validation.validate_resource_measurement_requirements(request.resource_measurement_requirements) def _validate_and_normalize_admission_request_contexts(request: ParticipantActionAdmissionRequest) -> None: @@ -174,7 +167,11 @@ def _validate_and_normalize_admission_request_contexts(request: ParticipantActio "observation_boundary_evidence_refs", "target_addresses", ): - object.__setattr__(request, field_name, _string_tuple(getattr(request, field_name), field_name)) + object.__setattr__( + request, + field_name, + _binding_validation.string_tuple(getattr(request, field_name), field_name), + ) @dataclass diff --git a/implementations/python/packages/raes_contracts/participant_binding_validation.py b/implementations/python/packages/raes_contracts/participant_binding_validation.py index 22f9067fd..5fde17ba9 100644 --- a/implementations/python/packages/raes_contracts/participant_binding_validation.py +++ b/implementations/python/packages/raes_contracts/participant_binding_validation.py @@ -2,6 +2,8 @@ from collections.abc import Iterable +from .contracts.participant_resource_budgets import ParticipantResourceMeasurementRequirementModel + ACTION_CONTRACT_PREFIX = "participant.action-contract." OBSERVATION_BOUNDARY_PREFIX = "participant.observation-boundary." @@ -26,3 +28,16 @@ def string_tuple(value: Iterable[str], field_name: str) -> tuple[str, ...]: if len(set(values)) != len(values): raise ValueError(f"{field_name} entries must be unique") return values + + +def validate_resource_measurement_requirements( + requirements: Iterable[object], +) -> None: + values = tuple(requirements) + if any(not isinstance(item, ParticipantResourceMeasurementRequirementModel) for item in values): + raise TypeError( + "resource_measurement_requirements entries must be ParticipantResourceMeasurementRequirementModel" + ) + state_refs = [item.budget_state_ref for item in values] + if len(state_refs) != len(set(state_refs)): + raise ValueError("resource measurement requirement budget_state_ref values must be unique") diff --git a/implementations/python/packages/raes_contracts/runtime_state.py b/implementations/python/packages/raes_contracts/runtime_state.py index 1ab88d1e4..93d635c02 100644 --- a/implementations/python/packages/raes_contracts/runtime_state.py +++ b/implementations/python/packages/raes_contracts/runtime_state.py @@ -87,6 +87,9 @@ class RuntimeSnapshot: participant_control_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) + participant_resource_pool_states: dict[str, dict[str, Any]] = field(default_factory=dict) + participant_resource_budget_events: dict[str, dict[str, Any]] = field(default_factory=dict) shared_state_records: dict[str, dict[str, Any]] = field(default_factory=dict) shared_state_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) joint_action_records: dict[str, dict[str, Any]] = field(default_factory=dict) @@ -104,6 +107,11 @@ def __post_init__(self) -> None: if map_key != entry.address: raise ValueError("RuntimeSnapshot entries map key must equal embedded address") require_participant_autonomous_state_snapshot(self.participant_autonomous_execution_states) + _require_participant_resource_budget_snapshot( + self.participant_resource_budget_states, + self.participant_resource_pool_states, + self.participant_resource_budget_events, + ) def get(self, address: str) -> SnapshotEntry | None: return self.entries.get(address) @@ -166,6 +174,21 @@ def with_entries( "participant_execution_services", self.participant_execution_services, ), + participant_resource_budget_states=_mapping_update( + updates, + "participant_resource_budget_states", + self.participant_resource_budget_states, + ), + participant_resource_pool_states=_mapping_update( + updates, + "participant_resource_pool_states", + self.participant_resource_pool_states, + ), + participant_resource_budget_events=_mapping_update( + updates, + "participant_resource_budget_events", + self.participant_resource_budget_events, + ), shared_state_records=_mapping_update( updates, "shared_state_records", @@ -217,6 +240,9 @@ def with_entries( "participant_control_history", "participant_autonomous_execution_states", "participant_execution_services", + "participant_resource_budget_states", + "participant_resource_pool_states", + "participant_resource_budget_events", "shared_state_records", "shared_state_history", "joint_action_records", @@ -228,6 +254,31 @@ def with_entries( } +def _require_participant_resource_budget_snapshot( + states: Mapping[str, Mapping[str, Any]], + pools: Mapping[str, Mapping[str, Any]], + events: Mapping[str, Mapping[str, Any]], +) -> None: + from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceBudgetEventModel, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolStateModel, + ) + + for state_ref, payload in states.items(): + state = ParticipantResourceBudgetStateModel.model_validate(payload) + if state_ref != state.state_ref: + raise ValueError("participant resource-budget state key must equal state_ref") + for pool_state_ref, payload in pools.items(): + pool = ParticipantResourcePoolStateModel.model_validate(payload) + if pool_state_ref != pool.pool_state_ref: + raise ValueError("participant resource-pool state key must equal pool_state_ref") + for event_id, payload in events.items(): + event = ParticipantResourceBudgetEventModel.model_validate(payload) + if event_id != event.event_id: + raise ValueError("participant resource-budget event key must equal event_id") + + def _validate_snapshot_update_keys(updates: Mapping[str, object]) -> None: unknown = sorted(key for key in updates if key not in _SNAPSHOT_UPDATE_KEYS) if unknown: diff --git a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py index 7ded3980e..3bd5d7571 100644 --- a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py +++ b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py @@ -5,18 +5,158 @@ from ..models import ( ParticipantAutonomousExecutionRuntime, ParticipantExecutionBindingRuntime, + ParticipantResourceDemandRuntime, + ParticipantResourceFairnessRuntime, + ParticipantResourceOwnerRuntime, ) from .addresses import ( _action_contract_address, _behavior_specification_address, _objective_address, _observation_boundary_address, + _resolve_node_service_ref, _section_ref_name, ) from .alias_index import _runtime_addressable_ref_index, _runtime_addresses_for_refs from .support import _address, _dump +def _resource_owner_address( + scenario: InstantiatedScenario, + *, + kind: str, + ref: str, + participant_addresses: tuple[str, ...], +) -> str: + if kind == "participant": + matching = tuple(address for address in participant_addresses if address.endswith(f".{ref}")) + if len(matching) != 1: + raise ValueError("participant resource owner must resolve to one policy participant") + return matching[0] + if kind == "deployment_tenant": + name = _section_ref_name(ref, "deployment_tenants", scenario.deployment_tenants) + return _address("deployment", "tenant", name) + if kind == "shared_service": + resolved = _resolve_node_service_ref(scenario, ref) + if resolved is None: + raise ValueError("shared-service resource owner must resolve to one node service") + return _address("provision", "node", resolved[0], "service", resolved[1]) + return ref + + +def _legacy_resource_demands( + policy: object, + participant_addresses: tuple[str, ...], +) -> tuple[ + tuple[ParticipantResourceOwnerRuntime, ...], + tuple[ParticipantResourceDemandRuntime, ...], + ParticipantResourceFairnessRuntime, +]: + owner_address = participant_addresses[0] + owner = ParticipantResourceOwnerRuntime( + owner_id="legacy-participant", + kind="participant", + address=owner_address, + ) + demands = ( + ParticipantResourceDemandRuntime( + budget_id="legacy-action-rate", + owner_id=owner.owner_id, + owner_kind=owner.kind, + owner_address=owner.address, + pool_ref="legacy-participant", + resource_kind="action_rate", + unit="actions", + accounting_mode="windowed_counter", + meter_profile_ref="raes.action-attempt/v1", + limit=policy.max_action_attempts, + reservation=1, + reset="time_segment", + window_ticks=policy.max_action_attempts, + provenance="legacy_maximum", + ), + ParticipantResourceDemandRuntime( + budget_id="legacy-concurrent-actions", + owner_id=owner.owner_id, + owner_kind=owner.kind, + owner_address=owner.address, + pool_ref="legacy-participant", + resource_kind="concurrent_actions", + unit="actions", + accounting_mode="reservable_gauge", + meter_profile_ref="raes.concurrent-action/v1", + limit=policy.max_in_flight, + reservation=1, + reset="reconciled", + provenance="legacy_maximum", + ), + ) + return (owner,), demands, ParticipantResourceFairnessRuntime() + + +def _compiled_resource_budget( + scenario: InstantiatedScenario, + policy: object, + participant_addresses: tuple[str, ...], +) -> tuple[ + tuple[ParticipantResourceOwnerRuntime, ...], + tuple[ParticipantResourceDemandRuntime, ...], + ParticipantResourceFairnessRuntime, +]: + authored = getattr(policy, "resource_budget", None) + if authored is None: + return _legacy_resource_demands(policy, participant_addresses) + owners = tuple( + ParticipantResourceOwnerRuntime( + owner_id=str(owner_id), + kind=owner.kind.value, + address=_resource_owner_address( + scenario, + kind=owner.kind.value, + ref=owner.ref, + participant_addresses=participant_addresses, + ), + ) + for owner_id, owner in sorted(authored.owners.items()) + ) + owner_by_id = {owner.owner_id: owner for owner in owners} + demands = tuple( + ParticipantResourceDemandRuntime( + budget_id=str(budget_id), + owner_id=str(dimension.owner_ref), + owner_kind=owner_by_id[str(dimension.owner_ref)].kind, + owner_address=owner_by_id[str(dimension.owner_ref)].address, + pool_ref=dimension.pool_ref, + resource_kind=dimension.resource_kind.value, + unit=dimension.unit, + accounting_mode=dimension.accounting_mode.value, + meter_profile_ref=dimension.meter_profile_ref, + limit=dimension.limit, + reservation=dimension.reservation, + reset=dimension.reset.value, + window_ticks=dimension.window_ticks, + parent_budget_ref=(str(dimension.parent_budget_ref) if dimension.parent_budget_ref is not None else None), + evidence_refs=tuple(dimension.evidence_refs), + ) + for budget_id, dimension in sorted(authored.dimensions.items()) + ) + fairness = authored.fairness + return ( + owners, + demands, + ParticipantResourceFairnessRuntime( + policy=fairness.policy, + priority_class=fairness.priority_class, + weight=fairness.weight, + protected=fairness.protected, + borrowing=fairness.borrowing, + reclaim=fairness.reclaim, + max_queue_ticks=fairness.max_queue_ticks, + starvation_bound_ticks=fairness.starvation_bound_ticks, + ), + ) + + def _compile_autonomous_execution( *, scenario: InstantiatedScenario, @@ -41,7 +181,7 @@ def _compile_autonomous_execution( pause_window_refs = list(getattr(policy, "pause_window_refs", ())) temporal_constraint_refs = ( [*work_window_refs, *pause_window_refs] - if profile == "participant-autonomous-execution/v2" + if profile in {"participant-autonomous-execution/v2", "participant-autonomous-execution/v3"} else list(policy.temporal_constraint_refs) ) addressable_ref_index = _runtime_addressable_ref_index(scenario) @@ -76,6 +216,11 @@ def _compile_autonomous_execution( target_addresses = tuple( dict.fromkeys(target for binding in execution_bindings for target in binding.target_addresses) ) + resource_owners, resource_demands, resource_fairness = _compiled_resource_budget( + scenario, + policy, + participant_addresses, + ) return ParticipantAutonomousExecutionRuntime( address=address, name=spec_name, @@ -159,6 +304,9 @@ def _compile_autonomous_execution( action_candidate_cooldown_ticks=tuple(candidate.cooldown_ticks for _, candidate in ordered_candidates), max_occurrences=int(getattr(policy, "max_occurrences", 0)), max_burst_size=int(getattr(policy, "max_burst_size", 1)), + resource_owners=resource_owners, + resource_demands=resource_demands, + resource_fairness=resource_fairness, refresh_dependencies=( *participant_addresses, *tuple( diff --git a/implementations/python/packages/raes_processor/models/__init__.py b/implementations/python/packages/raes_processor/models/__init__.py index 652fbe2a4..30dd1caf3 100644 --- a/implementations/python/packages/raes_processor/models/__init__.py +++ b/implementations/python/packages/raes_processor/models/__init__.py @@ -119,6 +119,9 @@ ParticipantInteractiveAccessRuntime, ParticipantObservationBoundaryRuntime, ParticipantOutcomeInterpretationRuleRuntime, + ParticipantResourceDemandRuntime, + ParticipantResourceFairnessRuntime, + ParticipantResourceOwnerRuntime, ParticipantToolAffordanceRuntime, ScriptRuntime, StoryRuntime, @@ -283,6 +286,9 @@ "ParticipantInjectDeliveryRuntime", "ParticipantAutonomousExecutionRuntime", "ParticipantExecutionBindingRuntime", + "ParticipantResourceDemandRuntime", + "ParticipantResourceFairnessRuntime", + "ParticipantResourceOwnerRuntime", "ParticipantInteractiveAccessRuntime", "ParticipantEpisodeControlAction", "ParticipantEpisodeExecutionState", diff --git a/implementations/python/packages/raes_processor/models/behavior_resources.py b/implementations/python/packages/raes_processor/models/behavior_resources.py index 660eabc48..bb77a1b83 100644 --- a/implementations/python/packages/raes_processor/models/behavior_resources.py +++ b/implementations/python/packages/raes_processor/models/behavior_resources.py @@ -10,6 +10,11 @@ from raes_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION from raes_contracts.workflow import WorkflowExecutionContract, WorkflowResultContract, WorkflowStepOutcome +from .participant_resources import ( + ParticipantResourceDemandRuntime, + ParticipantResourceFairnessRuntime, + ParticipantResourceOwnerRuntime, +) from .resources import ResolvedResource @@ -133,6 +138,9 @@ class ParticipantAutonomousExecutionRuntime(ResolvedResource): action_candidate_cooldown_ticks: tuple[int, ...] = () max_occurrences: int = 0 max_burst_size: int = 1 + resource_owners: tuple[ParticipantResourceOwnerRuntime, ...] = () + resource_demands: tuple[ParticipantResourceDemandRuntime, ...] = () + resource_fairness: ParticipantResourceFairnessRuntime = field(default_factory=ParticipantResourceFairnessRuntime) @dataclass(frozen=True) diff --git a/implementations/python/packages/raes_processor/models/participant_resources.py b/implementations/python/packages/raes_processor/models/participant_resources.py new file mode 100644 index 000000000..ab4a2cdbd --- /dev/null +++ b/implementations/python/packages/raes_processor/models/participant_resources.py @@ -0,0 +1,55 @@ +"""Canonical runtime models for participant resource governance.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ParticipantResourceOwnerRuntime: + """Canonical owner identity for participant resource accounting.""" + + owner_id: str + kind: str + address: str + + +@dataclass(frozen=True) +class ParticipantResourceDemandRuntime: + """One canonical resource dimension admitted and enforced at runtime.""" + + budget_id: str + owner_id: str + owner_kind: str + owner_address: str + pool_ref: str + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + limit: int + reservation: int + reset: str + window_ticks: int | None = None + parent_budget_ref: str | None = None + evidence_refs: tuple[str, ...] = () + provenance: str = "authored" + + +@dataclass(frozen=True) +class ParticipantResourceFairnessRuntime: + """Required scheduling/fairness behavior for one resource vector.""" + + policy: str = "legacy_bounded" + priority_class: str = "standard" + weight: int = 1 + protected: bool = False + borrowing: str = "none" + reclaim: str = "none" + max_queue_ticks: int = 0 + starvation_bound_ticks: int = 1 + + +__all__ = [ + "ParticipantResourceDemandRuntime", + "ParticipantResourceFairnessRuntime", + "ParticipantResourceOwnerRuntime", +] 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 30caabe38..5ffe129f4 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_models.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_models.py @@ -166,6 +166,9 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "participant_control_history": dict(snapshot.participant_control_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), + "participant_resource_pool_states": dict(snapshot.participant_resource_pool_states), + "participant_resource_budget_events": dict(snapshot.participant_resource_budget_events), "shared_state_records": dict(snapshot.shared_state_records), "shared_state_history": dict(snapshot.shared_state_history), "joint_action_records": dict(snapshot.joint_action_records), diff --git a/implementations/python/packages/raes_runtime/control_plane_store.py b/implementations/python/packages/raes_runtime/control_plane_store.py index 13326329e..7a3fe6258 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store.py +++ b/implementations/python/packages/raes_runtime/control_plane_store.py @@ -135,6 +135,9 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: }, "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), + "participant_resource_pool_states": dict(snapshot.participant_resource_pool_states), + "participant_resource_budget_events": dict(snapshot.participant_resource_budget_events), "shared_state_records": dict(snapshot.shared_state_records), "shared_state_history": { state_address: list(records) for state_address, records in snapshot.shared_state_history.items() @@ -202,6 +205,9 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: }, 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", {})), + participant_resource_pool_states=dict(payload.get("participant_resource_pool_states", {})), + participant_resource_budget_events=dict(payload.get("participant_resource_budget_events", {})), shared_state_records=dict(payload.get("shared_state_records", {})), shared_state_history={ state_address: list(records) for state_address, records in payload.get("shared_state_history", {}).items() diff --git a/implementations/python/packages/raes_runtime/participant_activity_support.py b/implementations/python/packages/raes_runtime/participant_activity_support.py index fa83d97dc..66be116da 100644 --- a/implementations/python/packages/raes_runtime/participant_activity_support.py +++ b/implementations/python/packages/raes_runtime/participant_activity_support.py @@ -100,7 +100,7 @@ def _activity_provenance( ) return ParticipantActivityOccurrenceProvenanceModel( policy_address=context.policy.address, - policy_profile="participant-autonomous-execution/v2", + policy_profile=context.policy.profile, occurrence_id=occurrence_id, attempt_id=request.action_instance_id, predecessor_attempt_id=predecessor, diff --git a/implementations/python/packages/raes_runtime/participant_execution_control.py b/implementations/python/packages/raes_runtime/participant_execution_control.py index 762cffdc4..8471ec18a 100644 --- a/implementations/python/packages/raes_runtime/participant_execution_control.py +++ b/implementations/python/packages/raes_runtime/participant_execution_control.py @@ -81,6 +81,11 @@ def _apply_participant_execution_phase( self._target.participant_runtime, state.working_snapshot, self._participant_activity_controls, + ( + self._target.manifest.participant_runtime.resource_budgets + if self._target.manifest.participant_runtime is not None + else None + ), ) self._record_phase_result(state, result) if not result.success: diff --git a/implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py b/implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py index 745813332..ad5e124f9 100644 --- a/implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py +++ b/implementations/python/packages/raes_runtime/participant_execution_scheduler_state.py @@ -8,6 +8,7 @@ from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel +from raes_contracts.contracts.participant_resource_budgets import participant_resource_budget_state_ref from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime @@ -57,6 +58,14 @@ def execution_service_state( scheduler_state_refs=tuple( f"{policy.address}.state.{participant_address}" for participant_address in policy.participant_addresses ), + resource_budget_state_refs=( + tuple( + participant_resource_budget_state_ref(policy.address, demand.budget_id) + for demand in policy.resource_demands + ) + if policy.profile == "participant-autonomous-execution/v3" + else () + ), capacity=policy.max_in_flight, reserved=0, in_flight=0, diff --git a/implementations/python/packages/raes_runtime/participant_resource_accounting.py b/implementations/python/packages/raes_runtime/participant_resource_accounting.py new file mode 100644 index 000000000..4f76b5097 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_resource_accounting.py @@ -0,0 +1,371 @@ +"""Commit, release, and reconciliation for participant resource reservations.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceBudgetEventModel, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolStateModel, + participant_resource_pool_state_ref, +) +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot + +from .participant_resource_pool_ledger import ( + commit as commit_pool_allocation, +) +from .participant_resource_pool_ledger import ( + reconcile as reconcile_pool_allocation, +) +from .participant_resource_pool_ledger import ( + release as release_pool_allocation, +) + + +def _diagnostic(code: str, policy_address: str, message: str) -> Diagnostic: + return Diagnostic( + code=code, + domain="participant-runtime", + address=f"/participant_resource_budgets/{policy_address}", + message=message, + ) + + +def _state(payload: Mapping[str, object]) -> ParticipantResourceBudgetStateModel: + return ParticipantResourceBudgetStateModel.model_validate(payload) + + +def _event(payload: Mapping[str, object]) -> ParticipantResourceBudgetEventModel: + return ParticipantResourceBudgetEventModel.model_validate(payload) + + +def _pool_state(payload: Mapping[str, object]) -> ParticipantResourcePoolStateModel: + return ParticipantResourcePoolStateModel.model_validate(payload) + + +def _payload( + model: ParticipantResourceBudgetStateModel + | ParticipantResourcePoolStateModel + | ParticipantResourceBudgetEventModel, +) -> dict[str, object]: + return model.model_dump(mode="json") + + +def _pool_ref_for_state(state: ParticipantResourceBudgetStateModel) -> str: + return participant_resource_pool_state_ref( + pool_ref=state.pool_ref, + owner_kind=state.owner_kind, + owner_ref=state.owner_ref, + resource_kind=state.resource_kind, + unit=state.unit, + accounting_mode=state.accounting_mode, + meter_profile_ref=state.meter_profile_ref, + ) + + +def _operation_reservations( + events: Mapping[str, Mapping[str, object]], + operation_id: str, +) -> tuple[ParticipantResourceBudgetEventModel, ...]: + return tuple( + _event(payload) + for payload in events.values() + if payload.get("operation_id") == operation_id and payload.get("transition") == "reserve" + ) + + +def commit_participant_resource_reservation( + snapshot: RuntimeSnapshot, + *, + operation_id: str, + execution_generation: int, + measured_quantities: Mapping[str, int], + evidence_refs: tuple[str, ...], +) -> ApplyResult: + """Commit trusted, complete measurements for an operation exactly once.""" + + events = dict(snapshot.participant_resource_budget_events) + reservations = _operation_reservations(events, operation_id) + if not reservations: + committed = any( + payload.get("operation_id") == operation_id and payload.get("transition") == "commit" + for payload in events.values() + ) + return ApplyResult( + success=committed, + snapshot=snapshot, + diagnostics=( + [] + if committed + else [ + _diagnostic( + "runtime.participant-resource-reservation-missing", + "unknown", + f"operation {operation_id} has no resource reservation", + ) + ] + ), + ) + expected_refs = {reservation.budget_state_ref for reservation in reservations} + valid_vector = ( + set(measured_quantities) == expected_refs + and bool(evidence_refs) + and all( + 0 <= measured_quantities[reservation.budget_state_ref] <= reservation.requested + for reservation in reservations + ) + ) + if not valid_vector: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-measurement-invalid", + reservations[0].policy_address, + "resource commit requires an exact, bounded measured vector and native evidence", + ) + ], + ) + states = dict(snapshot.participant_resource_budget_states) + pool_states = dict(snapshot.participant_resource_pool_states) + for reservation in reservations: + commit_id = f"{operation_id}:{reservation.budget_state_ref}:commit" + if commit_id in events: + continue + current = _state(states[reservation.budget_state_ref]) + if current.generation != execution_generation: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-stale-generation", + reservation.policy_address, + f"operation {operation_id} cannot commit across a generation boundary", + ) + ], + ) + measured = measured_quantities[reservation.budget_state_ref] + committed = current.model_copy( + update={ + "reserved": max(0, current.reserved - reservation.requested), + "cumulative_use": current.cumulative_use + measured, + "last_event_ref": commit_id, + "evidence_refs": tuple(dict.fromkeys((*current.evidence_refs, *evidence_refs))), + } + ) + event = ParticipantResourceBudgetEventModel( + event_id=commit_id, + operation_id=operation_id, + budget_state_ref=reservation.budget_state_ref, + budget_id=reservation.budget_id, + policy_address=reservation.policy_address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=execution_generation, + transition="commit", + disposition="committed", + requested=reservation.requested, + measured=measured, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=reservation.event_id, + evidence_refs=evidence_refs, + ) + states[reservation.budget_state_ref] = _payload(committed) + physical_pool_ref = _pool_ref_for_state(current) + physical_pool = _pool_state(pool_states[physical_pool_ref]) + pool_states[physical_pool_ref] = _payload( + commit_pool_allocation( + physical_pool, + current.state_ref, + reserved=reservation.requested, + measured=measured, + ) + ) + events[commit_id] = _payload(event) + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_resource_budget_states=states, + participant_resource_pool_states=pool_states, + participant_resource_budget_events=events, + ), + ) + + +def release_participant_resource_reservation( + snapshot: RuntimeSnapshot, + *, + operation_id: str, + execution_generation: int, + evidence_refs: tuple[str, ...], +) -> ApplyResult: + """Release an uncommitted complete vector after failed or untrusted execution.""" + + events = dict(snapshot.participant_resource_budget_events) + reservations = _operation_reservations(events, operation_id) + if not reservations: + return ApplyResult(success=True, snapshot=snapshot) + states = dict(snapshot.participant_resource_budget_states) + pool_states = dict(snapshot.participant_resource_pool_states) + for reservation in reservations: + release_id = f"{operation_id}:{reservation.budget_state_ref}:release" + if release_id in events: + continue + current = _state(states[reservation.budget_state_ref]) + if current.generation != execution_generation: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-stale-generation", + reservation.policy_address, + f"operation {operation_id} cannot release across a generation boundary", + ) + ], + ) + released = current.model_copy( + update={ + "reserved": max(0, current.reserved - reservation.requested), + "last_event_ref": release_id, + "evidence_refs": tuple(dict.fromkeys((*current.evidence_refs, *evidence_refs))), + } + ) + event = ParticipantResourceBudgetEventModel( + event_id=release_id, + operation_id=operation_id, + budget_state_ref=reservation.budget_state_ref, + budget_id=reservation.budget_id, + policy_address=reservation.policy_address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=execution_generation, + transition="release", + disposition="released", + requested=reservation.requested, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=reservation.event_id, + evidence_refs=evidence_refs, + ) + states[current.state_ref] = _payload(released) + physical_pool_ref = _pool_ref_for_state(current) + physical_pool = _pool_state(pool_states[physical_pool_ref]) + pool_states[physical_pool_ref] = _payload( + release_pool_allocation(physical_pool, current.state_ref, reservation.requested) + ) + events[release_id] = _payload(event) + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_resource_budget_states=states, + participant_resource_pool_states=pool_states, + participant_resource_budget_events=events, + ), + ) + + +def reconcile_participant_resource_budgets( + snapshot: RuntimeSnapshot, + *, + policy_address: str, + current_generation: int, + next_generation: int, + boundary: str, + evidence_refs: tuple[str, ...] = (), +) -> ApplyResult: + """Fence a reset generation and reconcile only dimensions owned by its boundary.""" + + if next_generation <= current_generation: + raise ValueError("resource-budget reconciliation must advance generation") + states = dict(snapshot.participant_resource_budget_states) + pool_states = dict(snapshot.participant_resource_pool_states) + events = dict(snapshot.participant_resource_budget_events) + selected = [ + (state_ref, _state(payload)) + for state_ref, payload in states.items() + if payload.get("policy_address") == policy_address + ] + for state_ref, current in selected: + if current.generation != current_generation: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-stale-generation", + policy_address, + f"resource budget {state_ref} cannot reconcile from generation {current_generation}", + ) + ], + ) + for state_ref, current in selected: + event_id = f"reconcile:{policy_address}:{next_generation}:{state_ref}" + clears = current.reset == boundary + reconciled = current.model_copy( + update={ + "generation": next_generation, + "reserved": 0, + "current_use": 0 if clears else current.current_use, + "cumulative_use": 0 if clears else current.cumulative_use, + "reconciliation_status": "reconciled", + "last_event_ref": event_id, + "evidence_refs": tuple(dict.fromkeys((*current.evidence_refs, *evidence_refs))), + } + ) + event = ParticipantResourceBudgetEventModel( + event_id=event_id, + operation_id=f"reconcile:{policy_address}:{next_generation}", + budget_state_ref=state_ref, + budget_id=current.budget_id, + policy_address=policy_address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=next_generation, + transition="reconcile", + disposition="reconciled", + requested=0, + measured=0, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=current.last_event_ref, + evidence_refs=evidence_refs, + ) + states[state_ref] = _payload(reconciled) + physical_pool_ref = _pool_ref_for_state(current) + physical_pool = _pool_state(pool_states[physical_pool_ref]) + pool_states[physical_pool_ref] = _payload( + reconcile_pool_allocation( + physical_pool, + current, + generation=next_generation, + clears=clears, + ) + ) + events[event_id] = _payload(event) + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_resource_budget_states=states, + participant_resource_pool_states=pool_states, + participant_resource_budget_events=events, + ), + ) + + +__all__ = ( + "commit_participant_resource_reservation", + "reconcile_participant_resource_budgets", + "release_participant_resource_reservation", +) diff --git a/implementations/python/packages/raes_runtime/participant_resource_budgets.py b/implementations/python/packages/raes_runtime/participant_resource_budgets.py new file mode 100644 index 000000000..9a26b548a --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_resource_budgets.py @@ -0,0 +1,464 @@ +"""Atomic initialization and reservation of participant resource budgets.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Protocol + +from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceBudgetEventModel, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolStateModel, + participant_resource_budget_state_ref, + participant_resource_pool_state_ref, +) +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot + +from .participant_resource_pool_ledger import ( + can_reserve as pool_can_reserve, +) +from .participant_resource_pool_ledger import ( + ensure_allocation, + new_pool_state, + pool_state_ref, +) +from .participant_resource_pool_ledger import ( + reserve as reserve_pool_allocation, +) + + +class ResourceDemand(Protocol): + budget_id: str + owner_kind: str + owner_address: str + pool_ref: str + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + limit: int + reservation: int + reset: str + + +class ResourceFairness(Protocol): + priority_class: str + weight: int + protected: bool + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + + +class ResourcePolicy(Protocol): + address: str + resource_demands: tuple[ResourceDemand, ...] + resource_fairness: ResourceFairness + + +class ResourcePool(Protocol): + pool_ref: str + owner_kind: str + owner_ref: str + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + capacity: int + protected_capacity: int + fairness_policy: str + priority_classes: tuple[str, ...] + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + + +class ResourceCapabilities(Protocol): + configured_pools: tuple[ResourcePool, ...] + + +def _diagnostic(code: str, policy_address: str, message: str) -> Diagnostic: + return Diagnostic( + code=code, + domain="participant-runtime", + address=f"/participant_resource_budgets/{policy_address}", + message=message, + ) + + +def _state(payload: Mapping[str, object]) -> ParticipantResourceBudgetStateModel: + return ParticipantResourceBudgetStateModel.model_validate(payload) + + +def _pool_state(payload: Mapping[str, object]) -> ParticipantResourcePoolStateModel: + return ParticipantResourcePoolStateModel.model_validate(payload) + + +def _payload( + model: ParticipantResourceBudgetStateModel + | ParticipantResourcePoolStateModel + | ParticipantResourceBudgetEventModel, +) -> dict[str, object]: + return model.model_dump(mode="json") + + +def _matching_pool(demand: ResourceDemand, capabilities: ResourceCapabilities) -> ResourcePool | None: + return next( + ( + pool + for pool in capabilities.configured_pools + if pool.pool_ref == demand.pool_ref + and pool.owner_kind == demand.owner_kind + and pool.owner_ref == demand.owner_address + and pool.resource_kind == demand.resource_kind + and pool.unit == demand.unit + and pool.accounting_mode == demand.accounting_mode + and pool.meter_profile_ref == demand.meter_profile_ref + ), + None, + ) + + +def initialize_participant_resource_budgets( + snapshot: RuntimeSnapshot, + policies: Sequence[ResourcePolicy], + capabilities: ResourceCapabilities, + *, + execution_generation: int, +) -> ApplyResult: + """Materialize policy budgets and authoritative physical-pool allocations.""" + + states = dict(snapshot.participant_resource_budget_states) + pool_states = dict(snapshot.participant_resource_pool_states) + for policy in policies: + for demand in policy.resource_demands: + pool = _matching_pool(demand, capabilities) + if pool is None: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-capacity-missing", + policy.address, + f"no exact configured capacity matches resource budget {demand.budget_id}", + ) + ], + ) + if policy.resource_fairness.protected and pool.protected_capacity < demand.reservation: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-protected-capacity-missing", + policy.address, + f"resource budget {demand.budget_id} lacks its protected reservation", + ) + ], + ) + state_ref = participant_resource_budget_state_ref(policy.address, demand.budget_id) + existing = states.get(state_ref) + if existing is not None: + current = _state(existing) + if current.generation != execution_generation: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-state-conflict", + policy.address, + f"resource budget {demand.budget_id} already has incompatible state", + ) + ], + ) + continue + budget_state = ParticipantResourceBudgetStateModel( + state_ref=state_ref, + budget_id=demand.budget_id, + policy_address=policy.address, + owner_kind=demand.owner_kind, + owner_ref=demand.owner_address, + pool_ref=demand.pool_ref, + resource_kind=demand.resource_kind, + unit=demand.unit, + accounting_mode=demand.accounting_mode, + meter_profile_ref=demand.meter_profile_ref, + reset=demand.reset, + generation=execution_generation, + limit=demand.limit, + configured_capacity=pool.capacity, + reserved=0, + current_use=0, + cumulative_use=0, + throttled=0, + rejected=0, + reconciliation_status="reconciled", + last_event_ref=f"initial:{state_ref}", + ) + states[state_ref] = _payload(budget_state) + exact_pool_ref = pool_state_ref(pool) + existing_pool = pool_states.get(exact_pool_ref) + physical_pool = new_pool_state(pool) if existing_pool is None else _pool_state(existing_pool) + try: + physical_pool = ensure_allocation( + physical_pool, + budget_state, + fairness=policy.resource_fairness, + ) + except ValueError as exc: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-pool-conflict", + policy.address, + str(exc), + ) + ], + ) + pool_states[exact_pool_ref] = _payload(physical_pool) + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_resource_budget_states=states, + participant_resource_pool_states=pool_states, + ), + ) + + +def _reservation_event_id(operation_id: str, state_ref: str) -> str: + return f"{operation_id}:{state_ref}:reserve" + + +def _used_capacity(state: ParticipantResourceBudgetStateModel) -> int: + if state.accounting_mode in {"reservable_gauge", "lease"}: + return state.current_use + state.reserved + return state.cumulative_use + state.reserved + + +def _pool_ref_for_state(state: ParticipantResourceBudgetStateModel) -> str: + return participant_resource_pool_state_ref( + pool_ref=state.pool_ref, + owner_kind=state.owner_kind, + owner_ref=state.owner_ref, + resource_kind=state.resource_kind, + unit=state.unit, + accounting_mode=state.accounting_mode, + meter_profile_ref=state.meter_profile_ref, + ) + + +def _throttled_result( + snapshot: RuntimeSnapshot, + states: dict[str, dict[str, object]], + pool_states: dict[str, dict[str, object]], + events: dict[str, dict[str, object]], + *, + policy: ResourcePolicy, + demand: ResourceDemand, + current: ParticipantResourceBudgetStateModel, + operation_id: str, + execution_generation: int, + amount: int, + budget_available: bool, +) -> ApplyResult: + event_id = f"{operation_id}:{current.state_ref}:throttle" + states[current.state_ref] = _payload( + current.model_copy( + update={ + "throttled": current.throttled + 1, + "last_event_ref": event_id, + } + ) + ) + events[event_id] = _payload( + ParticipantResourceBudgetEventModel( + event_id=event_id, + operation_id=operation_id, + budget_state_ref=current.state_ref, + budget_id=demand.budget_id, + policy_address=policy.address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=execution_generation, + transition="throttle", + disposition="throttled", + requested=amount, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=current.last_event_ref, + ) + ) + return ApplyResult( + success=False, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_resource_budget_states=states, + participant_resource_pool_states=pool_states, + participant_resource_budget_events=events, + ), + diagnostics=[ + _diagnostic( + "runtime.participant-resource-throttled", + policy.address, + ( + f"resource budget {demand.budget_id} has insufficient " + f"{'logical budget' if not budget_available else 'shared pool'} capacity" + ), + ) + ], + ) + + +def reserve_participant_resources( + snapshot: RuntimeSnapshot, + policy: ResourcePolicy, + *, + operation_id: str, + execution_generation: int, + requested_quantities: Mapping[str, int] | None = None, +) -> ApplyResult: + """Reserve a policy's complete resource vector or reserve none of it.""" + + events = dict(snapshot.participant_resource_budget_events) + state_refs = [ + participant_resource_budget_state_ref(policy.address, demand.budget_id) for demand in policy.resource_demands + ] + event_ids = [_reservation_event_id(operation_id, state_ref) for state_ref in state_refs] + if event_ids and all(event_id in events for event_id in event_ids): + return ApplyResult(success=True, snapshot=snapshot) + states = dict(snapshot.participant_resource_budget_states) + pool_states = dict(snapshot.participant_resource_pool_states) + checked: list[ + tuple[ + ResourceDemand, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolStateModel, + int, + ] + ] = [] + for demand, state_ref in zip(policy.resource_demands, state_refs, strict=True): + raw = states.get(state_ref) + if raw is None: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-state-missing", + policy.address, + f"resource budget {demand.budget_id} was not initialized", + ) + ], + ) + current = _state(raw) + if current.generation != execution_generation: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-stale-generation", + policy.address, + ( + f"resource budget {demand.budget_id} is generation {current.generation}; " + f"request is generation {execution_generation}" + ), + ) + ], + ) + amount = ( + requested_quantities.get(demand.budget_id, demand.reservation) + if requested_quantities is not None + else demand.reservation + ) + if amount < 0: + raise ValueError("requested participant resource quantities must be non-negative") + exact_pool_ref = _pool_ref_for_state(current) + raw_pool = pool_states.get(exact_pool_ref) + if raw_pool is None: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-pool-state-missing", + policy.address, + f"physical pool for resource budget {demand.budget_id} was not initialized", + ) + ], + ) + physical_pool = _pool_state(raw_pool) + budget_available = _used_capacity(current) + amount <= min( + current.limit, + current.configured_capacity, + ) + if not budget_available or not pool_can_reserve(physical_pool, state_ref, amount): + return _throttled_result( + snapshot, + states, + pool_states, + events, + policy=policy, + demand=demand, + current=current, + operation_id=operation_id, + execution_generation=execution_generation, + amount=amount, + budget_available=budget_available, + ) + checked.append((demand, current, physical_pool, amount)) + for demand, current, physical_pool, amount in checked: + event_id = _reservation_event_id(operation_id, current.state_ref) + event = ParticipantResourceBudgetEventModel( + event_id=event_id, + operation_id=operation_id, + budget_state_ref=current.state_ref, + budget_id=demand.budget_id, + policy_address=policy.address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=execution_generation, + transition="reserve", + disposition="reserved", + requested=amount, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=current.last_event_ref, + ) + states[current.state_ref] = _payload( + current.model_copy( + update={ + "reserved": current.reserved + amount, + "last_event_ref": event_id, + } + ) + ) + pool_states[physical_pool.pool_state_ref] = _payload( + reserve_pool_allocation(physical_pool, current.state_ref, amount) + ) + events[event_id] = _payload(event) + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_resource_budget_states=states, + participant_resource_pool_states=pool_states, + participant_resource_budget_events=events, + ), + ) + + +__all__ = ( + "initialize_participant_resource_budgets", + "reserve_participant_resources", +) diff --git a/implementations/python/packages/raes_runtime/participant_resource_pool_ledger.py b/implementations/python/packages/raes_runtime/participant_resource_pool_ledger.py new file mode 100644 index 000000000..8eac84124 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_resource_pool_ledger.py @@ -0,0 +1,201 @@ +"""Authoritative physical-pool allocation operations for participant budgets.""" + +from __future__ import annotations + +from typing import Protocol + +from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolAllocationModel, + ParticipantResourcePoolStateModel, + participant_resource_pool_state_ref, +) + + +class ResourcePool(Protocol): + pool_ref: str + owner_kind: str + owner_ref: str + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + capacity: int + protected_capacity: int + fairness_policy: str + priority_classes: tuple[str, ...] + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + + +class ResourceFairness(Protocol): + priority_class: str + weight: int + protected: bool + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + + +def pool_state_ref(pool: ResourcePool) -> str: + return participant_resource_pool_state_ref( + pool_ref=pool.pool_ref, + owner_kind=pool.owner_kind, + owner_ref=pool.owner_ref, + resource_kind=pool.resource_kind, + unit=pool.unit, + accounting_mode=pool.accounting_mode, + meter_profile_ref=pool.meter_profile_ref, + ) + + +def new_pool_state(pool: ResourcePool) -> ParticipantResourcePoolStateModel: + return ParticipantResourcePoolStateModel( + pool_state_ref=pool_state_ref(pool), + pool_ref=pool.pool_ref, + owner_kind=pool.owner_kind, + owner_ref=pool.owner_ref, + resource_kind=pool.resource_kind, + unit=pool.unit, + accounting_mode=pool.accounting_mode, + meter_profile_ref=pool.meter_profile_ref, + capacity=pool.capacity, + protected_capacity=pool.protected_capacity, + fairness_policy=pool.fairness_policy, + priority_classes=pool.priority_classes, + borrowing=pool.borrowing, + reclaim=pool.reclaim, + max_queue_ticks=pool.max_queue_ticks, + starvation_bound_ticks=pool.starvation_bound_ticks, + ) + + +def ensure_allocation( + pool: ParticipantResourcePoolStateModel, + budget: ParticipantResourceBudgetStateModel, + *, + fairness: ResourceFairness, +) -> ParticipantResourcePoolStateModel: + allocations = dict(pool.allocations) + existing = allocations.get(budget.state_ref) + if existing is not None: + if ( + existing.policy_address != budget.policy_address + or existing.budget_id != budget.budget_id + or existing.protected != fairness.protected + ): + raise ValueError("physical pool allocation conflicts with its canonical budget state") + return pool + allocations[budget.state_ref] = ParticipantResourcePoolAllocationModel( + budget_state_ref=budget.state_ref, + policy_address=budget.policy_address, + budget_id=budget.budget_id, + generation=budget.generation, + priority_class=fairness.priority_class, + weight=fairness.weight, + protected=fairness.protected, + borrowing=fairness.borrowing, + reclaim=fairness.reclaim, + max_queue_ticks=fairness.max_queue_ticks, + starvation_bound_ticks=fairness.starvation_bound_ticks, + reserved=0, + current_use=0, + cumulative_use=0, + ) + return pool.model_copy(update={"allocations": allocations}) + + +def _allocation_use(pool: ParticipantResourcePoolStateModel, allocation: ParticipantResourcePoolAllocationModel) -> int: + if pool.accounting_mode in {"reservable_gauge", "lease"}: + return allocation.current_use + allocation.reserved + return allocation.cumulative_use + allocation.reserved + + +def can_reserve( + pool: ParticipantResourcePoolStateModel, + budget_state_ref: str, + amount: int, +) -> bool: + allocation = pool.allocations[budget_state_ref] + total = sum(_allocation_use(pool, item) for item in pool.allocations.values()) + if total + amount > pool.capacity: + return False + if allocation.protected: + return True + unprotected = sum(_allocation_use(pool, item) for item in pool.allocations.values() if not item.protected) + return unprotected + amount <= pool.capacity - pool.protected_capacity + + +def reserve( + pool: ParticipantResourcePoolStateModel, + budget_state_ref: str, + amount: int, +) -> ParticipantResourcePoolStateModel: + allocations = dict(pool.allocations) + current = allocations[budget_state_ref] + allocations[budget_state_ref] = current.model_copy(update={"reserved": current.reserved + amount}) + return pool.model_copy(update={"allocations": allocations}) + + +def commit( + pool: ParticipantResourcePoolStateModel, + budget_state_ref: str, + *, + reserved: int, + measured: int, +) -> ParticipantResourcePoolStateModel: + allocations = dict(pool.allocations) + current = allocations[budget_state_ref] + allocations[budget_state_ref] = current.model_copy( + update={ + "reserved": max(0, current.reserved - reserved), + "cumulative_use": current.cumulative_use + measured, + } + ) + return pool.model_copy(update={"allocations": allocations}) + + +def release( + pool: ParticipantResourcePoolStateModel, + budget_state_ref: str, + amount: int, +) -> ParticipantResourcePoolStateModel: + allocations = dict(pool.allocations) + current = allocations[budget_state_ref] + allocations[budget_state_ref] = current.model_copy(update={"reserved": max(0, current.reserved - amount)}) + return pool.model_copy(update={"allocations": allocations}) + + +def reconcile( + pool: ParticipantResourcePoolStateModel, + budget: ParticipantResourceBudgetStateModel, + *, + generation: int, + clears: bool, +) -> ParticipantResourcePoolStateModel: + allocations = dict(pool.allocations) + current = allocations[budget.state_ref] + allocations[budget.state_ref] = current.model_copy( + update={ + "generation": generation, + "reserved": 0, + "current_use": 0 if clears else current.current_use, + "cumulative_use": 0 if clears else current.cumulative_use, + } + ) + return pool.model_copy(update={"allocations": allocations}) + + +__all__ = [ + "can_reserve", + "commit", + "ensure_allocation", + "new_pool_state", + "pool_state_ref", + "reconcile", + "release", + "reserve", +] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index 3541f9c1e..ba490d11e 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -26,6 +26,10 @@ reset_execution_service, set_execution_clock_lifecycle, ) +from .participant_resource_accounting import ( + reconcile_participant_resource_budgets, +) +from .participant_resource_budgets import initialize_participant_resource_budgets from .participant_scheduler_operations import ( SchedulerRunState, run_participant_due, @@ -91,7 +95,14 @@ def _initialize_participant( key = _state_key(policy.address, participant_address) segment, _ = clock_coordinate(working, policy.clock_address) activity_control = activity_control_for(policy, activity_controls) - if policy.profile == "participant-autonomous-execution/v2" and activity_control is None: + if ( + policy.profile + in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + } + and activity_control is None + ): return ApplyResult( success=False, snapshot=working, @@ -257,11 +268,39 @@ def initialize( participant_runtime: object, snapshot: RuntimeSnapshot, activity_controls: dict[str, ParticipantActivityRandomControl] | None = None, + resource_capabilities: object | None = None, ) -> ApplyResult: working = snapshot resolved_activity_controls = activity_controls or {} changed: list[str] = [] - for policy in policies: + normalized_policies = tuple(policies) + governed_policies = tuple( + policy for policy in normalized_policies if policy.profile == "participant-autonomous-execution/v3" + ) + if governed_policies: + if resource_capabilities is None: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code="runtime.participant-resource-capabilities-missing", + domain="participant", + address=governed_policies[0].address, + message="Participant execution v3 requires admitted resource-budget capabilities.", + ) + ], + ) + initialized = initialize_participant_resource_budgets( + working, + governed_policies, + resource_capabilities, + execution_generation=0, + ) + if not initialized.success: + return initialized + working = initialized.snapshot + for policy in normalized_policies: for participant_address in policy.participant_addresses: result = _initialize_participant( policy, @@ -371,6 +410,26 @@ def reset_clock( return result working = result.snapshot changed.extend(result.changed_addresses) + if policy.profile == "participant-autonomous-execution/v3": + service_payload = working.participant_execution_services.get(policy.address) + if service_payload is None: + return _missing_execution_service_result( + policy, + SchedulerRunState(working=working, diagnostics=[], changed=changed), + ) + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + generation = service.generation + 1 + budget_reset = reconcile_participant_resource_budgets( + working, + policy_address=policy.address, + current_generation=service.generation, + next_generation=generation, + boundary="time_segment", + evidence_refs=(f"evidence:{policy.address}:shared-time-reset:generation-{generation}",), + ) + if not budget_reset.success: + return budget_reset + working = budget_reset.snapshot working, service_changed = reset_execution_service( working, policy.address, diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py index e1d700031..85c371626 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py @@ -164,7 +164,7 @@ def _due_contexts( cadence_ticks: int, run: SchedulerRunState, ) -> tuple[list[_DueActionContext], list[ParticipantAutonomousExecutionStateModel]]: - from .participant_scheduler_operations import _cadence_missed_result + from .participant_scheduler_time import cadence_missed_result from .participant_scheduler_types import _DueActionContext contexts: list[_DueActionContext] = [] @@ -175,7 +175,7 @@ def _due_contexts( run.working.participant_autonomous_execution_states[key] ) if state.lifecycle_state == "running" and state.next_tick < current_tick: - run.failure = _cadence_missed_result(run.working, key, current_tick, state) + run.failure = cadence_missed_result(run.working, key, current_tick, state) break due = ( state.lifecycle_state == "running" diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py index 789b88c85..801fa2f21 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py @@ -30,33 +30,15 @@ persist_activity_state, ) from .participant_scheduler_concurrency import participant_generation_commit_diagnostic, run_policy_due_concurrently -from .participant_scheduler_time import clock_coordinate, participant_time_domain +from .participant_scheduler_resources import ( + commit_activity_resources, + measurement_requirements, + reserve_activity_resources, +) +from .participant_scheduler_time import cadence_missed_result, clock_coordinate, participant_time_domain from .participant_scheduler_types import SchedulerRunState, _DueActionContext -def _cadence_missed_result( - working: RuntimeSnapshot, - key: str, - current_tick: int, - state: ParticipantAutonomousExecutionStateModel, -) -> ApplyResult: - return ApplyResult( - success=False, - snapshot=working, - diagnostics=[ - Diagnostic( - code="runtime.participant-autonomous-cadence-missed", - domain="participant", - address=key, - message=( - f"Shared clock is at tick {current_tick}, after the next governed " - f"participant cadence tick {state.next_tick}." - ), - ) - ], - ) - - def _bound_action_request( context: _DueActionContext, working: RuntimeSnapshot, @@ -64,7 +46,10 @@ def _bound_action_request( ) -> ParticipantActionAdmissionRequest: policy = context.policy action_address = policy.action_contract_addresses[state.next_action_index % len(policy.action_contract_addresses)] - if policy.profile == "participant-autonomous-execution/v2": + if policy.profile in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + }: action_instance_id = activity_attempt_id( policy_address=policy.address, participant_address=context.participant_address, @@ -122,6 +107,7 @@ def _bound_action_request( target_addresses=matching_bindings[0].target_addresses, execution_scope_ref=policy.address, execution_generation=service.generation, + resource_measurement_requirements=measurement_requirements(policy), ), ) @@ -344,6 +330,8 @@ def _run_one_activity_action( run: SchedulerRunState, ) -> ParticipantAutonomousExecutionStateModel: request = _bound_action_request(context, run.working, state) + if not reserve_activity_resources(context, request, run): + return state predecessor = run.working result = context.participant_runtime.admit_action(request, predecessor) protocol_violation = autonomous_action_result_violation( @@ -353,6 +341,14 @@ def _run_one_activity_action( predecessor=predecessor, ) protocol_failure = _record_protocol_result(run, context, predecessor, result, protocol_violation) + if not commit_activity_resources( + context, + request, + result, + protocol_failure=protocol_failure, + run=run, + ): + return state action_result = getattr(result, "action_result", None) status = getattr(getattr(action_result, "status", None), "value", getattr(action_result, "status", None)) action_succeeded = bool(not protocol_failure and result.success and status == "succeeded") @@ -463,7 +459,7 @@ def run_participant_due( run.working.participant_autonomous_execution_states[key] ) if state.lifecycle_state == "running" and state.next_tick < current_tick: - run.failure = _cadence_missed_result(run.working, key, current_tick, state) + run.failure = cadence_missed_result(run.working, key, current_tick, state) return action_context = _DueActionContext( policy=policy, @@ -475,7 +471,10 @@ def run_participant_due( cadence_ticks=cadence_ticks, activity_control=activity_control_for(policy, activity_controls or {}), ) - if policy.profile == "participant-autonomous-execution/v2": + if policy.profile in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + }: _run_participant_activity_due(action_context, state, run) return action_is_due = ( diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_policy.py b/implementations/python/packages/raes_runtime/participant_scheduler_policy.py index 4a18401cb..af7fbd3aa 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_policy.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_policy.py @@ -45,7 +45,10 @@ def _policy_digest( "resolved_progression_policy": asdict(progression), "resolved_temporal_constraints": constraints, } - if policy.profile == "participant-autonomous-execution/v2": + if policy.profile in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + }: payload.update( { "profile": policy.profile, @@ -66,5 +69,13 @@ def _policy_digest( "max_burst_size": policy.max_burst_size, } ) + if policy.profile == "participant-autonomous-execution/v3": + payload.update( + { + "resource_owners": tuple(asdict(owner) for owner in policy.resource_owners), + "resource_demands": tuple(asdict(demand) for demand in policy.resource_demands), + "resource_fairness": asdict(policy.resource_fairness), + } + ) canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest() diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_resources.py b/implementations/python/packages/raes_runtime/participant_scheduler_resources.py new file mode 100644 index 000000000..2626af4f0 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_resources.py @@ -0,0 +1,158 @@ +"""Resource-governance integration for participant scheduler actions.""" + +from typing import Protocol + +from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceMeasurementRequirementModel, + participant_resource_budget_state_ref, +) +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.participant_binding import ParticipantActionAdmissionRequest, ParticipantActionApplyResult +from raes_contracts.runtime_state import ApplyResult + +from .participant_resource_accounting import ( + commit_participant_resource_reservation, + release_participant_resource_reservation, +) +from .participant_resource_budgets import ( + reserve_participant_resources, +) +from .participant_scheduler_types import SchedulerRunState, _DueActionContext + + +class _MeasurementDemand(Protocol): + budget_id: str + resource_kind: str + unit: str + meter_profile_ref: str + reservation: int + + +class _MeasurementPolicy(Protocol): + address: str + profile: str + resource_demands: tuple[_MeasurementDemand, ...] + + +def measurement_requirements( + policy: _MeasurementPolicy, +) -> tuple[ParticipantResourceMeasurementRequirementModel, ...]: + if policy.profile != "participant-autonomous-execution/v3": + return () + return tuple( + ParticipantResourceMeasurementRequirementModel( + budget_state_ref=participant_resource_budget_state_ref(policy.address, demand.budget_id), + resource_kind=demand.resource_kind, + unit=demand.unit, + meter_profile_ref=demand.meter_profile_ref, + reserved=demand.reservation, + ) + for demand in policy.resource_demands + ) + + +def _record_resource_failure(run: SchedulerRunState) -> None: + run.failure = ApplyResult( + success=False, + snapshot=run.working, + diagnostics=run.diagnostics, + changed_addresses=list(dict.fromkeys(run.changed)), + ) + + +def reserve_activity_resources( + context: _DueActionContext, + request: ParticipantActionAdmissionRequest, + run: SchedulerRunState, +) -> bool: + """Reserve the complete v3 resource vector before native execution.""" + + if context.policy.profile != "participant-autonomous-execution/v3": + return True + reservation = reserve_participant_resources( + run.working, + context.policy, + operation_id=request.action_instance_id, + execution_generation=request.execution_generation, + ) + run.working = reservation.snapshot + run.diagnostics.extend(reservation.diagnostics) + if not reservation.success: + _record_resource_failure(run) + return reservation.success + + +def commit_activity_resources( + context: _DueActionContext, + request: ParticipantActionAdmissionRequest, + result: ParticipantActionApplyResult, + *, + protocol_failure: bool, + run: SchedulerRunState, +) -> bool: + """Commit only a complete, trusted native measurement vector.""" + + if context.policy.profile != "participant-autonomous-execution/v3": + return True + action_result = result.action_result + requirements = {item.budget_state_ref: item for item in request.resource_measurement_requirements} + measurements = { + item.budget_state_ref: item for item in (() if action_result is None else action_result.resource_measurements) + } + trusted = ( + not protocol_failure + and request.execution_generation is not None + and set(measurements) == set(requirements) + and all( + measurement.operation_id == request.action_instance_id + and measurement.execution_generation == request.execution_generation + and measurement.resource_kind == requirements[state_ref].resource_kind + and measurement.unit == requirements[state_ref].unit + and measurement.meter_profile_ref == requirements[state_ref].meter_profile_ref + for state_ref, measurement in measurements.items() + ) + ) + if not trusted: + released = release_participant_resource_reservation( + run.working, + operation_id=request.action_instance_id, + execution_generation=request.execution_generation or 0, + evidence_refs=(f"evidence:{request.action_instance_id}:resource-release",), + ) + run.working = released.snapshot + run.diagnostics.extend(released.diagnostics) + if not protocol_failure: + run.diagnostics.append( + Diagnostic( + code="runtime.participant-resource-measurement-untrusted", + domain="participant-runtime", + address=context.policy.address, + message="native action did not return the exact trusted resource measurement vector", + ) + ) + _record_resource_failure(run) + return False + evidence_refs = tuple( + dict.fromkeys( + evidence_ref for measurement in measurements.values() for evidence_ref in measurement.evidence_refs + ) + ) + committed = commit_participant_resource_reservation( + run.working, + operation_id=request.action_instance_id, + execution_generation=request.execution_generation, + measured_quantities={state_ref: measurement.measured for state_ref, measurement in measurements.items()}, + evidence_refs=evidence_refs, + ) + run.working = committed.snapshot + run.diagnostics.extend(committed.diagnostics) + if not committed.success: + _record_resource_failure(run) + return committed.success + + +__all__ = [ + "commit_activity_resources", + "measurement_requirements", + "reserve_activity_resources", +] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_time.py b/implementations/python/packages/raes_runtime/participant_scheduler_time.py index 8ac2e6976..3f334123a 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_time.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_time.py @@ -1,6 +1,8 @@ """Shared-time helpers for autonomous participant scheduling.""" -from raes_contracts.runtime_state import RuntimeSnapshot +from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime @@ -15,6 +17,31 @@ def clock_coordinate(snapshot: RuntimeSnapshot, clock_address: str) -> tuple[int return clock.coordinate.segment, clock.coordinate.tick +def cadence_missed_result( + working: RuntimeSnapshot, + key: str, + current_tick: int, + state: ParticipantAutonomousExecutionStateModel, +) -> ApplyResult: + """Reject a participant whose governed cadence boundary was missed.""" + + return ApplyResult( + success=False, + snapshot=working, + diagnostics=[ + Diagnostic( + code="runtime.participant-autonomous-cadence-missed", + domain="participant", + address=key, + message=( + f"Shared clock is at tick {current_tick}, after the next governed " + f"participant cadence tick {state.next_tick}." + ), + ) + ], + ) + + def cadence(policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel) -> tuple[int, int]: selected = [ constraint @@ -42,4 +69,9 @@ def participant_time_domain( }[domain.kind] -__all__ = ["cadence", "clock_coordinate", "participant_time_domain"] +__all__ = [ + "cadence", + "cadence_missed_result", + "clock_coordinate", + "participant_time_domain", +] diff --git a/implementations/python/tests/test_dsl_437_benign_participant_execution.py b/implementations/python/tests/test_dsl_437_benign_participant_execution.py index e52997a48..627056bb6 100644 --- a/implementations/python/tests/test_dsl_437_benign_participant_execution.py +++ b/implementations/python/tests/test_dsl_437_benign_participant_execution.py @@ -34,6 +34,7 @@ RuntimeSnapshotEnvelopeModel, ) from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel +from raes_contracts.contracts.participant_resource_budgets import ParticipantResourceMeasurementModel from raes_contracts.contracts.random_stream import ( GovernedEntropyRefModel, PublicSeedModel, @@ -342,6 +343,19 @@ def _model_action( action_contract_address=request.action_contract_address, observation_point=request.temporal_contexts[0].observation_point, observations=["customer portal responded"], + resource_measurements=[ + ParticipantResourceMeasurementModel( + budget_state_ref=requirement.budget_state_ref, + operation_id=request.action_instance_id, + execution_generation=request.execution_generation or 0, + resource_kind=requirement.resource_kind, + unit=requirement.unit, + meter_profile_ref=requirement.meter_profile_ref, + measured=requirement.reserved, + evidence_refs=(f"evidence:{request.action_instance_id}:{requirement.budget_state_ref}",), + ) + for requirement in request.resource_measurement_requirements + ], evidence_refs=[], ), ) diff --git a/implementations/python/tests/test_issue_899_participant_resource_budgets.py b/implementations/python/tests/test_issue_899_participant_resource_budgets.py new file mode 100644 index 000000000..17a02332d --- /dev/null +++ b/implementations/python/tests/test_issue_899_participant_resource_budgets.py @@ -0,0 +1,927 @@ +"""Issue #899 participant resource budgets and shared-service fairness.""" + +from __future__ import annotations + +import json +from dataclasses import replace +from pathlib import Path + +import pytest +import yaml +from implementations.python.tests.test_dsl_437_benign_participant_execution import ( + SCENARIO_CLOCK_ADDRESS, + SCENARIO_CLOCK_STEP_TICKS, + _activity_control, + _activity_policy_yaml, + _advance_stepped_clock_to_tick, + _autonomous_manifest, + _compiled, + _NativeParticipantRuntime, +) +from pydantic import BaseModel +from raes import parse_sdl +from raes._errors import SDLValidationError +from raes.composition import _namespace_payload +from raes.participant_execution import ParticipantAutonomousExecutionPolicyV3 +from raes_backend_protocols.capability_admission import ( + participant_autonomous_execution_capability_gaps, +) +from raes_backend_protocols.participant_resource_budgets import ( + ParticipantResourceBudgetCapabilities, + ParticipantResourcePoolCapacity, +) +from raes_backend_stubs.stubs import create_stub_target +from raes_contracts.contracts import schema_bundle +from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceBudgetEventModel, + ParticipantResourceBudgetPolicyModel, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolCapacityModel, + participant_resource_budget_state_ref, +) +from raes_contracts.runtime_state import RuntimeSnapshot +from raes_processor.compiler import compile_runtime_model +from raes_runtime.control_plane_store import LocalControlPlaneStore +from raes_runtime.manager import RuntimeManager +from raes_runtime.participant_resource_accounting import ( + commit_participant_resource_reservation, + reconcile_participant_resource_budgets, +) +from raes_runtime.participant_resource_budgets import ( + initialize_participant_resource_budgets, + reserve_participant_resources, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +class _UnmeasuredParticipantRuntime(_NativeParticipantRuntime): + def _model_action(self, request, snapshot, *, episode_id): + execution = super()._model_action(request, snapshot, episode_id=episode_id) + assert execution.action_result is not None + return replace( + execution, + action_result=execution.action_result.model_copy(update={"resource_measurements": []}), + ) + + +def _measured_vector( + snapshot: RuntimeSnapshot, + operation_id: str, + **overrides: int, +) -> dict[str, int]: + return { + str(event["budget_state_ref"]): overrides.get( + str(event["budget_id"]), + int(event["requested"]), + ) + for event in snapshot.participant_resource_budget_events.values() + if event["operation_id"] == operation_id and event["transition"] == "reserve" + } + + +def _budget_policy_yaml() -> str: + payload = yaml.safe_load(_activity_policy_yaml()) + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + policy["profile"] = "participant-autonomous-execution/v3" + policy["resource_budget"] = { + "policy_id": "green-shared-capacity", + "owners": { + "green": {"kind": "participant", "ref": "participant-agent"}, + "range-a": {"kind": "deployment_tenant", "ref": "range-a"}, + "inference": { + "kind": "shared_service", + "ref": "nodes.customer-portal.services.http", + }, + "fleet": {"kind": "fleet", "ref": "fleet.primary"}, + }, + "fairness": { + "policy": "weighted_fair", + "priority_class": "background", + "weight": 1, + "protected": False, + "borrowing": "lendable_only", + "reclaim": "yield", + "max_queue_ticks": 20, + "starvation_bound_ticks": 100, + }, + "dimensions": { + "participant-actions": { + "owner_ref": "green", + "pool_ref": "participant-pool", + "resource_kind": "action_rate", + "unit": "actions", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "raes.action-attempt/v1", + "limit": 24, + "reservation": 1, + "reset": "time_segment", + "window_ticks": 100, + "parent_budget_ref": "range-actions", + }, + "range-actions": { + "owner_ref": "range-a", + "pool_ref": "range-pool", + "resource_kind": "action_rate", + "unit": "actions", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "raes.action-attempt/v1", + "limit": 240, + "reservation": 1, + "reset": "run", + "window_ticks": 100, + "parent_budget_ref": "fleet-actions", + }, + "fleet-actions": { + "owner_ref": "fleet", + "pool_ref": "fleet-pool", + "resource_kind": "action_rate", + "unit": "actions", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "raes.action-attempt/v1", + "limit": 2400, + "reservation": 1, + "reset": "run", + "window_ticks": 100, + }, + "concurrency": { + "owner_ref": "green", + "pool_ref": "participant-pool", + "resource_kind": "concurrent_actions", + "unit": "actions", + "accounting_mode": "reservable_gauge", + "meter_profile_ref": "raes.concurrent-action/v1", + "limit": 1, + "reservation": 1, + "reset": "reconciled", + }, + "storage": { + "owner_ref": "range-a", + "pool_ref": "range-pool", + "resource_kind": "storage_growth", + "unit": "bytes", + "accounting_mode": "growth_counter", + "meter_profile_ref": "raes.logical-byte/v1", + "limit": 1048576, + "reservation": 4096, + "reset": "reconciled", + }, + "tokens": { + "owner_ref": "inference", + "pool_ref": "inference-pool", + "resource_kind": "inference_tokens", + "unit": "tokens", + "accounting_mode": "windowed_counter", + "meter_profile_ref": "tokenizer.example/v1", + "limit": 20000, + "reservation": 500, + "reset": "time_segment", + "window_ticks": 100, + }, + "images": { + "owner_ref": "inference", + "pool_ref": "inference-pool", + "resource_kind": "image_generations", + "unit": "images", + "accounting_mode": "cumulative_counter", + "meter_profile_ref": "raes.image-generation/v1", + "limit": 20, + "reservation": 1, + "reset": "run", + }, + "accelerator": { + "owner_ref": "fleet", + "pool_ref": "accelerator-pool", + "resource_kind": "accelerator", + "unit": "accelerator_milliseconds", + "accounting_mode": "lease", + "meter_profile_ref": "raes.accelerator-class.generic/v1", + "limit": 60000, + "reservation": 1000, + "reset": "reconciled", + }, + }, + } + payload["deployment_tenants"] = { + "range-a": {"description": "Evaluated range A."}, + "range-b": {"description": "Independent range B."}, + } + payload.setdefault("relationships", {})["range-a-inference-service"] = { + "type": "uses_shared_service", + "source": "deployment_tenants.range-a", + "target": "nodes.customer-portal.services.http", + "description": "Range A is authorized to consume the governed inference service.", + "shared_service": { + "tenant_isolation": "stateless", + "workload_authentication": "tenant_scoped_workload_identity", + "mutable_state_refs": [], + "mutable_state_owner": "none", + "reset_generation_owner": "none", + }, + } + return yaml.safe_dump(payload, sort_keys=False) + + +def _pool( + pool_ref: str, + *, + owner_kind: str, + owner_ref: str, + resource_kind: str, + unit: str, + meter_profile_ref: str, + capacity: int, + isolation: str = "tenant_partitioned", +) -> ParticipantResourcePoolCapacity: + return ParticipantResourcePoolCapacity( + pool_ref=pool_ref, + owner_kind=owner_kind, + owner_ref=owner_ref, + resource_kind=resource_kind, + unit=unit, + accounting_mode=( + "reservable_gauge" + if resource_kind == "concurrent_actions" + else "growth_counter" + if resource_kind == "storage_growth" + else "lease" + if resource_kind == "accelerator" + else "cumulative_counter" + if resource_kind == "image_generations" + else "windowed_counter" + ), + meter_profile_ref=meter_profile_ref, + capacity=capacity, + tenant_isolation=isolation, + configuration_digest="sha256:" + "1" * 64, + fairness_policy="weighted_fair", + priority_classes=("evaluated", "standard", "background"), + borrowing="lendable_only", + reclaim="yield", + max_queue_ticks=20, + starvation_bound_ticks=100, + protected_capacity=1, + evidence_contract_ids=("participant-resource-budget-event-v1",), + ) + + +def _capabilities() -> ParticipantResourceBudgetCapabilities: + return ParticipantResourceBudgetCapabilities( + support_strength="exact", + supported_owner_kinds=frozenset({"participant", "deployment_tenant", "shared_service", "fleet"}), + supported_resource_kinds=frozenset( + { + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator", + } + ), + supported_accounting_modes=frozenset( + { + "windowed_counter", + "cumulative_counter", + "reservable_gauge", + "growth_counter", + "lease", + } + ), + supported_reset_modes=frozenset({"time_segment", "run", "reconciled"}), + supported_fairness_policies=frozenset({"weighted_fair"}), + supported_isolation_strengths=frozenset({"tenant_partitioned"}), + configured_pools=( + _pool( + "participant-pool", + owner_kind="participant", + owner_ref="participant.behavior.participant-agent", + resource_kind="action_rate", + unit="actions", + meter_profile_ref="raes.action-attempt/v1", + capacity=24, + ), + _pool( + "participant-pool", + owner_kind="participant", + owner_ref="participant.behavior.participant-agent", + resource_kind="concurrent_actions", + unit="actions", + meter_profile_ref="raes.concurrent-action/v1", + capacity=2, + ), + _pool( + "range-pool", + owner_kind="deployment_tenant", + owner_ref="deployment.tenant.range-a", + resource_kind="action_rate", + unit="actions", + meter_profile_ref="raes.action-attempt/v1", + capacity=240, + ), + _pool( + "range-pool", + owner_kind="deployment_tenant", + owner_ref="deployment.tenant.range-a", + resource_kind="storage_growth", + unit="bytes", + meter_profile_ref="raes.logical-byte/v1", + capacity=1048576, + ), + _pool( + "inference-pool", + owner_kind="shared_service", + owner_ref="provision.node.customer-portal.service.http", + resource_kind="inference_tokens", + unit="tokens", + meter_profile_ref="tokenizer.example/v1", + capacity=20000, + ), + _pool( + "inference-pool", + owner_kind="shared_service", + owner_ref="provision.node.customer-portal.service.http", + resource_kind="image_generations", + unit="images", + meter_profile_ref="raes.image-generation/v1", + capacity=20, + ), + _pool( + "fleet-pool", + owner_kind="fleet", + owner_ref="fleet.primary", + resource_kind="action_rate", + unit="actions", + meter_profile_ref="raes.action-attempt/v1", + capacity=2400, + ), + _pool( + "accelerator-pool", + owner_kind="fleet", + owner_ref="fleet.primary", + resource_kind="accelerator", + unit="accelerator_milliseconds", + meter_profile_ref="raes.accelerator-class.generic/v1", + capacity=60000, + ), + ), + realization_contract_ids=frozenset( + { + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", + } + ), + ) + + +def _governed_manifest() -> object: + base_runtime = compile_runtime_model(parse_sdl(_activity_policy_yaml())) + manifest = _autonomous_manifest(base_runtime) + capability = manifest.participant_runtime + assert capability is not None + governed = replace( + capability, + supported_autonomous_policy_profiles=( + capability.supported_autonomous_policy_profiles | {"participant-autonomous-execution/v3"} + ), + resource_budgets=_capabilities(), + ) + return replace( + manifest, + capabilities=replace(manifest.capabilities, participant_runtime=governed), + ) + + +def test_v3_resource_budget_compiles_complete_typed_vector() -> None: + runtime_model = compile_runtime_model(parse_sdl(_budget_policy_yaml())) + policy = next( + specification.autonomous_execution + for specification in runtime_model.behavior_specifications.values() + if specification.autonomous_execution is not None + ) + + assert policy.profile == "participant-autonomous-execution/v3" + assert {demand.resource_kind for demand in policy.resource_demands} == { + "action_rate", + "concurrent_actions", + "storage_growth", + "inference_tokens", + "image_generations", + "accelerator", + } + assert {owner.kind for owner in policy.resource_owners} == { + "participant", + "deployment_tenant", + "shared_service", + "fleet", + } + assert policy.resource_fairness.priority_class == "background" + + +def test_resource_owners_must_bind_to_authorized_execution_topology() -> None: + payload = yaml.safe_load(_budget_policy_yaml()) + del payload["relationships"]["range-a-inference-service"] + + with pytest.raises( + SDLValidationError, + match="lacks an authorized tenant uses_shared_service edge", + ): + parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + + +def test_composition_rewrites_kind_specific_resource_owner_refs() -> None: + payload = yaml.safe_load(_budget_policy_yaml()) + payload["module"] = { + "id": "example/resource-governed-participant", + "version": "1.0.0", + "exports": { + "agents": ["participant-agent"], + "behavior_specifications": ["participant-behavior"], + "deployment_tenants": ["range-a", "range-b"], + "nodes": ["customer-portal"], + }, + } + imported = parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + assert imported.module is not None + namespaced = _namespace_payload( + payload, + imported, + "shared", + imported.module, + ) + + owners = namespaced["behavior_specifications"]["shared.participant-behavior"]["autonomous_execution"][ + "resource_budget" + ]["owners"] + assert owners["green"]["ref"] == "shared.participant-agent" + assert owners["range-a"]["ref"] == "shared.range-a" + assert owners["inference"]["ref"] == "nodes.shared.customer-portal.services.http" + assert owners["fleet"]["ref"] == "fleet.primary" + + +def test_v3_resource_budget_rejects_incompatible_units_cycles_and_episode_reset() -> None: + payload = yaml.safe_load(_budget_policy_yaml()) + dimensions = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["resource_budget"][ + "dimensions" + ] + + dimensions["tokens"]["unit"] = "bytes" + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + with pytest.raises(ValueError, match="inference_tokens.*tokens"): + ParticipantAutonomousExecutionPolicyV3.model_validate(policy) + + payload = yaml.safe_load(_budget_policy_yaml()) + dimensions = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["resource_budget"][ + "dimensions" + ] + dimensions["range-actions"]["limit"] = 24 + dimensions["fleet-actions"]["limit"] = 24 + dimensions["fleet-actions"]["parent_budget_ref"] = "participant-actions" + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + with pytest.raises(ValueError, match="acyclic"): + ParticipantAutonomousExecutionPolicyV3.model_validate(policy) + + payload = yaml.safe_load(_budget_policy_yaml()) + dimensions = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["resource_budget"][ + "dimensions" + ] + dimensions["storage"]["reset"] = "episode" + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + with pytest.raises(ValueError, match="storage_growth.*reconciled"): + ParticipantAutonomousExecutionPolicyV3.model_validate(policy) + + payload = yaml.safe_load(_budget_policy_yaml()) + dimensions = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["resource_budget"][ + "dimensions" + ] + dimensions["tokens-copy"] = dict(dimensions["tokens"]) + policy = payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"] + with pytest.raises(ValueError, match="alias the same canonical resource pool"): + ParticipantAutonomousExecutionPolicyV3.model_validate(policy) + + +def test_resource_event_rejects_contradictory_transition_disposition() -> None: + payload = json.loads( + ( + REPO_ROOT + / "contracts/fixtures/participant-runtime/participant-resource-budget-event-v1/valid/token-commit-event.json" + ).read_text(encoding="utf-8") + ) + payload["disposition"] = "released" + with pytest.raises(ValueError, match="disposition must match"): + ParticipantResourceBudgetEventModel.model_validate(payload) + + +def test_legacy_profiles_compile_into_canonical_demand_representation() -> None: + _, v1 = _compiled() + v2 = next( + specification.autonomous_execution + for specification in compile_runtime_model(parse_sdl(_activity_policy_yaml())).behavior_specifications.values() + if specification.autonomous_execution is not None + ) + + for policy in (v1, v2): + by_kind = {demand.resource_kind: demand for demand in policy.resource_demands} + assert by_kind["action_rate"].limit == policy.max_action_attempts + assert by_kind["concurrent_actions"].limit == policy.max_in_flight + assert by_kind["action_rate"].provenance == "legacy_maximum" + + +def test_manifest_round_trip_separates_support_capacity_and_realization() -> None: + runtime_model, _ = _compiled() + manifest = _autonomous_manifest(runtime_model) + capability = manifest.participant_runtime + assert capability is not None + governed = replace(capability, resource_budgets=_capabilities()) + governed_manifest = replace( + manifest, + capabilities=replace(manifest.capabilities, participant_runtime=governed), + ) + + from raes_backend_protocols.manifest import ( + backend_manifest_from_v2_model, + backend_manifest_v2_model, + ) + + wire = backend_manifest_v2_model(governed_manifest) + restored = backend_manifest_from_v2_model(wire) + + assert restored.participant_runtime is not None + restored_budgets = restored.participant_runtime.resource_budgets + assert restored_budgets is not None + assert restored_budgets.support_strength == "exact" + assert restored_budgets.configured_pools[0].configuration_digest.startswith("sha256:") + assert "participant-resource-budget-event-v1" in restored_budgets.realization_contract_ids + + +def test_admission_is_atomic_for_complete_vector_and_exact_meters() -> None: + runtime_model = compile_runtime_model(parse_sdl(_budget_policy_yaml())) + policy = next( + specification.autonomous_execution + for specification in runtime_model.behavior_specifications.values() + if specification.autonomous_execution is not None + ) + admitted_manifest = _governed_manifest() + governed = admitted_manifest.participant_runtime + assert governed is not None + + assert ( + participant_autonomous_execution_capability_gaps( + admitted_manifest, + (policy,), + runtime_model.time_model, + ) + == () + ) + + weakened_pools = tuple( + replace(pool, capacity=499) if pool.resource_kind == "inference_tokens" else pool + for pool in _capabilities().configured_pools + ) + weakened = replace(_capabilities(), configured_pools=weakened_pools) + weakened_manifest = replace( + admitted_manifest, + capabilities=replace( + admitted_manifest.capabilities, + participant_runtime=replace(governed, resource_budgets=weakened), + ), + ) + gaps = participant_autonomous_execution_capability_gaps( + weakened_manifest, + (policy,), + runtime_model.time_model, + ) + assert any("inference_tokens" in gap and "capacity" in gap for gap in gaps) + + weak_fairness = replace( + _capabilities(), + configured_pools=tuple( + replace(pool, priority_classes=("evaluated", "standard")) for pool in _capabilities().configured_pools + ), + ) + fairness_manifest = replace( + admitted_manifest, + capabilities=replace( + admitted_manifest.capabilities, + participant_runtime=replace(governed, resource_budgets=weak_fairness), + ), + ) + fairness_gaps = participant_autonomous_execution_capability_gaps( + fairness_manifest, + (policy,), + runtime_model.time_model, + ) + assert any("priority class background" in gap for gap in fairness_gaps) + + incomplete_manifest = replace( + admitted_manifest, + supported_contract_versions=( + admitted_manifest.supported_contract_versions - {"participant-resource-pool-capacity-v1"} + ), + ) + contract_gaps = participant_autonomous_execution_capability_gaps( + incomplete_manifest, + (policy,), + runtime_model.time_model, + ) + assert any( + "missing manifest contracts" in gap and "participant-resource-pool-capacity-v1" in gap for gap in contract_gaps + ) + + competing = replace(policy, address=f"{policy.address}.competing") + aggregate_gaps = participant_autonomous_execution_capability_gaps( + admitted_manifest, + (policy, competing), + runtime_model.time_model, + ) + assert any("aggregate policy limits" in gap for gap in aggregate_gaps) + + +def test_runtime_pool_ledger_prevents_competing_policy_overcommit() -> None: + runtime_model = compile_runtime_model(parse_sdl(_budget_policy_yaml())) + policy = next( + specification.autonomous_execution + for specification in runtime_model.behavior_specifications.values() + if specification.autonomous_execution is not None + ) + competing = replace(policy, address=f"{policy.address}.competing") + initialized = initialize_participant_resource_budgets( + RuntimeSnapshot(), + (policy, competing), + _capabilities(), + execution_generation=0, + ) + assert initialized.success + assert len(initialized.snapshot.participant_resource_budget_states) == 16 + + first = reserve_participant_resources( + initialized.snapshot, + policy, + operation_id="first-policy", + execution_generation=0, + requested_quantities={"participant-actions": 23}, + ) + assert first.success + second = reserve_participant_resources( + first.snapshot, + competing, + operation_id="competing-policy", + execution_generation=0, + ) + assert second.success is False + assert second.diagnostics[0].code == "runtime.participant-resource-throttled" + assert "shared pool" in second.diagnostics[0].message + + +def test_runtime_reserve_commit_throttle_and_idempotency_are_generation_fenced( + tmp_path: Path, +) -> None: + runtime_model = compile_runtime_model(parse_sdl(_budget_policy_yaml())) + policy = next( + specification.autonomous_execution + for specification in runtime_model.behavior_specifications.values() + if specification.autonomous_execution is not None + ) + initialized = initialize_participant_resource_budgets( + RuntimeSnapshot(), + (policy,), + _capabilities(), + execution_generation=3, + ) + assert initialized.success + + reserved = reserve_participant_resources( + initialized.snapshot, + policy, + operation_id="action-1", + execution_generation=3, + ) + assert reserved.success + repeated = reserve_participant_resources( + reserved.snapshot, + policy, + operation_id="action-1", + execution_generation=3, + ) + assert repeated.success + assert repeated.snapshot.participant_resource_budget_events == ( + reserved.snapshot.participant_resource_budget_events + ) + + committed = commit_participant_resource_reservation( + reserved.snapshot, + operation_id="action-1", + execution_generation=3, + measured_quantities=_measured_vector(reserved.snapshot, "action-1", tokens=450), + evidence_refs=("evidence.resource-meter.action-1",), + ) + assert committed.success + state = ParticipantResourceBudgetStateModel.model_validate( + committed.snapshot.participant_resource_budget_states[ + participant_resource_budget_state_ref(policy.address, "tokens") + ] + ) + assert state.reserved == 0 + assert state.cumulative_use == 450 + assert state.evidence_refs == ("evidence.resource-meter.action-1",) + + store = LocalControlPlaneStore(tmp_path / "control-plane") + store.save_snapshot(committed.snapshot) + restored = store.load_snapshot() + assert restored.participant_resource_budget_states == (committed.snapshot.participant_resource_budget_states) + assert restored.participant_resource_budget_events == (committed.snapshot.participant_resource_budget_events) + + stale = reserve_participant_resources( + restored, + policy, + operation_id="action-stale", + execution_generation=2, + ) + assert stale.success is False + assert stale.snapshot is restored + assert stale.diagnostics[0].code == "runtime.participant-resource-stale-generation" + + exhausted = reserve_participant_resources( + restored, + policy, + operation_id="action-too-large", + execution_generation=3, + requested_quantities={"tokens": 20000}, + ) + assert exhausted.success is False + event = ParticipantResourceBudgetEventModel.model_validate( + next( + payload + for payload in exhausted.snapshot.participant_resource_budget_events.values() + if payload["operation_id"] == "action-too-large" + ) + ) + assert event.disposition == "throttled" + + +def test_reset_reconciles_participant_window_without_erasing_persistent_owners() -> None: + runtime_model = compile_runtime_model(parse_sdl(_budget_policy_yaml())) + policy = next( + specification.autonomous_execution + for specification in runtime_model.behavior_specifications.values() + if specification.autonomous_execution is not None + ) + initialized = initialize_participant_resource_budgets( + RuntimeSnapshot(), + (policy,), + _capabilities(), + execution_generation=0, + ) + reserved = reserve_participant_resources( + initialized.snapshot, + policy, + operation_id="action-1", + execution_generation=0, + ) + committed = commit_participant_resource_reservation( + reserved.snapshot, + operation_id="action-1", + execution_generation=0, + measured_quantities=_measured_vector( + reserved.snapshot, + "action-1", + participant_actions=1, + storage=4096, + ), + evidence_refs=("evidence.action-1",), + ) + reconciled = reconcile_participant_resource_budgets( + committed.snapshot, + policy_address=policy.address, + current_generation=0, + next_generation=1, + boundary="time_segment", + evidence_refs=("evidence.reset.generation-1",), + ) + assert reconciled.success + + states = { + key: ParticipantResourceBudgetStateModel.model_validate(value) + for key, value in reconciled.snapshot.participant_resource_budget_states.items() + } + participant_actions_ref = participant_resource_budget_state_ref(policy.address, "participant-actions") + storage_ref = participant_resource_budget_state_ref(policy.address, "storage") + assert states[participant_actions_ref].cumulative_use == 0 + assert states[participant_actions_ref].generation == 1 + assert states[storage_ref].cumulative_use == 4096 + assert states[storage_ref].generation == 1 + + +def test_runtime_manager_enforces_v3_budgets_and_reconciles_reset_generation() -> None: + scenario = parse_sdl(_budget_policy_yaml()) + runtime_model = compile_runtime_model(scenario) + participant_runtime = _NativeParticipantRuntime() + target = replace( + create_stub_target(), + manifest=_governed_manifest(), + participant_runtime=participant_runtime, + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + + applied = manager.apply(manager.plan(scenario)) + + assert applied.success + service = next(iter(applied.snapshot.participant_execution_services.values())) + assert set(service["resource_budget_state_refs"]) == set(applied.snapshot.participant_resource_budget_states) + scheduler_state = next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + due = _advance_stepped_clock_to_tick(manager, scheduler_state["next_tick"]) + assert due.success + assert any(event["transition"] == "commit" for event in due.snapshot.participant_resource_budget_events.values()) + + reset = manager.reset_time("time.clock.scenario-clock") + + assert reset.success + assert {state["generation"] for state in reset.snapshot.participant_resource_budget_states.values()} == {1} + + +def test_scheduler_releases_reservations_when_native_measurements_are_absent() -> None: + scenario = parse_sdl(_budget_policy_yaml()) + runtime_model = compile_runtime_model(scenario) + target = replace( + create_stub_target(), + manifest=_governed_manifest(), + participant_runtime=_UnmeasuredParticipantRuntime(), + ) + manager = RuntimeManager(target, stochastic_controls=[_activity_control()]) + applied = manager.apply(manager.plan(scenario)) + assert applied.success + scheduler_state = next(iter(applied.snapshot.participant_autonomous_execution_states.values())) + + assert scheduler_state["next_tick"] == SCENARIO_CLOCK_STEP_TICKS + due = manager.advance_time(SCENARIO_CLOCK_ADDRESS, ticks=SCENARIO_CLOCK_STEP_TICKS) + + assert due.success is False + assert any( + diagnostic.code == "runtime.participant-resource-measurement-untrusted" for diagnostic in due.diagnostics + ) + assert all(state["reserved"] == 0 for state in due.snapshot.participant_resource_budget_states.values()) + assert not any( + event["transition"] == "commit" for event in due.snapshot.participant_resource_budget_events.values() + ) + + +def test_cross_range_shared_pool_requires_partitioned_isolation() -> None: + capabilities = _capabilities() + shared = tuple( + replace(pool, tenant_isolation="none") if pool.pool_ref == "inference-pool" else pool + for pool in capabilities.configured_pools + ) + + with pytest.raises(ValueError, match="cross-range.*tenant_partitioned"): + ParticipantResourceBudgetCapabilities( + support_strength=capabilities.support_strength, + supported_owner_kinds=capabilities.supported_owner_kinds, + supported_resource_kinds=capabilities.supported_resource_kinds, + supported_accounting_modes=capabilities.supported_accounting_modes, + supported_reset_modes=capabilities.supported_reset_modes, + supported_fairness_policies=capabilities.supported_fairness_policies, + supported_isolation_strengths=capabilities.supported_isolation_strengths, + configured_pools=shared, + realization_contract_ids=capabilities.realization_contract_ids, + cross_range_pool_refs=frozenset({"inference-pool"}), + ) + + +@pytest.mark.parametrize( + ("contract_id", "model", "filename"), + [ + ( + "participant-resource-budget-policy-v1", + ParticipantResourceBudgetPolicyModel, + "complete-resource-vector.json", + ), + ( + "participant-resource-pool-capacity-v1", + ParticipantResourcePoolCapacityModel, + "configured-inference-pool.json", + ), + ( + "participant-resource-budget-state-v1", + ParticipantResourceBudgetStateModel, + "token-budget-state.json", + ), + ( + "participant-resource-budget-event-v1", + ParticipantResourceBudgetEventModel, + "token-commit-event.json", + ), + ], +) +def test_published_resource_budget_fixtures_match_contract_models( + contract_id: str, + model: type[BaseModel], + filename: str, +) -> None: + payload = json.loads( + (REPO_ROOT / "contracts" / "fixtures" / "participant-runtime" / contract_id / "valid" / filename).read_text( + encoding="utf-8" + ) + ) + + model.model_validate(payload) + assert schema_bundle()[contract_id]["additionalProperties"] is False diff --git a/specs/formal/participant-semantics/autonomous-execution.md b/specs/formal/participant-semantics/autonomous-execution.md index 7f038ffde..d2b6867d4 100644 --- a/specs/formal/participant-semantics/autonomous-execution.md +++ b/specs/formal/participant-semantics/autonomous-execution.md @@ -61,6 +61,37 @@ the authored disposition exactly: `skip` terminates that scheduling path; `next_opening` performs a finite forward search over declared work windows. It never clamps, redraws, sleeps on wall time, or consults host calendar state. +## V3 Scoped Resource Policy + +`participant-autonomous-execution/v3` preserves all v2 activity semantics and +adds one required `resource_budget`. A policy declares a finite owner table, +one fairness obligation, and a complete typed demand vector containing: + +- action-rate (`actions`, windowed counter); +- concurrent-action (`actions`, reservable gauge); +- storage-growth (`bytes`, growth counter); +- inference-token (`tokens`, windowed or cumulative counter); +- image-generation (`images`, windowed or cumulative counter); and +- accelerator (`accelerator_milliseconds`, lease). + +Each demand binds exactly one owner, logical pool, unit, accounting mode, +meter profile, limit, reservation quantity, and reset owner. Owner kinds are +participant, deployment tenant, shared service, and fleet. Participant, +deployment-tenant, and shared-service refs resolve to their existing canonical +addresses; a resource owner grants no participant authority or cross-tenant +access. Deployment-tenant ownership must match an authorized execution target, +and shared-service ownership must match both the exact target and an explicit +tenant `uses_shared_service` edge. The optional parent relation is acyclic, +preserves resource kind, unit, accounting mode, and meter, and cannot increase +or overcommit its parent limit. Two budget ids cannot alias one canonical pool. +Storage growth resets only through evidence-backed reconciliation. The single +participant-owned concurrent-action limit equals `max_in_flight`, so execution +service and budget readback cannot become independent authorities. + +V1 and v2 limits compile into the same canonical demand representation with +`legacy_maximum` provenance. They do not acquire v3 capacity, fairness, +isolation, or runtime-accounting requirements. + ## Non-Evaluated Authority Invariant When `evaluation_authority.mode = none`: @@ -143,6 +174,18 @@ An action may commit only in this order: 6. append action, state-transition, and observation events; and 7. update typed scheduler readback. +V3 inserts an atomic reservation of the complete resource vector before step +4. Physical capacity is owned by one canonical pool ledger shared across +policies. An exactly-once commit occurs only after the native result supplies +the complete matching operation/generation/resource/unit/meter measurement +vector with evidence; protocol failure or an absent/untrusted vector releases +or rolls back the reservation. +If any dimension or ancestor pool cannot reserve, no dimension reserves and +the native action is not invoked. Stable action identity makes reservation and +settlement idempotent. Every throttle, reservation, commit, release, or reset +reconciliation is a typed, generation-fenced event; metadata and log text are +not authoritative accounting. + The scheduler applies these checks to every participant-runtime implementation, not only implementations derived from the reference base class. If the result has the wrong result type, is absent, non-terminal, bound to another episode, @@ -248,6 +291,14 @@ pacing marks the service degraded, not ready, and paused and appends an explicit pacing-deviation evidence reference; it is never silently treated as successful timing. +V3 reset reconciles outstanding reservations before advancing the execution +generation. A `time_segment` boundary clears only dimensions owned by that +boundary. Tenant, shared-service, fleet, and persistent storage use survive +participant or segment reset unless their own declared reset/reconciliation +rule applies. Execution-service resource refs name the authoritative budget +states; its concurrency capacity, reservation, and in-flight projection must +equal the referenced concurrent-action state. + ## Backend Admission Let backend capability \(K\) declare supported strategies and finite maxima. @@ -308,6 +359,25 @@ V2 additionally requires exact admission of: Missing or differently named support fails admission; no compatible-profile, transform, or strategy fallback is inferred. +V3 additionally requires exact, all-or-nothing admission of: + +- the complete six-kind demand vector and owner/parent graph; +- bounded or exact backend support for every owner, kind, accounting mode, + reset mode, and fairness policy; +- one configuration-bound pool entry matching each demand's canonical owner, + pool, kind, unit, accounting mode, and meter; +- configured capacity at least equal to the admitted limit; +- tenant-partitioned isolation for every declared cross-range pool; and +- the budget state and event realization contracts. + +Manifest support, configured capacity, and measured realization are distinct +authorities. A capability declaration is not utilization evidence; a runtime +sample cannot enlarge configured capacity. Fairness is explicit and +role-independent: priority class, weight, protected posture, borrowing, +reclaim, queue bound, and starvation bound are admitted as one obligation. +Backends may not infer priority from participant color or evaluation +authority. + ## Nonclaims This contract does not prove participant intelligence, human realism, service @@ -329,4 +399,7 @@ execution remains rejected because no portable transition-notification contract is yet governed. This establishes the portable protocol behavior only. A production backend still must prove that its selected participant implementation, native adapter, targets, evidence, and readback faithfully -materialize a scenario. +materialize a scenario. V3 additionally covers canonical legacy projection, +atomic multi-resource admission and reservation, typed runtime state/events, +generation-fenced settlement and reset reconciliation, durable/control-plane +projection, and cross-range isolation rejection. diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index 0746eabed..446594397 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -77,6 +77,10 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: "participant-execution-binding-v1", "participant-execution-control-v1", "participant-execution-service-state-v1", + "participant-resource-budget-policy-v1", + "participant-resource-pool-capacity-v1", + "participant-resource-budget-state-v1", + "participant-resource-budget-event-v1", "participant-outcome-report-v1", "runtime-fact-binding-plane-v1", }: diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index c0bf38862..c14484ddd 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -29,8 +29,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": 350, - "content_sha256": "2ded49472972eaeedf5753ff64e420020e0576e9d7753f0271046a7340d9ada9" + "occurrences": 346, + "content_sha256": "362f96df38c79d9d33f85e69a919b4f0e3732482e9589b26c932470bc6e7d82f" }, { "path": "contracts/schema-publication/tombstones/\u0061ces-semantic-invariants-v1.json", @@ -44,7 +44,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", "occurrences": 4, - "content_sha256": "bcb796f8491d00f1c842cffbfe36dc3c0fccd5838e7353d1d600f6a780bfa184" + "content_sha256": "a467393349328faccb797dfb20c871cbfe3a4ce4b115c64630a6b136a7089744" }, { "path": "docs/decisions/adrs/adr-000-use-adrs.md", @@ -1577,7 +1577,7 @@ "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 24, - "content_sha256": "050e205672cf340fa167e2a2d7b3e2739f058794eb28d34b68612767249a511a" + "content_sha256": "89676790565dc970e4f07ff48dd705094124877bdd4f4aaa7e574c4cce4af9e9" }, { "path": "docs/research/participant-backend-contracts/index.md", From d38b38a6e2a5a52be838025b27ca1430745c2a0c Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 05:41:11 +0200 Subject: [PATCH 38/55] Fix autonomous execution migration docs navigation --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 6bf9cd803..604925fe7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -161,6 +161,7 @@ specs/formal lessons/README migration/README migration/autonomous-execution-v2 +migration/autonomous-execution-v3 migration/participant-execution-control migration/raes-rename research/experiment-core/index From 40a13bbbb6cdcc1ab0c02ea62219577010ce3768 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 05:43:09 +0200 Subject: [PATCH 39/55] Establish reader-first public documentation --- .bestpractices.json | 70 ++++ .github/ISSUE_TEMPLATE/bug_report.md | 12 +- .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.md | 12 +- .github/PULL_REQUEST_TEMPLATE.md | 18 +- .github/workflows/docs.yml | 13 +- .github/workflows/scorecard.yml | 52 +++ .readthedocs.yaml | 4 +- .vale.ini | 10 + CODE_OF_CONDUCT.md | 42 +- CONTRIBUTING.md | 49 ++- GOVERNANCE.md | 35 ++ MAINTAINERS.md | 13 + README.md | 379 +++++++---------- SECURITY.md | 70 +--- SUPPORT.md | 10 +- docs/DEVELOPMENT_WORKFLOW.md | 44 ++ docs/Makefile | 2 +- docs/README.md | 24 ++ ...ssue-928-public-documentation-preflight.md | 320 ++++++++++++++ .../development/authorship-anomaly-2026-07.md | 33 ++ .../reference/canonical-reference-map.md | 10 +- .../reference/documentation-style-guide.md | 96 ++++- docs/index.md | 201 +-------- .../_static/examples/first-scenario.sdl.yaml | 23 + docs/{ => public}/api/cli.rst | 0 docs/{ => public}/api/contracts.rst | 0 docs/public/api/index.rst | 15 + docs/{ => public}/api/processor-semantics.rst | 0 docs/{ => public}/api/processor.rst | 0 docs/{ => public}/api/runtime.rst | 0 docs/{ => public}/api/sdl-semantics.rst | 0 docs/{ => public}/api/sdl.rst | 0 docs/public/backends.md | 20 + docs/public/citation.md | 17 + docs/public/concepts.md | 26 ++ docs/{ => public}/conf.py | 14 +- docs/public/contributing.md | 17 + docs/public/guides/cli.md | 26 ++ docs/public/guides/python.md | 27 ++ docs/public/index.md | 42 ++ docs/public/limitations.md | 18 + docs/public/quickstart.md | 54 +++ docs/public/redirects.json | 14 + docs/public/research.md | 21 + docs/public/sdl/index.md | 25 ++ docs/public/support.md | 11 + docs/public/tutorials/first-scenario.md | 34 ++ examples/library/catalog.yaml | 2 +- implementations/python/pyproject.toml | 1 + .../python/tests/test_project_positioning.py | 46 +- .../python/tests/test_public_docs_policy.py | 171 ++++++++ .../tests/test_public_project_readiness.py | 69 +++ .../python/tests/test_repo_policy_tools.py | 53 +++ .../python/tests/test_vale_tool.py | 71 ++++ .../python/tests/test_verification_plan.py | 18 +- implementations/python/uv.lock | 15 + noxfile.py | 92 +++- specs/agent-guidance/agent-guidance.yaml | 6 +- specs/authority/authority-boundary.yaml | 4 + styles/RAES/AvoidDismissive.yml | 6 + styles/RAES/AvoidHype.yml | 6 + styles/RAES/HeadingCase.yml | 21 + styles/RAES/HeadingPunctuation.yml | 7 + styles/RAES/PlainWords.yml | 15 + styles/RAES/ReadingLevel.yml | 6 + styles/RAES/SentenceLength.yml | 6 + styles/config/vocabularies/RAES/accept.txt | 26 ++ tools/check_project_positioning.py | 40 +- tools/check_public_docs.py | 392 ++++++++++++++++++ tools/policy/requirement_order.yaml | 33 ++ tools/tool_versions.py | 1 + tools/vale_tool.py | 96 +++++ tools/verification_plan.py | 2 +- 74 files changed, 2477 insertions(+), 659 deletions(-) create mode 100644 .bestpractices.json create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/scorecard.yml create mode 100644 .vale.ini create mode 100644 GOVERNANCE.md create mode 100644 MAINTAINERS.md create mode 100644 docs/DEVELOPMENT_WORKFLOW.md create mode 100644 docs/README.md create mode 100644 docs/decisions/issue-928-public-documentation-preflight.md create mode 100644 docs/development/authorship-anomaly-2026-07.md create mode 100644 docs/public/_static/examples/first-scenario.sdl.yaml rename docs/{ => public}/api/cli.rst (100%) rename docs/{ => public}/api/contracts.rst (100%) create mode 100644 docs/public/api/index.rst rename docs/{ => public}/api/processor-semantics.rst (100%) rename docs/{ => public}/api/processor.rst (100%) rename docs/{ => public}/api/runtime.rst (100%) rename docs/{ => public}/api/sdl-semantics.rst (100%) rename docs/{ => public}/api/sdl.rst (100%) create mode 100644 docs/public/backends.md create mode 100644 docs/public/citation.md create mode 100644 docs/public/concepts.md rename docs/{ => public}/conf.py (88%) create mode 100644 docs/public/contributing.md create mode 100644 docs/public/guides/cli.md create mode 100644 docs/public/guides/python.md create mode 100644 docs/public/index.md create mode 100644 docs/public/limitations.md create mode 100644 docs/public/quickstart.md create mode 100644 docs/public/redirects.json create mode 100644 docs/public/research.md create mode 100644 docs/public/sdl/index.md create mode 100644 docs/public/support.md create mode 100644 docs/public/tutorials/first-scenario.md create mode 100644 implementations/python/tests/test_public_docs_policy.py create mode 100644 implementations/python/tests/test_public_project_readiness.py create mode 100644 implementations/python/tests/test_vale_tool.py create mode 100644 styles/RAES/AvoidDismissive.yml create mode 100644 styles/RAES/AvoidHype.yml create mode 100644 styles/RAES/HeadingCase.yml create mode 100644 styles/RAES/HeadingPunctuation.yml create mode 100644 styles/RAES/PlainWords.yml create mode 100644 styles/RAES/ReadingLevel.yml create mode 100644 styles/RAES/SentenceLength.yml create mode 100644 styles/config/vocabularies/RAES/accept.txt create mode 100644 tools/check_public_docs.py create mode 100644 tools/vale_tool.py diff --git a/.bestpractices.json b/.bestpractices.json new file mode 100644 index 000000000..241e5af74 --- /dev/null +++ b/.bestpractices.json @@ -0,0 +1,70 @@ +{ + "name": "Reproducible Agentic Environments System", + "description": "RAES describes and validates authored agentic-environment scenarios, with published contracts, examples, conformance checks, and a Python reference implementation.", + "homepage_url": "https://raesystem.github.io/rae/", + "repo_url": "https://github.com/RAESystem/rae", + "license": "MIT", + "implementation_languages": "Python", + "description_good_status": "Met", + "description_good_justification": "The README states the purpose, scope, first successful task, and current limits: https://github.com/RAESystem/rae#readme.", + "interact_status": "Met", + "interact_justification": "Public GitHub issues and pull requests support URL-addressable discussion: https://github.com/RAESystem/rae/issues.", + "contribution_status": "Met", + "contribution_justification": "Contribution setup and pull-request steps are in https://github.com/RAESystem/rae/blob/main/CONTRIBUTING.md.", + "contribution_requirements_status": "Met", + "contribution_requirements_justification": "CONTRIBUTING.md documents the dev branch, tests, docs rules, and Conventional Commit pull-request titles.", + "floss_license_status": "Met", + "floss_license_justification": "RAES uses the MIT License.", + "floss_license_osi_status": "Met", + "floss_license_osi_justification": "MIT is an OSI-approved open source license.", + "license_location_status": "Met", + "license_location_justification": "The license is at https://github.com/RAESystem/rae/blob/main/LICENSE.", + "documentation_basics_status": "Met", + "documentation_basics_justification": "The README and docs/public provide installation, a tested quickstart, concepts, task guides, research context, and limits.", + "documentation_interface_status": "Met", + "documentation_interface_justification": "The public Python, CLI, SDL, backend, and generated API pages document the current external interfaces.", + "sites_https_status": "Met", + "sites_https_justification": "The repository and package page use HTTPS.", + "discussion_status": "Met", + "discussion_justification": "GitHub issues and pull requests provide public, searchable, URL-addressable discussion.", + "english_status": "Met", + "english_justification": "Project documentation, issue templates, and contribution routes accept English.", + "maintained_status": "Met", + "maintained_justification": "The public repository has current commits, releases, issues, and pull requests.", + "repo_public_status": "Met", + "repo_public_justification": "The source repository is public at https://github.com/RAESystem/rae.", + "repo_track_status": "Met", + "repo_track_justification": "The project uses Git and preserves public history on GitHub.", + "repo_interim_status": "Met", + "repo_interim_justification": "Development work is visible on the public dev branch between releases.", + "version_unique_status": "Met", + "version_unique_justification": "Each published package release has a unique PEP 440 version.", + "version_semver_status": "Met", + "version_semver_justification": "Release Please manages semantic versions from Conventional Commit titles.", + "version_tags_status": "Met", + "version_tags_justification": "Release Please creates a matching Git tag and GitHub release.", + "release_notes_status": "Met", + "release_notes_justification": "Release Please publishes human-readable GitHub release notes and maintains CHANGELOG.md.", + "report_tracker_status": "Met", + "report_tracker_justification": "GitHub Issues is the public defect tracker.", + "report_archive_status": "Met", + "report_archive_justification": "GitHub archives and indexes issue reports and their discussion.", + "vulnerability_report_process_status": "Met", + "vulnerability_report_process_justification": "SECURITY.md explains how to report a suspected vulnerability.", + "vulnerability_report_private_status": "Met", + "vulnerability_report_private_justification": "SECURITY.md directs reporters to GitHub private vulnerability reporting or a private maintainer contact.", + "test_status": "Met", + "test_justification": "CONTRIBUTING.md documents the canonical nox verification command.", + "test_invocation_status": "Met", + "test_invocation_justification": "The same locked nox command runs locally and in CI.", + "continuous_integration_status": "Met", + "continuous_integration_justification": "GitHub Actions runs repository policy, tests, docs, security, and conformance checks.", + "code_of_conduct_status": "Met", + "code_of_conduct_justification": "The project policy is at https://github.com/RAESystem/rae/blob/main/CODE_OF_CONDUCT.md.", + "governance_status": "Met", + "governance_justification": "The maintainer-led decision and release model is at https://github.com/RAESystem/rae/blob/main/GOVERNANCE.md.", + "bus_factor_status": "Unmet", + "bus_factor_justification": "RAES currently has one maintainer, so its bus factor is below two.", + "two_person_review_status": "Unmet", + "two_person_review_justification": "The single-maintainer project does not require independent two-person review for every change." +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 246f5ee8d..33ec16d4c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- -name: Bug Report -about: Report a bug in RAES SDL +name: Bug report +about: Report a reproducible problem in RAES title: "[BUG] " labels: bug assignees: '' @@ -10,17 +10,17 @@ assignees: '' -## Steps to Reproduce +## Steps to reproduce 1. 2. 3. -## Expected Behavior +## Expected behavior -## Actual Behavior +## Actual behavior @@ -31,6 +31,6 @@ assignees: '' - Python version: - OS: -## Minimal Input +## Minimal input diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..fffd98f04 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security report + url: https://github.com/RAESystem/rae/security/advisories/new + about: Report a suspected vulnerability privately. + - name: Support guide + url: https://github.com/RAESystem/rae/blob/main/SUPPORT.md + about: Choose the right route and include a small example. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 71957eaa1..a43a2b0cb 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,6 @@ --- -name: Feature Request -about: Suggest a feature for RAES SDL +name: Feature request +about: Propose a focused improvement to RAES title: "[FEATURE] " labels: enhancement assignees: '' @@ -10,18 +10,18 @@ assignees: '' -## Use Case +## Use case -## Proposed Solution +## Proposed solution -## Alternatives Considered +## Alternatives considered -## Compatibility Notes +## Compatibility notes diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ded0d8e0e..c3c988e9e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,34 +2,28 @@ -## Related Issues +## Related issues -## Requirement Context - -- Requirement UID: -- ADRs touched: -- Ground Control project: `raes-sdl` - ## Changes - -## Test Plan +## Test plan - [ ] Relevant tests pass -- [ ] `uvx nox -s verify` passes, or the unchecked sessions are listed below -- [ ] Docs build passes when documentation changed (`uvx nox -s docs`) +- [ ] The canonical `nox -s verify` gate passes, or unchecked sessions are listed below +- [ ] The docs gate passes when public documentation changed ## Checklist -- [ ] Code follows project coding standards (`docs/explain/reference/coding-standards.md`) +- [ ] Code follows the [project coding standards](../docs/explain/reference/coding-standards.md) - [ ] FM level classified if semantic change - [ ] Published contract schemas regenerated if models changed - [ ] PR title is a Conventional Commit (release-please derives the version and `CHANGELOG.md` from it) - [ ] Architectural docs updated if applicable -## Notes for Review +## Notes for review diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3acdef21a..28940e1bf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,7 @@ name: Docs -# Build the Sphinx documentation on every PR (as a breakage check) and publish it -# to GitHub Pages on pushes to the default branch. Pages must be enabled with the -# "GitHub Actions" build source (Settings -> Pages) for the deploy job to succeed. +# Validate the curated public docs on pull requests and publish that same output +# to GitHub Pages from main. on: push: @@ -23,15 +22,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v8 - - name: Build Sphinx docs - run: | - uv sync --extra docs --directory implementations/python - implementations/python/.venv/bin/sphinx-build -W --keep-going -b html docs docs/_build/html + - name: Validate and build public docs + run: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s docs - name: Upload Pages artifact if: github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000..78cee0dea --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,52 @@ +name: OpenSSF Scorecard + +on: + branch_protection_rule: + schedule: + - cron: "17 3 * * 1" + push: + branches: [main] + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + id-token: write + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2 + with: + egress-policy: audit + + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Run Scorecard + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: "true" + + - name: Upload Scorecard artifact + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: scorecard-results + path: results.sarif + retention-days: 5 + if-no-files-found: error + + - name: Upload Scorecard results to code scanning + if: ${{ always() }} + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 + with: + sarif_file: results.sarif diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 76a2b2558..d44e05bd3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,13 +9,13 @@ build: python: "3.13" sphinx: - configuration: docs/conf.py + configuration: docs/public/conf.py fail_on_warning: true python: install: - method: uv - command: sync + command: sync --frozen path: implementations/python extras: - docs diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 000000000..b13ce89fe --- /dev/null +++ b/.vale.ini @@ -0,0 +1,10 @@ +StylesPath = styles +MinAlertLevel = error +Vocab = RAES + +[*.{md,rst}] +BasedOnStyles = RAES +Vale.Spelling = NO +Vale.Terms = NO +Vale.Avoid = NO +Vale.Repetition = error diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 87be00cf5..f024fe594 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,37 +1,33 @@ -# Code of Conduct +# Code of conduct -## Expected Behavior +## Expected behavior -Participants are expected to keep discussion technical, concrete, and -respectful. Good-faith disagreement about language design, implementation -tradeoffs, and research claims is welcome. +Keep project discussions clear, kind, and focused on the work. Good-faith +disagreement is welcome. Expected behavior includes: -- focusing criticism on ideas, code, evidence, and project goals -- giving enough context for others to understand a concern -- accepting that maintainers may reject changes that do not fit the project -- avoiding personal attacks, harassment, and discriminatory language -- not publishing private information without permission +- Focus criticism on ideas, code, evidence, and project goals. +- Give enough context for others to understand a concern. +- Accept that the maintainer may reject work that does not fit the project. +- Do not use personal attacks, harassment, or biased language. +- Do not publish private information without permission. ## Scope -This code of conduct applies to project spaces, including issues, pull -requests, discussions, documentation contributions, and other repository-linked -collaboration. +This code applies to issues, pull requests, docs work, and other spaces linked +to the project. -## Reporting +## Report a concern -Report conduct concerns privately to the maintainer through the contact path -listed on Brad Edwards' GitHub profile. +Report a concern to the maintainer through the private contact path on Brad +Edwards' GitHub profile. -The project is maintained by a sole maintainer, so there is no independent -enforcement committee. Reports will be handled as practically and fairly as -possible, including removing comments, closing threads, declining -contributions, or blocking participants when needed. +The project has one maintainer and no separate enforcement group. The +maintainer may remove comments, close threads, decline work, or block a person +when needed. ## Attribution -This document is intentionally minimal and project-specific. It is informed by -common open-source conduct policies, including the Contributor Covenant, but it -is not a verbatim copy. +This project-specific policy draws from common open source conduct policies, +including the Contributor Covenant. It is not a full copy. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1e807613..35032ca08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,21 @@ -# Contributing to RAES SDL +# Contribute to RAES RAES SDL is a research-oriented engineering project. Contributions are useful when they make the language, reference implementation, contracts, examples, or documentation more precise and easier to validate. -## Before Opening a Pull Request +## Choose the right route -- For small documentation fixes, typo fixes, and narrow test improvements, a - pull request is enough. +- For a small docs fix, typo fix, or narrow test improvement, a pull request + is enough. - For SDL language changes, contract changes, processor behavior changes, or backend conformance changes, open an issue first. Those changes can affect authored scenario meaning and generated artifacts. -- Keep unrelated changes in separate pull requests. +- Keep unrelated work in separate pull requests. - Base pull requests on `dev`, not `main`. `main` is the stable release line; `dev` is the integration branch. -## Development Setup +## Set up the repository Prerequisites: @@ -23,42 +23,45 @@ Prerequisites: - [uv](https://github.com/astral-sh/uv) - [nox](https://nox.thea.codes/) or `uvx nox` -Set up the Python implementation: +Install the locked Python environment: ```shell git clone https://github.com/RAESystem/rae.git -cd raes/implementations/python -uv sync --all-extras +cd rae +uv sync --project implementations/python --all-extras --frozen ``` -## Making Changes +The [developer documentation index](docs/README.md) links to architecture, +research, migration, release, and workflow records that are not part of the +hosted reader guide. + +## Make a change 1. Fork the repository and create a branch from `dev`. 2. Make the smallest coherent change that solves the issue. 3. Add or update tests when behavior changes. 4. Update examples, schemas, contracts, or documentation when the public surface changes. -5. For documentation changes, follow +5. For public docs changes, follow [`docs/explain/reference/documentation-style-guide.md`](docs/explain/reference/documentation-style-guide.md). -6. Use a [Conventional Commit](https://www.conventionalcommits.org/) PR title - (for example `feat:` or `fix:`): release-please derives the version bump and - the `CHANGELOG.md` entry from it. Do not hand-edit `CHANGELOG.md`. +6. Use a [Conventional Commit](https://www.conventionalcommits.org/) PR title, + such as `feat:` or `fix:`. Release Please reads that title. 7. Run the relevant checks locally. 8. Open a pull request against `dev` with a concrete description of what changed and why. -## Verification +## Run the checks The full repository gate is: ```shell -uvx nox -s verify +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify ``` Run the change-aware local gate while iterating: ```shell -uvx nox -s verify-changed +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify-changed ``` It selects from status-aware changes against the branch's upstream ref and @@ -69,15 +72,15 @@ unconditional pull-request gate. Useful narrower sessions: ```shell -uvx nox -s tests -uvx nox -s docs -uvx nox -l +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s tests +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s docs +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -l ``` Run the full gate before requesting review for language, contract, generated artifact, or shared runtime changes. -## Changelog +## Let Release Please write the changelog `CHANGELOG.md` is generated by [release-please](https://github.com/googleapis/release-please) from the @@ -85,11 +88,11 @@ Conventional Commit history on `main`; do not hand-edit it or add changelog fragments. The Conventional Commit PR title is the entry release-please reads. See [`docs/explain/releasing.md`](docs/explain/releasing.md). -## Security Reports +## Report security issues privately Do not open public issues for suspected security vulnerabilities. See [SECURITY.md](SECURITY.md). -## Community Expectations +## Follow the community rules Participation in this project is covered by [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 000000000..1dd4693df --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,35 @@ +# Project governance + +RAES is a maintainer-led open source project. The maintainer sets project +scope, accepts changes, manages releases, and protects the repository's +normative specifications and contracts. + +## Propose a change + +Open an issue before changing RAES SDL meaning, published contracts, processor +behavior, backend conformance, or project governance. Describe the user need, +current boundary, examples, compatibility effect, and evidence that can test +the change. + +Small docs, test, and defect fixes can start as pull requests against `dev`. +The maintainer may ask for an issue when review reveals a wider design choice. + +## Make a decision + +The maintainer decides whether a change fits the project and has enough +evidence. Durable architecture choices are recorded as architecture decision +records. Normative meaning remains in the approved specification and contract +surfaces. + +The project welcomes review but does not require a second maintainer or +independent approval for every change. This keeps the current single-maintainer +model honest and usable. + +## Release a change + +Pull requests target `dev`. Release Please prepares release changes on `main` +from Conventional Commit titles. It owns package versions, release notes, and +`CHANGELOG.md`. + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the working steps and +[MAINTAINERS.md](MAINTAINERS.md) for current ownership. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 000000000..ec8c9083f --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,13 @@ +# Project maintainers + +RAES currently has one maintainer: + +- Brad Edwards — [GitHub](https://github.com/Brad-Edwards) + +The maintainer reviews contributions, manages releases, handles private +security reports, and makes final scope decisions. + +This file describes the current project state. It does not promise response +times, independent review, or a path to maintainer status. Material governance +changes are proposed through a public issue and recorded in +[GOVERNANCE.md](GOVERNANCE.md). diff --git a/README.md b/README.md index 9d447182b..695bdcb40 100644 --- a/README.md +++ b/README.md @@ -1,276 +1,187 @@ # Reproducible Agentic Environments System -Reproducible Agentic Environments System (RAES) is a system for describing, -realizing, controlling, evaluating, and reproducing agentic environments. An -agentic environment is a declared and realized setting in which participants -receive observations, take actions, interact with resources or other -participants, and are evaluated under stated controls. Participants can -include software agents, policies, scripts, and human-control proxies. - -RAES supports a bounded reproduction attempt by connecting authored intent, -deterministic or governed variation, realization inputs, participant behavior, -observations, apparatus identity, provenance, evidence, replay boundaries, and -conformance results. It does not guarantee deterministic runtime behavior, -equal outcomes, exact replay, scientific validity, or reproducibility. +[![CI](https://github.com/RAESystem/rae/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/RAESystem/rae/actions/workflows/ci.yml) +[![Docs](https://github.com/RAESystem/rae/actions/workflows/docs.yml/badge.svg?branch=main)](https://raesystem.github.io/rae/) +[![PyPI](https://img.shields.io/pypi/v/raes.svg)](https://pypi.org/project/raes/) +[![Python](https://img.shields.io/pypi/pyversions/raes.svg)](https://pypi.org/project/raes/) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/RAESystem/rae/blob/main/LICENSE) -Cyber, AI security, AI safety, testing, research, and evaluation are -non-exhaustive application areas. The general model can support additional -domains through domain-specific examples, controlled vocabularies, semantic -profiles, reusable assets, backend profiles, and evidence requirements. - -The current repository materializes RAES through its Scenario Description -Language (SDL), Python reference implementation, published contracts, examples, -and assurance material. RAES names the overall system; RAES SDL is the authored -scenario language within it. - -The repository separates authored scenario meaning from processors, backends, -participant implementations, runtime state, and archived evidence. In the -current implementation, an SDL document can be parsed, validated, -instantiated, compiled into runtime models, and checked against published -backend contracts without binding the authored scenario to one cloud, range -implementation, or execution harness. - -This is an academic and engineering project. The repository is intended to be -read, tested, and used as reference implementation code, not treated as a -managed service. - -The repository is not a managed environment service and does not include a -production backend. Backend contracts, stubs, conformance checks, and examples -are present; real deployment backends remain separate implementations. Cyber is -the strongest current example and lineage base, not the boundary of the core. - -A worked example of RAES SDL driving a concrete range is -[APTL (Advanced Purple Team Lab)](https://github.com/Brad-Edwards/aptl), a -separate project that specifies its scenarios as RAES SDL documents and -realizes the selected topology on a Docker Compose backend. - -## Contents - -- [Agentic Environments And RAES SDL](#agentic-environments-and-raes-sdl) -- [Getting Started](#getting-started) -- [Using the Python Reference Implementation](#using-the-python-reference-implementation) -- [Repository Layout](#repository-layout) -- [Lineage](#lineage) -- [Documentation](#documentation) -- [Verification](#verification) -- [Contributing](#contributing) -- [Versioning](#versioning) -- [Citation](#citation) -- [License](#license) -- [Maintainer](#maintainer) - -## Agentic Environments And RAES SDL - -RAES SDL records authored scenario and experiment intent. An SDL file can -describe topology, hosts, services, identities, content, relationships, agents, -objectives, workflows, variables, and evaluation material without directly -describing a specific backend's infrastructure primitives. Processors, -backends, participant implementations, and runtime choices turn that authored -scenario into a realized environment; the realization is not identical to the -SDL document. +RAES, the Reproducible Agentic Environments System, helps you describe and +check an agentic environment. RAES SDL is its YAML language for authored +scenarios. -```yaml -name: hospital-ransomware-surgery-day -description: Surgery-day ransomware exercise for a regional hospital. +You can use RAES to record nodes, links, participants, objectives, workflows, +variation, and evidence needs without tying the scenario to one deployment +backend. -variables: - surgery_day_speed: - type: number - default: 1.0 +## Validate your first scenario + +You need Python 3.11 or newer. + +```console +python -m venv .venv +source .venv/bin/activate +python -m pip install raes +``` + +Save this file as `first-scenario.sdl.yaml`: + + +```yaml +name: first-scenario +description: A small network with one Linux host. nodes: - internet-edge: + lab-network: type: Switch - description: Public ingress for email, VPN, and external access - - mail-gateway: + web: type: VM os: linux - source: secure-mail-gateway - resources: {ram: 2 gib, cpu: 1} - services: - - {port: 25, name: smtp-inbound} - roles: {mail-admin: postfix} + resources: + ram: 2 GiB + cpu: 1 + +infrastructure: + lab-network: + count: 1 + properties: + cidr: 10.0.0.0/24 + gateway: 10.0.0.1 + web: + count: 1 + links: + - lab-network ``` + -Complete examples live in [`examples/scenarios/`](https://github.com/RAESystem/rae/tree/main/examples/scenarios). -Reusable non-normative templates and patterns are indexed by -[`examples/library/catalog.yaml`](https://github.com/RAESystem/rae/blob/main/examples/library/catalog.yaml). - -## Getting Started +Validate it: -Prerequisites: +```console +python - <<'PY' +from pathlib import Path +from raes import parse_sdl_file -- Python 3.11 or newer -- [uv](https://github.com/astral-sh/uv) -- [nox](https://nox.thea.codes/) for the repository verification graph, or - `uvx nox` without a separate install +scenario = parse_sdl_file(Path("first-scenario.sdl.yaml")) +print(f"Validated {scenario.name} with {len(scenario.nodes)} nodes.") +PY +``` -Set up the Python reference implementation: +The command prints: -```shell -git clone https://github.com/RAESystem/rae.git -cd rae/implementations/python -uv sync --all-extras -uv run raes --help +```text +Validated first-scenario with 2 nodes. ``` -## Using the Python Reference Implementation +RAES has checked the file shape and current semantic rules. It has not created +infrastructure. Continue with the +[quickstart](https://raesystem.github.io/rae/quickstart.html) to learn what +each part means. + +## Choose your route + +- **Scenario authors:** Start with the + [SDL guide](https://raesystem.github.io/rae/sdl/) and + [worked examples](https://github.com/RAESystem/rae/tree/main/examples/scenarios). +- **Python users:** Use the + [Python guide](https://raesystem.github.io/rae/guides/python.html) and + [API reference](https://raesystem.github.io/rae/api/). +- **CLI users:** See the + [command-line guide](https://raesystem.github.io/rae/guides/cli.html). +- **Backend implementers:** Read the + [backend and conformance guide](https://raesystem.github.io/rae/backends.html). +- **Researchers:** Review the + [research context](https://raesystem.github.io/rae/research.html), + [current limits](https://raesystem.github.io/rae/limitations.html), and + [citation guide](https://raesystem.github.io/rae/citation.html). +- **Contributors:** Follow + [CONTRIBUTING.md](https://github.com/RAESystem/rae/blob/main/CONTRIBUTING.md) + and the + [developer documentation index](https://github.com/RAESystem/rae/blob/main/docs/README.md). + +## Understand what RAES promises + +An authored scenario records intent. A processor and backend may turn supported +parts of that intent into runtime resources. Reports and evidence show what was +accepted, changed, observed, or left unsupported. + +RAES can support a bounded reproduction attempt. It does not promise +deterministic runtime behavior, equal outcomes, exact replay, scientific +validity, or reproducibility. + +The repository does not include a production deployment backend or a managed +environment service. It includes contracts, stubs, examples, conformance +checks, and reference code. Read the +[current limits](https://raesystem.github.io/rae/limitations.html) before +choosing it for a study or integration. + +## See where RAES fits -Parse and validate a scenario from Python: +Cyber, AI security, AI safety, testing, research, and evaluation are +non-exhaustive application areas. Additional domains can add their own +profiles, assets, examples, vocabularies, backends, and evidence rules. -```python -from pathlib import Path +RAES separates these concerns: -from raes import parse_sdl_file +- **Authored scenario:** The meaning written in RAES SDL. +- **Processor:** The code that validates, expands, and compiles that meaning. +- **Backend:** The implementation that accepts a supported runtime request. +- **Runtime:** The resources and participant activity that occur during a run. +- **Evidence:** The records used to state and inspect a bounded result. -scenario = parse_sdl_file( - Path("../../examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml") -) +The strongest current examples come from cyber ranges and agent evaluation. +The core model is not limited to those areas. -for advisory in scenario.advisories: - print(advisory) -``` +## Work from a repository checkout -Run the CLI from `implementations/python`: +Install the locked development environment: -```shell -uv run raes sdl resolve ../../examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml -uv run raes sdl verify-imports ../../examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml -uv run raes sdl publish ../../examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml -uv run raes processor --help -uv run raes conformance --help -uv run raes-mcp +```console +git clone https://github.com/RAESystem/rae.git +cd rae +uv sync --project implementations/python --all-extras --frozen +uv run --project implementations/python raes --help ``` -## Repository Layout - -- `specs/` - normative prose and formal specification material -- `contracts/` - published schemas, fixtures, manifests, and profiles -- `implementations/` - reference implementations and their local tooling -- `examples/` - worked SDL scenario examples plus reusable authoring templates and patterns -- `docs/` - explanatory documentation, API docs, and architecture decisions -- `research/` - supporting literature and reference ecosystem material -- `tools/` - repository maintenance, policy, and publication tooling - -## Lineage - -- [Open Cyber Range SDL](https://documentation.opencyberrange.ee/docs/sdl/reference/) -- [Open Cybersecurity Schema Framework](https://schema.ocsf.io/) -- [CACAO Security Playbooks v2.0](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html) -- [STIX 2.1](https://docs.oasis-open.org/cti/stix/v2.1/stix-v2.1.html) -- [CybORG](https://github.com/cage-challenge/CybORG) -- [TENA](https://www.trmc.osd.mil/tena-about.html) -- [IEEE High Level Architecture](https://standards.ieee.org/standard/1516-2025.html) -- [SISO Cyber DEM](https://cdn.ymaws.com/www.sisostandards.org/resource/resmgr/standards_products/siso-std-025-2023_cyberdem.pdf) -- [SISO Cyber FOM](https://www.sisostandards.org/news/690125/Publication-of-Cyber-FOM-and-SIRL-Users-Guide.htm) -- [MITRE CALDERA](https://github.com/mitre/caldera) -- [Atomic Red Team](https://github.com/redcanaryco/atomic-red-team) - -For a dimension-by-dimension comparison against these systems — what RAES -expresses that they do not, and where they still lead RAES — see -[Related-Work Comparison](https://github.com/RAESystem/rae/blob/main/docs/explain/sdl/related-work-comparison.md). - -## Documentation - -The documentation source is under [`docs/`](https://github.com/RAESystem/rae/tree/main/docs). Important entry points: - -- [`docs/index.md`](https://github.com/RAESystem/rae/blob/main/docs/index.md) - documentation index -- [`docs/explain/getting-started.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/getting-started.md) - use-case and rigor-level entrypoint -- [`docs/migration/raes-rename.md`](https://github.com/RAESystem/rae/blob/main/docs/migration/raes-rename.md) - RAES rename hard-cutover map -- [`examples/README.md`](https://github.com/RAESystem/rae/blob/main/examples/README.md) - current worked example inventory -- [`examples/library/catalog.yaml`](https://github.com/RAESystem/rae/blob/main/examples/library/catalog.yaml) - template and pattern library catalog -- [`docs/explain/reference/canonical-reference-map.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/reference/canonical-reference-map.md) - current reference map -- [`docs/explain/reference/documentation-style-guide.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/reference/documentation-style-guide.md) - documentation style and citation rules -- [`docs/explain/reference/glossary.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/reference/glossary.md) - current terminology -- [`docs/explain/sdl/index.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/sdl/index.md) - SDL guide -- [`docs/explain/sdl/runtime-architecture.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/sdl/runtime-architecture.md) - runtime architecture -- [`docs/explain/reference/backend-conformance.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/reference/backend-conformance.md) - backend conformance model -- [`docs/decisions/adrs/README.md`](https://github.com/RAESystem/rae/blob/main/docs/decisions/adrs/README.md) - architecture decisions -- [`contracts/README.md`](https://github.com/RAESystem/rae/blob/main/contracts/README.md) - contract publication surface - -## Verification - -`nox` is the canonical verification graph. From the repository root: - -```shell -uvx nox -s verify -uvx nox -s verify-changed -uvx nox -s tests -uvx nox -l -``` +Run the canonical verification graph: -The full `verify` session is unconditional and runs the project checks expected -for pull requests, including repository policy, governed evidence and generated -artifacts, parallel worker-safe test coverage, serial integration tests, and -docs. Use `verify-changed` while iterating: it compares the branch and working -tree with the upstream ref, skips evidence and regression stages only for -allowlisted prose or research-only changes, and fails closed to the full local -gate for unknown, deleted, renamed, executable, contract, or configuration -changes. The pre-push hook uses the same change-aware plan. - -Whole-scenario finite-domain satisfiability can be inspected without applying -or provisioning a scenario: - -```shell -uv run --project implementations/python raes processor satisfiability \ - path/to/scenario.sdl.yaml \ - --profile raes-finite-domain-satisfiability-v1 +```console +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify ``` -The command emits the published replayable evidence envelope. Exit `0` is a -completed satisfiable or unsatisfiable analysis, `2` is an explicit unsupported -fragment result, and `1` is an input or operational failure. See ADR-086 for the -bounded target coverage and nonclaims. +Useful repository roots are: -## Contributing +- `docs/public/` for hosted reader documentation; +- `docs/README.md` for developer and working records; +- `specs/` for normative specifications; +- `contracts/` for published schemas and fixtures; +- `examples/` for authored scenarios and reusable patterns; +- `implementations/python/` for the reference implementation. -Contributions are welcome where they improve the language, reference -implementation, contracts, tests, examples, or documentation. Start with -[CONTRIBUTING.md](https://github.com/RAESystem/rae/blob/main/CONTRIBUTING.md). +## Project status -Language and contract changes should be discussed before implementation because -small SDL changes can affect validation, generated schemas, backend -conformance, and existing scenario examples. +RAES is an academic and engineering project with one maintainer. Contributions +are welcome. The project does not require a second maintainer or independent +reviewer for every change. -## Versioning +Release Please owns package versions, GitHub releases, and `CHANGELOG.md`. +Published schemas carry separate stability labels. See +[GOVERNANCE.md](https://github.com/RAESystem/rae/blob/main/GOVERNANCE.md) and +[MAINTAINERS.md](https://github.com/RAESystem/rae/blob/main/MAINTAINERS.md) for +the current decision and maintenance model. -The Python package version lives in -[`implementations/python/packages/raes/_version.py`](https://github.com/RAESystem/rae/blob/main/implementations/python/packages/raes/_version.py) -and is bumped by [release-please](https://github.com/googleapis/release-please) -from the Conventional Commit history on `main`, which also generates -`CHANGELOG.md`. Do not hand-edit the version or `CHANGELOG.md`. See -[`docs/explain/releasing.md`](https://github.com/RAESystem/rae/blob/main/docs/explain/releasing.md). - -Published JSON Schemas use versioned contract identifiers such as -`sdl-authoring-input-v1`, but the suffix is not the same as a stability promise. -The authoritative schema publication index assembles independent per-contract -records, each carrying its schema's `draft` or `stable` stability class and -canonical content hash. Current checked-in schemas -are draft until a maintainer explicitly promotes them; stable breaking changes -must mint a new schema version as described in -[ADR-061](https://github.com/RAESystem/rae/blob/main/docs/decisions/adrs/adr-061-published-schema-evolution-policy.md). - -## Maintainers - -- Brad Edwards — [Personal GitHub](https://github.com/Brad-Edwards), [PANW GitHub](https://github.com/Brad-Edwards-SecOps), [LinkedIn](https://www.linkedin.com/in/bradley-edwards-dev/) - -## Citation - -If you use RAES in academic work, cite the system: +## Cite RAES ```bibtex @software{raes, - author = {Edwards, Brad}, - title = {RAES: Reproducible Agentic Environments System}, - year = {2026}, - license = {MIT}, - url = {https://github.com/RAESystem/rae} + author = {Edwards, Brad}, + title = {RAES: Reproducible Agentic Environments System}, + year = {2026}, + license = {MIT}, + url = {https://github.com/RAESystem/rae} } ``` -## License - -Released under the MIT License. See [LICENSE](https://github.com/RAESystem/rae/blob/main/LICENSE). -Third-party attribution and license notices are recorded in +RAES is released under the +[MIT License](https://github.com/RAESystem/rae/blob/main/LICENSE). Third-party +notices are in [THIRD_PARTY_NOTICES.md](https://github.com/RAESystem/rae/blob/main/THIRD_PARTY_NOTICES.md). diff --git a/SECURITY.md b/SECURITY.md index b0102dd47..700daaa77 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,13 +1,12 @@ -# Security Policy +# Security policy -## Reporting a Vulnerability +## Report a vulnerability Do not report suspected vulnerabilities through public GitHub issues. -Use GitHub private vulnerability reporting for this repository if it is -available. If private reporting is not available, contact the maintainer -privately through the contact path listed on Brad Edwards' GitHub profile and -include `RAES SDL security report` in the subject or first line. +Use GitHub private vulnerability reporting when it is available. If it is not, +contact the maintainer through the private path on Brad Edwards' GitHub +profile. Put `RAES SDL security report` in the subject or first line. Include enough detail to reproduce and assess the issue: @@ -18,7 +17,7 @@ Include enough detail to reproduce and assess the issue: - impact - proof of concept or logs, if available -## Scope +## Check the scope Security reports are most useful for issues in: @@ -30,55 +29,14 @@ Security reports are most useful for issues in: - runtime control-plane code - repository automation that handles untrusted input -This repository also contains research material and reference ecosystem -material. Reports against archived third-party material may be documented here, -but fixes usually need to happen upstream. +The repository also holds research and third-party reference material. A fix +for archived third-party material often belongs in the upstream project. -## Response Expectations +## Know what to expect -RAES SDL is maintained by a sole maintainer. There is no formal security -response SLA. Reports will be reviewed on a best-effort basis, with priority -given to reproducible issues that affect current code, published contracts, or -documented workflows. +RAES has one maintainer and no security response SLA. Reports are reviewed as +time allows. A small example helps the maintainer assess current code, +published contracts, and documented workflows. -Please avoid publishing exploit details until there has been reasonable time to -triage and prepare a fix or mitigation. - -## Commit Authorship and Signing - -Commits are signed with the maintainer's SSH key (ED25519, -`SHA256:fdBpsrHmMxkK9DikzdhtWINNcTLkhmdsYrHK5cIMa/o`) under the identity -`Brad Edwards `. - -### Authorship anomaly, 2026-07-01 to 2026-07-05 - -36 commits in this window were authored as the placeholder identity -`Test ` and appear as **Unverified** on GitHub. They are not -unsigned or forged: each is signed by the maintainer's SSH key above and -verifies locally — `git log --show-signature` reports a good signature for -`j.bradley.edwards@gmail.com`. GitHub withholds the Verified badge only -because `t@example.com` is not a verified email on the account. - -**Cause.** A repository-local `[user]` override -(`user.email=t@example.com`, `user.name=Test`) was written into the shared -`.git/config` during a pre-push recovery on 2026-07-01 ~20:15 (recovery -branch `local/prepush-base-damage-20260701-201555`). A history replay in -that recovery re-created ~13 commits in one second at 20:15:53 under the -placeholder identity; the override then shadowed the correct global identity -for subsequent local commits until it was found. - -**Scope.** Limited to the author/committer identity fields. No other local -configuration was altered — no `url.*.insteadOf` push redirection, no local -`core.hooksPath`, no `core.sshCommand`, no `credential.helper`. - -**Affected commits.** Author `Test `, from `cf4cdaa` -(2026-07-01 20:15:53) through `a1fb96e` (2026-07-05 06:33:42). Enumerate -with `git log --all --author='t@example.com' --format='%H %cI'`. Merge -commits created by GitHub in this window are separately signed by GitHub's -web-flow key and are Verified; they are not part of this set. - -**Remediation.** The local override was removed on 2026-07-05 -(`git config --local --remove-section user`); identity now resolves to the -correct global values. History was not rewritten: the affected commits are -merged into shared branches, so the placeholder author remains as the -historical record and this note is the durable explanation. +Avoid publishing exploit details until the maintainer has had reasonable time +to assess the report and prepare a fix or mitigation. diff --git a/SUPPORT.md b/SUPPORT.md index 30acc0f89..8519f48d9 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,16 +1,14 @@ -# Support +# Get support RAES SDL is community supported and maintained on a best-effort basis. -## Where to Ask +## Choose a route - Use GitHub issues for reproducible bugs, documentation problems, and focused feature requests. -- Use GitHub discussions if they are enabled for design questions or broader - usage questions. - For security issues, do not open a public issue. See [SECURITY.md](SECURITY.md). -## What to Include +## Include a small example For a bug report, include: @@ -28,7 +26,7 @@ For a language or contract proposal, include: - the affected SDL sections, schemas, contracts, or backend expectations - examples of documents that should become valid or invalid -## Maintenance Expectations +## Know what to expect The project is maintained by a sole maintainer. Issues and pull requests are handled as time permits. Well-scoped reports with runnable examples are the diff --git a/docs/DEVELOPMENT_WORKFLOW.md b/docs/DEVELOPMENT_WORKFLOW.md new file mode 100644 index 000000000..1dc0667a1 --- /dev/null +++ b/docs/DEVELOPMENT_WORKFLOW.md @@ -0,0 +1,44 @@ +# Development workflow + +Use this page to find the canonical local checks. Public contribution steps are +in [`CONTRIBUTING.md`](../CONTRIBUTING.md). + +## Install the locked environment + +From the repository root: + +```shell +uv sync --project implementations/python --all-extras --frozen +``` + +## Run verification + +The full pull-request gate is: + +```shell +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify +``` + +Use `verify-changed` while you work. It selects a fail-closed subset from the +branch diff. Unknown, source, deleted, renamed, contract, and configuration +changes run the full graph. + +The docs session checks the curated source boundary, the RAES Vale style, +warning-strict Sphinx HTML, generated route and search inventories, and links: + +```shell +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s docs +``` + +## Release model + +Release Please owns `CHANGELOG.md`, package versions, GitHub releases, and the +release pull request. A feature pull request uses a Conventional Commit title +because its squash-merge title becomes the commit that Release Please reads. +Do not edit the changelog or version by hand. + +## Review records + +Ground Control stores implementation plans, review findings, readiness, and +traceability records for governed work. These developer records stay outside +the hosted public documentation source. diff --git a/docs/Makefile b/docs/Makefile index 69674a595..a73a1100d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build -SOURCEDIR = . +SOURCEDIR = public BUILDDIR = _build .PHONY: help clean html livehtml diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..a7cbfbf52 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,24 @@ +# Developer documentation + +Hosted reader documentation lives only under [`docs/public/`](public/index.md). +The rest of `docs/` contains developer, design, research, migration, audit, and +working records that remain visible in the repository. + +## Work on the repository + +- [Development workflow](DEVELOPMENT_WORKFLOW.md) +- [Coding standards](explain/reference/coding-standards.md) +- [Documentation style guide](explain/reference/documentation-style-guide.md) +- [Release process](explain/releasing.md) +- [Architecture decisions](decisions/adrs/README.md) + +## Inspect working records + +- [Decision and preflight notes](decisions/index.md) +- [Research records](research/) +- [Migration records](migration/README.md) +- [Lessons](lessons/README.md) +- [Commit authorship anomaly](development/authorship-anomaly-2026-07.md) + +Files outside `docs/public/` are not copied, included, or indexed by the hosted +Sphinx build. diff --git a/docs/decisions/issue-928-public-documentation-preflight.md b/docs/decisions/issue-928-public-documentation-preflight.md new file mode 100644 index 000000000..4ac603547 --- /dev/null +++ b/docs/decisions/issue-928-public-documentation-preflight.md @@ -0,0 +1,320 @@ +# Issue 928 Public Documentation Preflight + +Date: 2026-07-27 + +Issue: #928. Requirement: DOC-928. + +This note records the repository-wide boundaries for the public-documentation +and community-readiness work. It is design guidance, not an implementation +plan, a new documentation schema, or a change to RAES runtime behavior. + +## Architecture Decisions + +### Public source and developer records + +- `docs/public/` is the only Sphinx source root for the hosted site. Public + pages move beneath it while retaining their current source-root-relative + paths where practical; for example, `docs/explain/sdl/index.md` becomes + `docs/public/explain/sdl/index.md` and still publishes at + `/explain/sdl/index.html`. +- `docs/README.md` is the repository-facing developer-documentation index. + `CONTRIBUTING.md` links to it. ADRs, issue/preflight notes, research working + records, migrations, audits, search logs, and requirement/status snapshots + remain under `docs/` but outside `docs/public/`. +- The public root is the positive publication authority. A blacklist of + internal filenames may be defense in depth, but must not decide what is + public. Files outside the root are never copied, symlinked, included, or + globbed into a public build. +- Published URLs and repository source paths are separate compatibility + concerns. Preserve hosted URLs by preserving paths relative to the new + source root. Update repository-relative links and governed `source_refs` + after moves. Add an explicit redirect only when a hosted route actually + changes; do not redirect an intentionally unpublished working record back + into the public site. + +One `PUBLIC_DOCS_ROOT` constant in `noxfile.py` is the local build, Vale, and +publication-check parameter. `.github/workflows/docs.yml` calls the nox docs +session instead of repeating source and output paths. Read the Docs points its +Sphinx configuration directly into the same root because its configuration +schema cannot consume a Python constant. Adding a page beneath the root must +not require another allowlist edit. + +### Independent validation responsibilities + +Keep the existing validation layers distinct: + +- Sphinx owns MyST/reStructuredText parsing, autodoc, toctrees, references, + and warning-strict HTML and link-check builds. +- Vale owns the mechanically enforceable subset of public prose style. +- `tools/check_project_positioning.py` continues to own RAES identity, + application-area framing, and evidence-bounded capability claims. Update its + registered paths after public files move; do not reproduce those semantic + checks as Vale vocabulary rules. +- One focused public-documentation checker owns source-root containment and + publication inventory. It derives allowed page routes and search-index + document names from files beneath `docs/public/`, known Sphinx-generated + routes, plus an explicit redirect map if redirects are later needed. It + rejects symlinks and source directives that escape the root, and verifies + that generated HTML/search artifacts contain no additional document routes. + It also rejects Sphinx-copied downloads whose source is outside the root; + public references to canonical specs/contracts use repository links rather + than silently copying those authorities into the hosted artifact. +- Existing example-library and SDL validators continue to own example meaning. + Quickstart and README snippets must be executed by focused tests against the + current CLI or Python entrypoint rather than acquiring a second SDL parser. + The current CLI has `format`, `resolve`, `verify-imports`, and `publish`, but + no generic `raes sdl validate` command. Use `parse_sdl`/`parse_sdl_file` for + the first validation success rather than adding a command to fit the prose. + +The publication checker follows the existing repository-policy convention: a +pure evaluator returns `tools.policy.common.PolicyFailure`; its CLI reuses the +existing text/JSON rendering and exception mechanism. It reads bounded UTF-8 +or structured inputs through containment-checked repository paths, performs no +network access, and reports rule id, path, and bounded messages rather than +file bodies. + +### Reader-first editorial boundary + +Stripe Documentation is the editorial exemplar for task-first, +example-led entry pages, not a source of RAES semantics, copied content, theme +assets, or a new documentation framework. + +- The README, overview, quickstart, tutorial entry points, SDL authoring entry, + CLI/Python entry, backend entry, research entry, and contributing/support + entry lead with a reader task and a current, tested example. +- Normative specifications, explanatory guidance, academic evidence, + reference material, and implementation status remain visibly distinct. + ADR-009, ADR-019, and `specs/authority/authority-boundary.yaml` remain the + authority boundary; moving or rewriting a page does not promote it. +- `docs/explain/reference/documentation-style-guide.md` is the incumbent prose + policy and becomes developer guidance outside the public root. Extend it + with the reader-first rules and the Stripe exemplar. Vale rules encode only + objective checks and point back to that guide. +- A repository-owned RAES Vale style and vocabulary may enforce spelling, + casing, prohibited promotional/process language, and other low-ambiguity + rules. Do not add broad bans on valid technical words such as `surface`, + `boundary`, `authority`, `bounded`, `cyber`, `replay`, or `deterministic`. + Do not fetch third-party Vale packages during a gate. + +Pin Vale in `tools/tool_versions.py` and provision it through the same +versioned, checksum-verified, repository-local cache pattern as gitleaks and +OSV-Scanner. Vale is a nox docs substage with `SessionReporter` output, not a +new pre-commit command, GitHub Actions command list, logger, or exception +hierarchy. + +### Publication and workflow topology + +- `_run_docs` in `noxfile.py` is the canonical docs graph. It runs the source + boundary check, Vale, warning-strict Sphinx HTML, public-output inventory, + focused executable documentation examples, and link checking. `verify`, + `verify-changed`, pre-push, CI, and the docs workflow must select that graph + consistently. +- Update `tools/verification_plan.py`: documentation configuration, Vale + configuration/styles, public-source moves, publication tooling, and docs + workflow changes must select the docs graph. The current full fallback has + `docs=False`; leaving that unchanged would let the riskiest documentation + changes skip documentation validation locally. +- `.readthedocs.yaml`, `docs/public/conf.py`, `docs/Makefile`, and the GitHub + Pages upload all use the public root and keep output outside it. Read the + Docs remains warning-strict and installs through the frozen uv lock. +- Search indexes and sitemaps are derivative publication artifacts, not + independent content sources. The same public source root governs production, + pull-request previews, GitHub Pages, search document names, and sitemap + routes. Do not maintain separate public lists for each publisher. +- `docs/public/conf.py` retains installed distribution metadata as the version + source and the honest `0.0.0+unknown` fallback. Do not introduce an + environment-controlled title, version, source root, or inclusion list. + +DOC-928 belongs in the existing `documentation-surfaces` requirement phase. +Extend that phase only for the focused publication/style tests and moved +public paths it must own; do not add a parallel documentation-governance phase. +Because the branch name does not contain a UID, governance commands use +`RAES_REQUIREMENT_UID=DOC-928` or `--requirement-uid DOC-928`, never the +requirement-free bypass. + +## Canonical Incumbents + +Implementation builds on these existing surfaces: + +- Documentation build and publication: `noxfile.py`, `.readthedocs.yaml`, + `.github/workflows/docs.yml`, `docs/Makefile`, `docs/conf.py`, and the + locked `docs` extra in `implementations/python/pyproject.toml` and + `implementations/python/uv.lock`. +- Verification and workflow composition: ADR-014, + `.ground-control.yaml`, `.gc/plan-rules.md`, `.pre-commit-config.yaml`, + `tools/verification_plan.py`, `tools/verify_all.py`, and + `.github/workflows/ci.yml`. +- Policy evaluation and errors: `tools.policy.common.PolicyFailure`, + `safe_repo_path`, bounded loaders, `tools/policy/exceptions.yaml`, + `tools/check_repo_policy.py`, and the temporary-root test pattern in + `implementations/python/tests/test_*_policy.py`. +- Claims and terminology: `tools/check_project_positioning.py`, + `docs/explain/reference/documentation-style-guide.md`, + `docs/explain/reference/glossary.md`, ADR-009, ADR-019, ADR-021, ADR-093, + `specs/agent-guidance/agent-guidance.yaml`, and + `specs/authority/authority-boundary.yaml`. +- Examples and commands: `examples/README.md`, + `examples/library/catalog.yaml`, `tools/check_example_library.py`, current + parser/CLI integration tests, and checked-in scenarios. A public example + points to or is tested against these authorities; it does not become a + parallel fixture corpus. +- Community and release workflow: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, + `SECURITY.md`, `SUPPORT.md`, existing issue/PR templates, + `.github/workflows/release-please.yml`, and `.github/dependabot.yml`. +- README packaging: root `README.md`, the Hatch metadata hook in + `implementations/python/hatch_build.py`, and installed-wheel tests. The root + README is also the PyPI long description; do not add a package-local copy. +- Supply-chain workflow conventions: SHA-pinned actions, explicit least- + privilege permissions, `GITHUB_TOKEN`, Dependabot updates for GitHub + Actions, OIDC where already required for publishing, and no repository PAT. + +Moving the public tree requires a whole-repository reference migration. +Hard-coded incumbents include `tools/check_project_positioning.py`, +`tools/check_sdl_lineage.py`, `specs/agent-guidance/agent-guidance.yaml`, +`examples/library/catalog.yaml`, example `source_refs`, root/community +Markdown links, formal-spec links, and their focused tests. Treat broken +governed references as migration defects; do not leave compatibility copies +outside the public root or duplicate pages to satisfy old paths. + +## Cross-Cutting Layers + +### Security and configuration + +- **GitHub authorization:** Scorecard uses the official OpenSSF workflow and + the workflow `GITHUB_TOKEN`. Start from `permissions: {}` or an equivalent + deny-by-default shape, then grant `contents: read` and only the documented + job-level `security-events`/`id-token` capabilities required for SARIF and + result publication. Keep checkout credentials non-persistent. Do not add a + PAT, `pull_request_target`, write access to repository contents, or an + approval rule that blocks the sole maintainer or release-please. +- **Release authorization:** `.github/workflows/release-please.yml` remains the + version, changelog, release, and `main`-to-`dev` authority. Repository rules + and required checks must continue to admit its `GITHUB_TOKEN`-created release + PR and the maintainer's documented release operation. +- **Read the Docs and Pages:** public builds consume checked-in source and the + frozen docs dependency set. No token, private repository content, environment + dump, or host path is injected into Sphinx configuration or generated pages. + Only the verified public output directory is uploaded. +- **Path and parser validation:** RTD YAML, GitHub workflow YAML, Vale config, + MyST/reStructuredText, Python Sphinx config, TOML/uv lock data, and any + redirect mapping pass their owning parsers. Publication tooling resolves + repository-relative paths, rejects absolute/parent traversal and symlink + escapes, bounds file size, and never executes documentation input. +- **Secret handling:** retain private-key detection and gitleaks in the + canonical hygiene graph. Do not publish `.env` examples with real values, + credentials, tokens, private URLs, prompts, hidden answers, raw backend + objects, or machine-specific paths. Synthetic SDL values remain distinct + from operator secrets. +- **OS-level exposure:** invoke Sphinx, Vale, and link checking with fixed argv + and repository-contained paths. Tokens and credentials never appear in argv, + command interpolation, artifacts, or error messages. Downloaded Vale + binaries are version pinned and checksum verified before execution. +- **Error envelopes and observability:** repository policy failures use + `PolicyFailure`; nox uses `SessionReporter`; Sphinx and Vale retain their + native bounded diagnostics; Scorecard emits its official SARIF and GitHub + annotations. Do not dump source bodies, environment values, tokens, + tracebacks, or complete external responses into a new result format. No new + application logger, audit stream, or exception class is needed. +- **Persistence:** checked-in sources, the uv lock, ephemeral build output, + GitHub Pages/Read the Docs pages, official Scorecard/SARIF publication, and + the bestpractices.dev project record are the only persistence surfaces. + There is no application database, DTO, repository class, migration, or + runtime configuration binding in this work. + +Enrollment on bestpractices.dev and repository-rule changes are authenticated +external administrative actions. Keep credentials in the provider's browser +or app authorization flow, verify resulting public state before adding a +badge, and record unmet criteria honestly. Do not make a hermetic repository +check depend on bestpractices.dev availability. + +Community-file revisions use the CNCF Project Template as a checklist, not a +source of fictional teams, meetings, elections, contributor ladders, +independent escalation bodies, CLAs/DCO enforcement, or response guarantees. +Keep one owner for each route: `CONTRIBUTING.md` for setup and submissions, +`SUPPORT.md` for questions, issue templates for bugs/features, `SECURITY.md` +for coordinated disclosure, and `CODE_OF_CONDUCT.md` for conduct. Move the +dated commit-authorship anomaly out of the public security policy and retain it +through the developer/audit index. Validate issue-template frontmatter and +referenced labels rather than copying labels that do not exist. + +### Extensibility seam + +The extension seam is placement beneath `PUBLIC_DOCS_ROOT`. + +- A new public page is curated by placing it under that root and linking it + from the public information architecture; source, search, sitemap, preview, + and Vale coverage follow without adding another publication allowlist. +- A new developer record is placed outside the root and linked from + `docs/README.md`; it remains repository-visible without entering a hosted + artifact. +- A real hosted route change adds one explicit old-to-new redirect entry. + Redirects are the only parameterized exception to the output route set. +- A new public root-level community entrypoint is added to the small + `PUBLIC_ENTRYPOINTS` style/test tuple. Do not recursively lint all developer, + historical, normative, or research prose to accommodate one new public file. + +## Gotchas and Anti-Patterns + +Avoid: + +- changing only the toctree or `exclude_patterns` while Sphinx still receives + the full `docs/` tree; +- copying, symlinking, `include`-ing, or `literalinclude`-ing developer records + from outside the public root; +- maintaining separate allowlists for Sphinx, search, sitemap, previews, + Pages, RTD, and Vale; +- treating absence from navigation as absence from generated HTML or search; +- overlooking `{download}`, `download`, include, or literal-include directives + that can copy or inline files from outside the public root even when the page + route itself is allowed; +- moving public source paths without updating policy checkers, source + references, formal-spec links, examples, tests, and `html_theme_options`; +- duplicating public pages at old repository paths to preserve GitHub links; +- adding redirects for internal pages that should stop publishing; +- checking generated HTML only for a growing blacklist instead of deriving + its allowed routes from the positive source root; +- using Vale as a semantic claim checker, SDL validator, link checker, or + substitute for editorial review; +- enforcing reader-first prose with broad keyword bans, sentence-length + absolutism, or a copied third-party style package; +- copying Stripe content, branding, information architecture, or visual + assets, or implying Stripe endorses RAES; +- creating another docs script, CI command list, failure DTO, waiver file, + logger, or test runner outside nox and `tools.policy.common`; +- duplicating SDL examples that are not extracted and executed by tests, or + documenting commands that only display `--help` but do not reach first + success; +- inventing a generic validation CLI or runtime feature solely to make the + quickstart read more smoothly; +- publishing raw ADR/preflight, audit, migration-working, research-log, + requirement, Ground Control, or current-state records merely because they + contain durable facts; curate the useful fact into a public explanation; +- claiming Scorecard or bestpractices.dev status before the external result is + live, gaming criteria, using N/A dishonestly, or adding governance the + single-maintainer project cannot satisfy; +- copying CNCF meetings, teams, elections, escalation committees, labels, + CLAs/DCO requirements, or service levels that the repository does not have; +- granting broad workflow permissions, using unpinned actions, introducing a + long-lived token, or weakening release-please to improve a score; +- editing `CHANGELOG.md`, the package version, normative specifications, + contracts, schemas, runtime behavior, or conformance semantics to make + documentation claims easier. + +## Non-Goals and Implementation Boundaries + +- No SDL, schema, contract, parser, processor, backend, runtime, API, MCP, + authentication, authorization, logging, persistence, or exception behavior + change. +- No deletion of developer, research, migration, audit, issue, preflight, or + Ground Control records from the repository. +- No new documentation content schema, CMS, database, service, controller, + repository layer, project-description registry, or generated prose system. +- No promise of production backends, managed environments, deterministic + outcomes, exact replay, complete domain coverage, staffing levels, response + SLAs, independent review, or multi-maintainer governance. +- No Silver/Gold best-practices claim, mandatory independent approval, PAT for + Scorecard/release automation, or replacement of release-please. +- No broad editorial rewrite of normative or historical records. Public prose + may explain their durable conclusions while retaining their authority and + evidence boundaries. +- No implementation of DOC-928 in this preflight note. diff --git a/docs/development/authorship-anomaly-2026-07.md b/docs/development/authorship-anomaly-2026-07.md new file mode 100644 index 000000000..6e3d385c9 --- /dev/null +++ b/docs/development/authorship-anomaly-2026-07.md @@ -0,0 +1,33 @@ +# Commit authorship anomaly from 2026-07-01 to 2026-07-05 + +This developer record explains a historical commit identity issue. It is not +part of the public security reporting policy. + +Commits are signed with the maintainer's SSH key under the identity +`Brad Edwards `. + +Thirty-six commits in this window were authored as the placeholder identity +`Test ` and appear as unverified on GitHub. They are not +unsigned or forged. Local signature checks report a good signature for the +maintainer's email. + +## Cause + +A repository-local user override set `user.email=t@example.com` and +`user.name=Test` during a pre-push recovery on 2026-07-01. A history replay +re-created commits under that identity. The override then shadowed the global +identity until it was found. + +## Scope + +The issue was limited to author and committer identity fields. No push +redirection, hooks path, SSH command, or credential helper changed. + +The affected range starts at `cf4cdaa` and ends at `a1fb96e`. Use +`git log --all --author='t@example.com' --format='%H %cI'` to list it. +GitHub-created merge commits in that window are separate. + +## Resolution + +The local override was removed on 2026-07-05. Shared history was not rewritten, +so the placeholder author remains in the historical record. diff --git a/docs/explain/reference/canonical-reference-map.md b/docs/explain/reference/canonical-reference-map.md index 5cf73f68e..3c3bba10a 100644 --- a/docs/explain/reference/canonical-reference-map.md +++ b/docs/explain/reference/canonical-reference-map.md @@ -24,7 +24,7 @@ page is an index, not a replacement for the linked artifacts. | Surface | Current reference | |---------|-------------------| | RAES and agentic-environment terminology | [`glossary.md`](glossary.md) | -| Current system boundary and entrypoints | [`docs/explain/getting-started.md`](../getting-started.md) | +| Current system boundary and entrypoints | [`docs/public/index.md`](../../public/index.md) | | Authored scenario language | [`docs/explain/sdl/index.md`](../sdl/index.md) | | Instantiation, planning, and realization path | [`docs/explain/sdl/runtime-architecture.md`](../sdl/runtime-architecture.md) | | Explicitness and realized-form semantics | [`explicitness-realization-semantics.md`](explicitness-realization-semantics.md), [`realization-envelopes.md`](realization-envelopes.md) | @@ -48,7 +48,7 @@ page is an index, not a replacement for the linked artifacts. | Surface | Current reference | |---------|-------------------| -| Getting started | [`docs/explain/getting-started.md`](../getting-started.md) | +| Getting started | [`docs/public/quickstart.md`](../../public/quickstart.md) | | Worked examples | `examples/README.md`, `examples/scenarios/*.sdl.yaml` | | Template and pattern library | `examples/library/catalog.yaml`, `examples/library/templates/`, `examples/library/patterns/` | | SDL guide | [`docs/explain/sdl/index.md`](../sdl/index.md) | @@ -69,9 +69,9 @@ page is an index, not a replacement for the linked artifacts. | Contract root | `contracts/README.md` | | Published schemas | `contracts/schemas/README.md` | | Schema inventory | [`contracts/schema-publication-manifest.json`](../../../contracts/schema-publication-manifest.json) | -| Processor API | [`docs/api/processor.rst`](../../api/processor.rst) | -| Processor semantics API | [`docs/api/processor-semantics.rst`](../../api/processor-semantics.rst) | -| Runtime API | [`docs/api/runtime.rst`](../../api/runtime.rst) | +| Processor API | [`docs/public/api/processor.rst`](../../public/api/processor.rst) | +| Processor semantics API | [`docs/public/api/processor-semantics.rst`](../../public/api/processor-semantics.rst) | +| Runtime API | [`docs/public/api/runtime.rst`](../../public/api/runtime.rst) | | Runtime architecture | [`docs/explain/sdl/runtime-architecture.md`](../sdl/runtime-architecture.md) | | Backend conformance | [`backend-conformance.md`](backend-conformance.md) | diff --git a/docs/explain/reference/documentation-style-guide.md b/docs/explain/reference/documentation-style-guide.md index e892d0161..d649e3aee 100644 --- a/docs/explain/reference/documentation-style-guide.md +++ b/docs/explain/reference/documentation-style-guide.md @@ -1,12 +1,104 @@ # Documentation Style Guide This guide applies to prose documentation in this repository: root Markdown -files, `docs/`, `specs/`, `contracts/`, `implementations/`, and release-note -fragments. +files, `docs/`, `specs/`, `contracts/`, and `implementations/`. The audience is technical and academic. Documentation describes the current repository state. It is not a product page, roadmap, or funding document. +## Stripe Documentation Is The Editorial Exemplar + +[Stripe Documentation](https://docs.stripe.com/) is the explicit editorial +exemplar for RAES public documentation. Stripe's docs are approachable because +they move a reader from a concrete task to a visible result with little +friction. RAES adopts that reader experience while retaining its own technical +voice, evidence boundaries, semantics, and visual identity. + +Study these official examples: + +- [Development environment](https://docs.stripe.com/get-started/development-environment?lang=python) + leads with a task, states what the reader will achieve, and puts exact + commands next to the step they complete. +- [Quickstarts](https://docs.stripe.com/quickstarts) helps readers choose a + route before presenting detail. +- [Checkout quickstart](https://docs.stripe.com/checkout/quickstart) moves in + small numbered steps from setup to a working result. +- [Create a customer](https://docs.stripe.com/api/customers/create) keeps the + resource purpose, parameters, request example, and returned result close + together. +- [Testing](https://docs.stripe.com/testing) places test values and cautions + where readers use them instead of opening with a caveat wall. + +These pages are examples of information design. Do not copy Stripe wording, +brand assets, theme code, product concepts, or API conventions. Do not imply +that Stripe has reviewed or endorsed RAES. + +### Translate The Pattern Into RAES + +| Stripe pattern | RAES rule | +| --- | --- | +| Task-oriented title | Name the action and its result: "Validate your first scenario." | +| Short outcome-led opening | Tell the reader what they will complete before explaining the system. | +| Early route choice | Offer separate routes for authors, Python users, CLI users, backend implementers, and researchers. | +| Working request or command | Use a current SDL file, Python call, or CLI command that a test executes. | +| Placeholder near first use | Explain each path, name, or value beside the step that introduces it. | +| Visible response or result | Show the output, created file, exit meaning, or next screen immediately after the action. | +| Contextual caution | Put limits beside the claim or command they constrain. | +| Progressive disclosure | Finish first success before linking to concepts, specifications, and full API reference. | + +Public entry pages follow this order: + +1. State the reader's task and outcome. +2. List only the prerequisites needed for that task. +3. Give exact steps with current commands or code. +4. Explain placeholders where they first appear. +5. Show the expected result. +6. State the boundary of that result. +7. Link to the next task and deeper reference material. + +### Before And After + +Avoid an abstract opening: + +> The scenario validation capability facilitates the establishment of a +> structurally and semantically conformant authored artifact. + +Lead with the task and result: + +> Validate an SDL file from Python. If the file is valid, RAES returns a +> `Scenario` that your code can inspect. + +Avoid separating a command from its meaning: + +> Run the formatter. See the reference section for flags and exit behavior. + +Keep the outcome with the command: + +> Run `raes sdl format --check scenario.sdl.yaml`. Exit code `0` means the +> file parses and already uses the canonical format. The command does not +> provision infrastructure. + +### Vale Enforcement + +The repository-owned style under `styles/RAES/` enforces the objective part of +this guide on the root README, hosted public docs, and public community +entrypoints. It checks: + +- plain words instead of formal substitutes such as "utilize" or "in order + to"; +- dismissive words such as "obviously", "trivial", "simply", and "just"; +- promotional terms that replace evidence with praise; +- sentence-case headings without terminal punctuation; +- repeated words, sentence length, and a document-level Flesch-Kincaid target. + +Vale does not decide whether a technical claim is true. The repository's +positioning, contract, schema, example, and link checks keep those +responsibilities. Run the complete documentation gate with: + +```shell +uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s docs +``` + ## Required Stance - Be accurate before being persuasive. diff --git a/docs/index.md b/docs/index.md index 6bf9cd803..d2db2df4e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,196 +1,11 @@ -# Reproducible Agentic Environments System +# Choose the documentation index -Reproducible Agentic Environments System (RAES) describes, realizes, controls, -evaluates, and supports bounded reproduction of agentic environments. An -agentic environment is a declared and realized setting in which participants -receive observations, take actions, interact with resources or other -participants, and are evaluated under stated controls. +This path remains as a repository link for older records. It is not the hosted +Sphinx source root. -Cyber, AI security, AI safety, testing, research, and evaluation are -non-exhaustive application areas. Additional domains can use the same -authored-intent, realization, participant, observation, evidence, provenance, -and conformance boundaries through their own profiles, assets, examples, and -backends. +- Read the [public documentation](public/index.md) to install and use RAES. +- Use the [developer documentation index](README.md) for architecture, + research, migration, audit, release, and workflow records. -The current `raes` distribution provides RAES SDL, a Python reference -implementation, published contracts, examples, and assurance material. RAES -names the overall system; RAES SDL is the authored scenario language. The -reference implementation can validate authored meaning, instantiate and -compile runtime models, plan against backend manifests, and check published -backend contracts. - -The repository is not a managed environment service and does not ship -production backend implementations. It is a working codebase for evaluating -language, semantic, runtime, and assurance claims against source code, schemas, -examples, and tests. Its reproducibility surfaces support a bounded -reproduction attempt; they do not guarantee deterministic runtime behavior, -equal outcomes, exact replay, or reproducibility. - -## Quick Start - -```python -from raes import parse_sdl, parse_sdl_file - -# Parse from a string -scenario = parse_sdl(yaml_string) - -# Parse from a file -scenario = parse_sdl_file(Path("scenarios/my-scenario.yaml")) - -# Skip semantic validation (structural only) -scenario = parse_sdl(yaml_string, skip_semantic_validation=True) - -# Non-fatal authoring advisories -for advisory in scenario.advisories: - print(advisory) -``` - -## What's Included - -- **Author-facing SDL** models and parsing for 21 scenario sections -- **Semantic validation** and formal semantic artifacts -- **Processor layer** with compiler, planner, and control-plane contracts -- **Schemas** and backend conformance fixtures -- **CLI commands**, docs, examples, reusable authoring templates, patterns, and tests - -## Reader Map - -- New users can start with the getting-started guide to choose the smallest - current entrypoint for their task and rigor level. -- Agentic-environment users can start with the glossary and reference map to - distinguish authored scenarios, realized environments, apparatus, evidence, - and conformance. -- Scenario authors usually start with the SDL guide, sections reference, - parser behavior, validation rules, and limitations. -- Backend implementers usually start with runtime architecture, contract - schemas, backend conformance, and the canonical reference map. -- Researchers usually start with lineage, design precedents, formal - specifications, glossary, and limitations. -- Contributors should read the documentation style guide before changing prose. - -```{toctree} -:maxdepth: 2 -:caption: Getting Started - -explain/getting-started -``` - -```{toctree} -:maxdepth: 2 -:caption: Maintainer Guide - -explain/releasing -``` - -```{toctree} -:maxdepth: 2 -:caption: SDL Guide - -explain/sdl/index -explain/sdl/sections -explain/sdl/parser -explain/sdl/variation-points -explain/sdl/language-service -explain/sdl/agent-guidance -explain/sdl/validation -explain/sdl/precedents -explain/sdl/lineage -explain/sdl/related-work-comparison -explain/sdl/scientific-scenario-completeness -explain/sdl/scenario-delivery-drift-audit -explain/sdl/complex-scenarios -explain/sdl/limitations -explain/sdl/testing -``` - -```{toctree} -:maxdepth: 2 -:caption: Runtime - -explain/sdl/runtime-architecture -``` - -```{toctree} -:maxdepth: 2 -:caption: Asset Inventory - -raes/inventory/index -``` - -```{toctree} -:maxdepth: 2 -:caption: Architecture Decisions - -decisions/index -``` - -```{toctree} -:maxdepth: 2 -:caption: Reference - -explain/reference/README -explain/reference/coding-standards -explain/reference/canonical-reference-map -explain/reference/documentation-style-guide -explain/reference/glossary -explain/reference/shared-concept-model -explain/reference/shared-semantic-integrity -explain/reference/backend-conformance -explain/reference/reference-emulation-backend -explain/reference/normative-artifact-authority -explain/reference/assessment-semantics -explain/reference/objective-semantics -explain/reference/explicitness-realization-semantics -explain/reference/realization-envelopes -explain/reference/scenario-variation-and-trial-realization -explain/reference/experiment-binding-contracts -explain/reference/participant-decision-surface-v2-migration -``` - -```{toctree} -:maxdepth: 2 -:caption: Formal Specifications - -specs/formal -``` - -```{toctree} -:maxdepth: 2 -:caption: Project Notes - -lessons/README -migration/README -migration/autonomous-execution-v2 -migration/participant-execution-control -migration/raes-rename -research/experiment-core/index -research/realization-envelope/index -research/scenario-variation-trial-realization/index -research/scoring-scope/index -research/validation-admission-profiles/index -research/primary/index -research/lineage/source-audit-2026-07-12 -research/behavioral-relations/conflation-audit-2026-07-13 -research/related-work-comparison/index -research/dsl-language-evaluation/index -research/specification-coverage/index -research/formal-semantic-validation/index -research/participant-backend-contracts/index -research/participant-io-control/index -research/participant-interactive-access/index -research/time-model/prior-art-and-design-criteria -research/initial-service-state-precedents-2026-07-24 -``` - -```{toctree} -:maxdepth: 2 -:caption: API Reference - -api/sdl -api/sdl-semantics -api/processor -api/processor-semantics -api/contracts -api/runtime -api/cli -``` +Only files under `docs/public/` enter the hosted build, search index, sitemap, +and pull-request preview. diff --git a/docs/public/_static/examples/first-scenario.sdl.yaml b/docs/public/_static/examples/first-scenario.sdl.yaml new file mode 100644 index 000000000..d7f0422ee --- /dev/null +++ b/docs/public/_static/examples/first-scenario.sdl.yaml @@ -0,0 +1,23 @@ +name: first-scenario +description: A small network with one Linux host. + +nodes: + lab-network: + type: Switch + web: + type: VM + os: linux + resources: + ram: 2 GiB + cpu: 1 + +infrastructure: + lab-network: + count: 1 + properties: + cidr: 10.0.0.0/24 + gateway: 10.0.0.1 + web: + count: 1 + links: + - lab-network diff --git a/docs/api/cli.rst b/docs/public/api/cli.rst similarity index 100% rename from docs/api/cli.rst rename to docs/public/api/cli.rst diff --git a/docs/api/contracts.rst b/docs/public/api/contracts.rst similarity index 100% rename from docs/api/contracts.rst rename to docs/public/api/contracts.rst diff --git a/docs/public/api/index.rst b/docs/public/api/index.rst new file mode 100644 index 000000000..70b346f4e --- /dev/null +++ b/docs/public/api/index.rst @@ -0,0 +1,15 @@ +Use the Python API +================== + +Choose the narrowest API for your task. Most authors start with the SDL parser. + +.. toctree:: + :maxdepth: 2 + + sdl + sdl-semantics + processor + processor-semantics + contracts + runtime + cli diff --git a/docs/api/processor-semantics.rst b/docs/public/api/processor-semantics.rst similarity index 100% rename from docs/api/processor-semantics.rst rename to docs/public/api/processor-semantics.rst diff --git a/docs/api/processor.rst b/docs/public/api/processor.rst similarity index 100% rename from docs/api/processor.rst rename to docs/public/api/processor.rst diff --git a/docs/api/runtime.rst b/docs/public/api/runtime.rst similarity index 100% rename from docs/api/runtime.rst rename to docs/public/api/runtime.rst diff --git a/docs/api/sdl-semantics.rst b/docs/public/api/sdl-semantics.rst similarity index 100% rename from docs/api/sdl-semantics.rst rename to docs/public/api/sdl-semantics.rst diff --git a/docs/api/sdl.rst b/docs/public/api/sdl.rst similarity index 100% rename from docs/api/sdl.rst rename to docs/public/api/sdl.rst diff --git a/docs/public/backends.md b/docs/public/backends.md new file mode 100644 index 000000000..632bfd78f --- /dev/null +++ b/docs/public/backends.md @@ -0,0 +1,20 @@ +# Check a backend boundary + +Use backend manifests and conformance fixtures to state what an implementation +accepts and what it can realize. + +RAES separates three questions: + +1. Is the SDL document valid? +2. Can a processor compile the requested meaning? +3. Can a selected backend realize the compiled request? + +A valid scenario can still exceed a backend's declared capabilities. Read the +backend report and keep unsupported or degraded results with the run evidence. + +The repository includes contracts, stubs, a reference emulation backend, and +conformance tests. It does not ship a production deployment backend or managed +environment service. + +Start with the [backend schemas](https://github.com/RAESystem/rae/tree/main/contracts/schemas/backend-manifest) +and the [conformance API](api/contracts.rst). diff --git a/docs/public/citation.md b/docs/public/citation.md new file mode 100644 index 000000000..65b8ea08e --- /dev/null +++ b/docs/public/citation.md @@ -0,0 +1,17 @@ +# Cite RAES + +Use this software citation when RAES contributes to your work: + +```bibtex +@software{raes, + author = {Edwards, Brad}, + title = {RAES: Reproducible Agentic Environments System}, + year = {2026}, + license = {MIT}, + url = {https://github.com/RAESystem/rae} +} +``` + +Also record the release or commit that you used. Name the contracts, profiles, +scenario, backend, and run evidence. These details show the limits of your +result better than the project name alone. diff --git a/docs/public/concepts.md b/docs/public/concepts.md new file mode 100644 index 000000000..3b8b4d117 --- /dev/null +++ b/docs/public/concepts.md @@ -0,0 +1,26 @@ +# Understand the RAES model + +Use these ideas to decide what a RAES result means. + +## Authored scenario + +A RAES SDL file records intent. It can name nodes, links, people, software, +goals, tasks, and evidence needs. The file is not a running environment. + +## Realized environment + +A processor and backend turn supported parts of the scenario into runtime +resources. Backend reports show what worked, what changed, and what was not +supported. + +## Bounded reproduction + +RAES can save inputs, choices, names, observations, and evidence for another +attempt. It does not promise equal outcomes, a fixed runtime, exact replay, or +scientific reproducibility. + +## Conformance + +Contracts and test fixtures check whether an implementation honors a stated +RAES boundary. Passing one profile does not mean that every SDL feature or +deployment target works. diff --git a/docs/conf.py b/docs/public/conf.py similarity index 88% rename from docs/conf.py rename to docs/public/conf.py index d5470fda7..e53367f6c 100644 --- a/docs/conf.py +++ b/docs/public/conf.py @@ -1,5 +1,7 @@ +import json from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _distribution_version +from pathlib import Path # -- Project information ------------------------------------------------------- @@ -22,14 +24,15 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", - "sphinx.ext.intersphinx", "sphinx.ext.autosummary", "myst_parser", "sphinx_copybutton", + "sphinx_reredirects", ] templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +redirects = json.loads((Path(__file__).parent / "redirects.json").read_text(encoding="utf-8")) # -- MyST (Markdown) settings -------------------------------------------------- @@ -50,7 +53,7 @@ html_theme_options = { "source_repository": "https://github.com/RAESystem/rae", "source_branch": "main", - "source_directory": "docs/", + "source_directory": "docs/public/", "navigation_with_keys": True, } @@ -71,13 +74,6 @@ napoleon_numpy_docstring = True napoleon_include_init_with_doc = True -# -- intersphinx --------------------------------------------------------------- - -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "pydantic": ("https://docs.pydantic.dev/latest/", None), -} - # -- autosummary --------------------------------------------------------------- autosummary_generate = True diff --git a/docs/public/contributing.md b/docs/public/contributing.md new file mode 100644 index 000000000..c959dab60 --- /dev/null +++ b/docs/public/contributing.md @@ -0,0 +1,17 @@ +# Improve RAES + +Choose the route that matches your change: + +- Report a reproducible defect with the + [bug template](https://github.com/RAESystem/rae/issues/new?template=bug_report.md). +- Propose a focused improvement with the + [feature template](https://github.com/RAESystem/rae/issues/new?template=feature_request.md). +- Follow [CONTRIBUTING.md](https://github.com/RAESystem/rae/blob/main/CONTRIBUTING.md) + for setup, tests, commits, and pull requests. +- Read the repository + [documentation style guide](https://github.com/RAESystem/rae/blob/main/docs/explain/reference/documentation-style-guide.md) + before changing public prose. + +Discuss SDL, contract, or authority changes before implementation. A small +field change can affect schemas, examples, processors, backends, and +conformance fixtures. diff --git a/docs/public/guides/cli.md b/docs/public/guides/cli.md new file mode 100644 index 000000000..f7f4321e1 --- /dev/null +++ b/docs/public/guides/cli.md @@ -0,0 +1,26 @@ +# Work with SDL from the command line + +Use the CLI to format, resolve imports, verify imports, and publish an authored +scenario. + +From a repository checkout: + +```console +uv run --project implementations/python raes sdl --help +``` + +Common commands include: + +```console +uv run --project implementations/python raes sdl format --check scenario.sdl.yaml +uv run --project implementations/python raes sdl verify-imports scenario.sdl.yaml +uv run --project implementations/python raes sdl resolve scenario.sdl.yaml +``` + +`format --check` parses the document and checks its canonical formatting. +`verify-imports` checks referenced modules. `resolve` prints the composed +scenario. None of these commands provisions infrastructure. + +Use `raes processor --help` and `raes conformance --help` for the processor and +backend-contract surfaces. The [CLI API reference](../api/cli.rst) lists the +current command implementation. diff --git a/docs/public/guides/python.md b/docs/public/guides/python.md new file mode 100644 index 000000000..d43b2d490 --- /dev/null +++ b/docs/public/guides/python.md @@ -0,0 +1,27 @@ +# Validate SDL from Python + +Use `parse_sdl_file` when your app stores a scenario as a file: + +```python +from pathlib import Path + +from raes import parse_sdl_file + +scenario = parse_sdl_file(Path("first-scenario.sdl.yaml")) +print(scenario.name) +``` + +Use `parse_sdl` when you already have SDL text: + +```python +from raes import parse_sdl + +scenario = parse_sdl(sdl_text) +``` + +Both functions check the file shape and its meaning by default. Treat parse +errors as authoring failures. Show the error message to the author. Keep +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. diff --git a/docs/public/index.md b/docs/public/index.md new file mode 100644 index 000000000..f5fdb527a --- /dev/null +++ b/docs/public/index.md @@ -0,0 +1,42 @@ +# Describe a reproducible agentic environment + +RAES, the Reproducible Agentic Environments System, helps you describe an +agentic environment, check the authored scenario, and keep evidence about what +was realized. RAES SDL is the YAML language used for authored scenarios. + +Start with a working scenario in the [quickstart](quickstart.md). It takes +about five minutes and uses the Python package. + +## Choose your route + +- **New to RAES?** Learn the [core concepts](concepts.md), then complete the + [first-scenario tutorial](tutorials/first-scenario.md). +- **Writing a scenario?** Use the [SDL guide](sdl/index.md) and + [examples](https://github.com/RAESystem/rae/tree/main/examples/scenarios). +- **Integrating RAES?** Choose the [Python API](guides/python.md) or + [command-line interface](guides/cli.md). +- **Building a backend?** Read the [backend and conformance guide](backends.md). +- **Evaluating the research?** Start with the [research context](research.md), + [current limits](limitations.md), and [citation](citation.md). +- **Improving the project?** See [contributing](contributing.md), + [support](support.md), and the + [API reference](api/index.rst). + +```{toctree} +:hidden: +:maxdepth: 2 + +quickstart +concepts +tutorials/first-scenario +sdl/index +guides/python +guides/cli +backends +research +limitations +contributing +support +citation +api/index +``` diff --git a/docs/public/limitations.md b/docs/public/limitations.md new file mode 100644 index 000000000..51f83af1b --- /dev/null +++ b/docs/public/limitations.md @@ -0,0 +1,18 @@ +# Check current limits + +Read these limits before you choose RAES for a study or an app. + +- RAES is an academic and engineering project, not a managed service. +- The repository does not include a production deployment backend. +- A valid SDL file may request features that a chosen backend does not + support. +- Validation cannot guarantee that a runtime behaves deterministically. +- Saved inputs and evidence support another attempt; they do not guarantee + equal outcomes, exact replay, scientific validity, or reproducibility. +- The project currently has one maintainer. It does not require a second + maintainer or independent reviewer for every change. +- Public schemas have their own stability labels. A versioned name does + not by itself mean that a schema is stable. + +Use backend reports, test results, provenance, and evidence to state the limits +of a result. diff --git a/docs/public/quickstart.md b/docs/public/quickstart.md new file mode 100644 index 000000000..717505d0d --- /dev/null +++ b/docs/public/quickstart.md @@ -0,0 +1,54 @@ +# Validate your first scenario + +Install RAES, validate a small SDL file, and print its name. You need Python +3.11 or newer. + +## Install RAES + +Create a virtual environment and install the published package: + +```console +python -m venv .venv +source .venv/bin/activate +python -m pip install raes +``` + +On Windows PowerShell, activate the environment with +`.venv\Scripts\Activate.ps1`. + +## Save the scenario + +Copy this file to `first-scenario.sdl.yaml`: + +```{literalinclude} _static/examples/first-scenario.sdl.yaml +:language: yaml +``` + +The `nodes` section declares the network and host. The `infrastructure` +section asks a backend for one instance of each and links the host to the +network. + +## Validate the file + +Run: + +```console +python - <<'PY' +from pathlib import Path +from raes import parse_sdl_file + +scenario = parse_sdl_file(Path("first-scenario.sdl.yaml")) +print(f"Validated {scenario.name} with {len(scenario.nodes)} nodes.") +PY +``` + +If validation succeeds, the command prints: + +```text +Validated first-scenario with 2 nodes. +``` + +RAES has checked the file's structure and current semantic rules. It has not +created infrastructure. Continue with the +[first-scenario tutorial](tutorials/first-scenario.md) to inspect and format +the scenario. diff --git a/docs/public/redirects.json b/docs/public/redirects.json new file mode 100644 index 000000000..89ff89bcb --- /dev/null +++ b/docs/public/redirects.json @@ -0,0 +1,14 @@ +{ + "explain/getting-started": "../quickstart.html", + "explain/reference/backend-conformance": "../../backends.html", + "explain/reference/canonical-reference-map": "../../concepts.html", + "explain/reference/documentation-style-guide": "../../contributing.html", + "explain/reference/glossary": "../../concepts.html", + "explain/sdl/index": "../../sdl/index.html", + "explain/sdl/limitations": "../../limitations.html", + "explain/sdl/lineage": "../../research.html", + "explain/sdl/related-work-comparison": "../../research.html", + "explain/sdl/runtime-architecture": "../../backends.html", + "explain/sdl/scientific-scenario-completeness": "../../research.html", + "specs/formal": "../research.html" +} diff --git a/docs/public/research.md b/docs/public/research.md new file mode 100644 index 000000000..d8cdf391c --- /dev/null +++ b/docs/public/research.md @@ -0,0 +1,21 @@ +# Evaluate the research surface + +Use RAES files to inspect a bounded claim, repeat a check, or compare code with +a published contract. + +You can inspect: + +- specifications and published schemas; +- source code and tests; +- example scenarios and conformance fixtures; +- decisions that explain key design choices; +- research records with searches, comparisons, and known gaps. + +Working records remain in the repository for peer review. They are not part of +this hosted guide. Start with the +[formal specifications](https://github.com/RAESystem/rae/tree/main/specs), +[contracts](https://github.com/RAESystem/rae/tree/main/contracts), and +[research directory](https://github.com/RAESystem/rae/tree/main/docs/research). + +RAES can support a bounded reproduction attempt. One successful parse, +realization, or replay does not prove scientific reproducibility. diff --git a/docs/public/sdl/index.md b/docs/public/sdl/index.md new file mode 100644 index 000000000..0ba180ae8 --- /dev/null +++ b/docs/public/sdl/index.md @@ -0,0 +1,25 @@ +# Author RAES SDL + +Use RAES SDL to describe scenario intent without tying the document to one +deployment backend. + +Start with four parts: + +1. Give the scenario a stable `name`. +2. Declare resources under `nodes`. +3. Set requested counts and links under `infrastructure`. +4. Parse the file and address validation errors before adding more sections. + +The [quickstart](../quickstart.md) shows all four parts. Larger scenarios can +add services, identities, participants, behaviors, objectives, workflows, +variation, and evidence requirements. + +The published schemas and normative specifications remain the authority for +accepted fields and meaning: + +- [SDL schema](https://github.com/RAESystem/rae/tree/main/contracts/schemas/sdl) +- [Normative SDL specification](https://github.com/RAESystem/rae/tree/main/specs) +- [Worked examples](https://github.com/RAESystem/rae/tree/main/examples/scenarios) + +Read [current limits](../limitations.md) before assuming that a backend can +realize every valid authored section. diff --git a/docs/public/support.md b/docs/public/support.md new file mode 100644 index 000000000..9d0139d99 --- /dev/null +++ b/docs/public/support.md @@ -0,0 +1,11 @@ +# Get help + +Use a [GitHub issue](https://github.com/RAESystem/rae/issues/new/choose) for a +reproducible defect, docs problem, or focused feature request. + +Do not post a suspected vulnerability in a public issue. Follow the private +reporting route in +[SECURITY.md](https://github.com/RAESystem/rae/blob/main/SECURITY.md). + +RAES is maintained on a best-effort basis. The project does not promise a +response time or support service level. diff --git a/docs/public/tutorials/first-scenario.md b/docs/public/tutorials/first-scenario.md new file mode 100644 index 000000000..8f8842666 --- /dev/null +++ b/docs/public/tutorials/first-scenario.md @@ -0,0 +1,34 @@ +# Inspect and format a scenario + +Build on the quickstart file and use the CLI to inspect its current structure. + +## Prepare the repository environment + +Clone the repository when you want the CLI and complete example library: + +```console +git clone https://github.com/RAESystem/rae.git +cd rae +uv sync --project implementations/python --all-extras --frozen +``` + +`uv` creates the environment from the checked-in lock file. + +## Check the file format + +Run the formatter in check mode: + +```console +uv run --project implementations/python raes sdl format \ + --check docs/public/_static/examples/first-scenario.sdl.yaml +``` + +Exit code `0` means the file parses and already uses the canonical format. +The command does not provision the scenario. + +## Explore a larger scenario + +The [scenario collection](https://github.com/RAESystem/rae/tree/main/examples/scenarios) +contains authored examples with participants, behaviors, objectives, and +evidence requirements. Check each example's notes before treating it as a +backend-ready deployment. diff --git a/examples/library/catalog.yaml b/examples/library/catalog.yaml index f82ca5019..a5c9d250b 100644 --- a/examples/library/catalog.yaml +++ b/examples/library/catalog.yaml @@ -3,7 +3,7 @@ version: 1 requirement_refs: [AUT-806] source_refs: - examples/README.md - - docs/explain/getting-started.md + - docs/public/quickstart.md - docs/explain/reference/canonical-reference-map.md description: > Domain-neutral RAES agentic-environment authoring library for non-normative, diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index 9b5b9bf03..cdb4c4e91 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -47,6 +47,7 @@ docs = [ "myst-parser>=3.0.0", "sphinx-copybutton>=0.5.2", "sphinx-autobuild>=2024.4.16", + "sphinx-reredirects==1.1.0", ] [project.scripts] diff --git a/implementations/python/tests/test_project_positioning.py b/implementations/python/tests/test_project_positioning.py index 786805c4c..6a151d8ab 100644 --- a/implementations/python/tests/test_project_positioning.py +++ b/implementations/python/tests/test_project_positioning.py @@ -32,22 +32,15 @@ def _seed_positioning_surfaces(repo_root: Path) -> None: """ for relative_path in ( "README.md", - "docs/index.md", - "docs/explain/getting-started.md", - "docs/explain/reference/canonical-reference-map.md", - "docs/explain/sdl/index.md", - "docs/explain/sdl/runtime-architecture.md", + "docs/public/index.md", + "docs/public/quickstart.md", + "docs/public/concepts.md", + "docs/public/sdl/index.md", + "docs/public/backends.md", + "docs/public/research.md", "examples/README.md", ): _write(repo_root / relative_path, primary_copy) - - _write( - repo_root / "docs/explain/reference/glossary.md", - primary_copy - + "\n**RAES**\n: The overall system.\n" - + "\n**Agentic environment**\n: A declared and realized setting.\n" - + "\n**Reproducibility support**\n: Support for a bounded reproduction attempt.\n", - ) _write( repo_root / "examples/library/catalog.yaml", "description: >\n" @@ -59,7 +52,7 @@ def _seed_positioning_surfaces(repo_root: Path) -> None: '[project]\ndescription = "Contracts and reference tooling for reproducible agentic environments."\n', ) _write( - repo_root / "docs/conf.py", + repo_root / "docs/public/conf.py", 'project = "Reproducible Agentic Environments System"\nhtml_title = "RAES Documentation"\n', ) _write( @@ -98,14 +91,14 @@ def test_positioning_check_reports_missing_domain_neutral_metadata(tmp_path: Pat def test_positioning_check_reports_missing_claim_boundary(tmp_path: Path) -> None: _seed_positioning_surfaces(tmp_path) _write( - tmp_path / "docs/explain/getting-started.md", + tmp_path / "docs/public/concepts.md", "Reproducible Agentic Environments System guarantees exact replay.\n", ) failures = validate_project_positioning(tmp_path) assert any( - failure.rule_id == "project-positioning-claim-boundary" and failure.path == "docs/explain/getting-started.md" + failure.rule_id == "project-positioning-claim-boundary" and failure.path == "docs/public/concepts.md" for failure in failures ) @@ -114,7 +107,7 @@ def test_positioning_check_reports_missing_claim_boundary(tmp_path: Path) -> Non ("relative_path", "replacement"), ( ("README.md", None), - ("docs/index.md", "x" * (MAX_SURFACE_BYTES + 1)), + ("docs/public/index.md", "x" * (MAX_SURFACE_BYTES + 1)), ), ) def test_positioning_check_reports_invalid_required_surface( @@ -140,7 +133,7 @@ def test_positioning_check_reports_invalid_required_surface( "relative_path", ( "README.md", - "docs/explain/reference/canonical-reference-map.md", + "docs/public/backends.md", ), ) def test_positioning_check_reports_missing_entrypoint_framing( @@ -157,21 +150,6 @@ def test_positioning_check_reports_missing_entrypoint_framing( ) -def test_positioning_check_reports_incomplete_glossary_framing(tmp_path: Path) -> None: - _seed_positioning_surfaces(tmp_path) - _write( - tmp_path / "docs/explain/reference/glossary.md", - "Agentic environments are declared and realized settings.\n", - ) - - failures = validate_project_positioning(tmp_path) - - assert any( - failure.rule_id == "project-positioning-framing" and failure.path == "docs/explain/reference/glossary.md" - for failure in failures - ) - - def test_positioning_check_reports_incomplete_application_areas(tmp_path: Path) -> None: _seed_positioning_surfaces(tmp_path) readme = (tmp_path / "README.md").read_text(encoding="utf-8") @@ -186,7 +164,7 @@ def test_positioning_check_reports_incomplete_application_areas(tmp_path: Path) ("relative_path", "replacement"), ( ( - "docs/conf.py", + "docs/public/conf.py", 'project = "RAES"\nhtml_title = "Documentation"\n', ), ( diff --git a/implementations/python/tests/test_public_docs_policy.py b/implementations/python/tests/test_public_docs_policy.py new file mode 100644 index 000000000..14bee2275 --- /dev/null +++ b/implementations/python/tests/test_public_docs_policy.py @@ -0,0 +1,171 @@ +"""Behavioral tests for the curated public documentation boundary.""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[3] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from raes import parse_sdl_file # noqa: E402 +from tools.check_public_docs import ( # noqa: E402 + REQUIRED_PUBLIC_PAGES, + REQUIRED_PUBLIC_REDIRECTS, + evaluate_public_output, + evaluate_public_sources, +) + + +def _write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _seed_public_docs(repo_root: Path) -> Path: + public_root = repo_root / "docs" / "public" + for relative_path in REQUIRED_PUBLIC_PAGES: + _write(public_root / relative_path, f"# {Path(relative_path).stem.title()}\n") + _write(public_root / "redirects.json", json.dumps(REQUIRED_PUBLIC_REDIRECTS)) + _write(public_root / "conf.py", 'project = "RAES"\n') + return public_root + + +def test_public_source_root_accepts_only_contained_regular_sources(tmp_path: Path) -> None: + _seed_public_docs(tmp_path) + + assert evaluate_public_sources(tmp_path) == [] + + +def test_public_source_root_rejects_symlinks(tmp_path: Path) -> None: + public_root = _seed_public_docs(tmp_path) + internal = tmp_path / "docs" / "decisions" / "private.md" + _write(internal, "# Internal decision\n") + os.symlink(internal, public_root / "linked-decision.md") + + failures = evaluate_public_sources(tmp_path) + + assert any(failure.rule_id == "public-docs-symlink" for failure in failures) + assert all("Internal decision" not in failure.message for failure in failures) + + +def test_public_source_root_rejects_directives_that_escape_it(tmp_path: Path) -> None: + public_root = _seed_public_docs(tmp_path) + _write( + public_root / "quickstart.md", + "# Quickstart\n\n```{literalinclude} ../decisions/private.md\n```\n", + ) + + failures = evaluate_public_sources(tmp_path) + + assert [(failure.rule_id, failure.path) for failure in failures] == [ + ("public-docs-source-escape", "docs/public/quickstart.md") + ] + + +def test_public_source_root_requires_the_legacy_redirect_contract(tmp_path: Path) -> None: + public_root = _seed_public_docs(tmp_path) + redirects = dict(REQUIRED_PUBLIC_REDIRECTS) + redirects.pop("explain/getting-started") + _write(public_root / "redirects.json", json.dumps(redirects)) + + failures = evaluate_public_sources(tmp_path) + + assert [(failure.rule_id, failure.path) for failure in failures] == [ + ("public-docs-redirect-map", "docs/public/redirects.json") + ] + + +def _seed_public_output(public_root: Path, output_root: Path) -> None: + for source in public_root.rglob("*.md"): + route = source.relative_to(public_root).with_suffix(".html") + _write(output_root / route, "\n") + for source, target in REQUIRED_PUBLIC_REDIRECTS.items(): + _write( + output_root / f"{source}.html", + f'\n', + ) + _write(output_root / "genindex.html", "\n") + _write(output_root / "search.html", "\n") + search_index = { + "docnames": sorted( + source.relative_to(public_root).with_suffix("").as_posix() for source in public_root.rglob("*.md") + ) + } + _write(output_root / "searchindex.js", f"Search.setIndex({json.dumps(search_index)})") + + +def test_public_output_rejects_unexpected_pages_and_search_documents(tmp_path: Path) -> None: + public_root = _seed_public_docs(tmp_path) + output_root = tmp_path / "docs" / "_build" / "html" + _seed_public_output(public_root, output_root) + _write(output_root / "decisions" / "private.html", "\n") + search_index = { + "docnames": [ + *sorted(source.relative_to(public_root).with_suffix("").as_posix() for source in public_root.rglob("*.md")), + "decisions/private", + ] + } + _write(output_root / "searchindex.js", f"Search.setIndex({json.dumps(search_index)})") + _write( + output_root / "sitemap.xml", + '\n' + '\n' + " https://docs.example.test/en/latest/decisions/index.html\n" + "\n", + ) + + failures = evaluate_public_output(tmp_path, output_root) + + assert {failure.rule_id for failure in failures} == { + "public-docs-output-route", + "public-docs-search-route", + "public-docs-sitemap-route", + } + + +def test_public_output_rejects_missing_or_tampered_redirects(tmp_path: Path) -> None: + public_root = _seed_public_docs(tmp_path) + output_root = tmp_path / "docs" / "_build" / "html" + _seed_public_output(public_root, output_root) + source, _target = next(iter(REQUIRED_PUBLIC_REDIRECTS.items())) + _write( + output_root / f"{source}.html", + '\n', + ) + + failures = evaluate_public_output(tmp_path, output_root) + + assert [(failure.rule_id, failure.path) for failure in failures] == [ + ("public-docs-redirect-output", f"{source}.html") + ] + + +def test_checked_in_quickstart_scenario_parses() -> None: + scenario_path = REPO_ROOT / "docs" / "public" / "_static" / "examples" / "first-scenario.sdl.yaml" + + parsed = parse_sdl_file(scenario_path) + + assert parsed.name == "first-scenario" + + +def test_readme_quickstart_matches_checked_in_scenario() -> None: + readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + match = re.search( + r"\s*```yaml\s*(.*?)\s*```\s*", + readme, + re.DOTALL, + ) + assert match is not None + readme_scenario = yaml.safe_load(match.group(1)) + checked_in_scenario = yaml.safe_load( + (REPO_ROOT / "docs" / "public" / "_static" / "examples" / "first-scenario.sdl.yaml").read_text(encoding="utf-8") + ) + + assert readme_scenario == checked_in_scenario diff --git a/implementations/python/tests/test_public_project_readiness.py b/implementations/python/tests/test_public_project_readiness.py new file mode 100644 index 000000000..400177457 --- /dev/null +++ b/implementations/python/tests/test_public_project_readiness.py @@ -0,0 +1,69 @@ +"""Structural checks for public-project security and enrollment metadata.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def test_scorecard_workflow_is_pinned_least_privilege_and_publishes_sarif() -> None: + workflow_path = REPO_ROOT / ".github" / "workflows" / "scorecard.yml" + source = workflow_path.read_text(encoding="utf-8") + workflow = yaml.safe_load(source) + triggers = workflow.get("on", workflow.get(True)) + + assert workflow["permissions"] == "read-all" + assert "pull_request_target" not in triggers + analysis = workflow["jobs"]["analysis"] + assert analysis["permissions"] == { + "contents": "read", + "security-events": "write", + "id-token": "write", + } + scorecard_step = next( + step for step in analysis["steps"] if step.get("uses", "").startswith("ossf/scorecard-action@") + ) + assert scorecard_step["uses"] == ("ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc") + assert scorecard_step["with"] == { + "results_file": "results.sarif", + "results_format": "sarif", + "publish_results": "true", + } + assert "SCORECARD_TOKEN" not in source + for match in re.finditer(r"^\s*uses:\s*([^#\s]+)", source, re.MULTILINE): + assert re.fullmatch(r"[^@]+@[0-9a-f]{40}", match.group(1)) + + +def test_best_practices_proposal_is_factual_about_single_maintainer_limits() -> None: + proposal = json.loads((REPO_ROOT / ".bestpractices.json").read_text(encoding="utf-8")) + + assert proposal["repo_url"] == "https://github.com/RAESystem/rae" + assert proposal["license"] == "MIT" + assert proposal["bus_factor_status"] == "Unmet" + assert proposal["two_person_review_status"] == "Unmet" + assert "one maintainer" in proposal["bus_factor_justification"].casefold() + assert "badge" not in proposal + + +def test_publishers_build_only_the_curated_public_source() -> None: + rtd = yaml.safe_load((REPO_ROOT / ".readthedocs.yaml").read_text(encoding="utf-8")) + assert rtd["sphinx"] == { + "configuration": "docs/public/conf.py", + "fail_on_warning": True, + } + assert rtd["python"]["install"][0]["command"] == "sync --frozen" + + docs_workflow = (REPO_ROOT / ".github" / "workflows" / "docs.yml").read_text(encoding="utf-8") + assert "nox[uv]==2026.4.10" in docs_workflow + assert "-s docs" in docs_workflow + assert "sphinx-build" not in docs_workflow + assert "path: docs/_build/html" in docs_workflow + + makefile = (REPO_ROOT / "docs" / "Makefile").read_text(encoding="utf-8") + assert "SOURCEDIR = public" in makefile + assert "BUILDDIR = _build" in makefile diff --git a/implementations/python/tests/test_repo_policy_tools.py b/implementations/python/tests/test_repo_policy_tools.py index 4e4c25587..f944a7d60 100644 --- a/implementations/python/tests/test_repo_policy_tools.py +++ b/implementations/python/tests/test_repo_policy_tools.py @@ -139,6 +139,59 @@ def test_canonical_verify_does_not_use_change_aware_selection(monkeypatch: pytes assert "_run_docs" in source +def test_docs_graph_uses_curated_root_and_reader_style_gate( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + noxfile = load_noxfile_with_fake_nox(monkeypatch) + commands: list[tuple[str, ...]] = [] + + class FakeSession: + def log(self, _message: str) -> None: + pass + + def run(self, *args: str, **_kwargs: Any) -> None: + commands.append(args) + + fake_vale = tmp_path / "vale" + fake_vale.write_text("", encoding="utf-8") + monkeypatch.setattr(noxfile, "ensure_vale", lambda _repo_root: fake_vale) + monkeypatch.setattr(noxfile, "REPO_ROOT", tmp_path) + monkeypatch.setattr(noxfile, "PROJECT_ROOT", tmp_path / "implementations" / "python") + public_root = tmp_path / "docs" / "public" + monkeypatch.setattr(noxfile, "PUBLIC_DOCS_ROOT", public_root) + monkeypatch.setattr(noxfile, "DOCS_BUILD_ROOT", tmp_path / "docs" / "_build") + reporter = noxfile.SessionReporter(FakeSession(), "docs") + + noxfile._run_docs(reporter.session, reporter) + + assert [result.name for result in reporter.results] == [ + "docs / public source boundary", + "docs / Vale reader style", + "docs / executable quickstart", + "docs / Sphinx HTML", + "docs / public output inventory", + "docs / Sphinx link check", + ] + sphinx_commands = [command for command in commands if "sphinx-build" in command] + assert len(sphinx_commands) == 2 + assert all(str(public_root) in command for command in sphinx_commands) + assert all("-W" in command and "--keep-going" in command for command in sphinx_commands) + vale_command = next(command for command in commands if command and command[0] == str(fake_vale)) + assert "--config=.vale.ini" in vale_command + assert "README.md" in vale_command + assert str(public_root) in vale_command + pytest_command = next(command for command in commands if "pytest" in command) + assert ( + "implementations/python/tests/test_public_docs_policy.py::test_checked_in_quickstart_scenario_parses" + in pytest_command + ) + assert ( + "implementations/python/tests/test_public_docs_policy.py::test_readme_quickstart_matches_checked_in_scenario" + in pytest_command + ) + + def write_text(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") diff --git a/implementations/python/tests/test_vale_tool.py b/implementations/python/tests/test_vale_tool.py new file mode 100644 index 000000000..010174793 --- /dev/null +++ b/implementations/python/tests/test_vale_tool.py @@ -0,0 +1,71 @@ +"""Tests for the pinned repository-local Vale installer.""" + +from __future__ import annotations + +import io +import sys +import tarfile +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[3] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +import tools.vale_tool as vale_tool # noqa: E402 + + +@pytest.mark.parametrize( + ("system", "machine", "expected"), + [ + ("Linux", "x86_64", "vale_3.15.2_Linux_64-bit.tar.gz"), + ("Linux", "aarch64", "vale_3.15.2_Linux_arm64.tar.gz"), + ("Darwin", "x86_64", "vale_3.15.2_macOS_64-bit.tar.gz"), + ("Darwin", "arm64", "vale_3.15.2_macOS_arm64.tar.gz"), + ], +) +def test_vale_release_asset_matches_supported_platform( + monkeypatch: pytest.MonkeyPatch, + system: str, + machine: str, + expected: str, +) -> None: + monkeypatch.setattr("platform.system", lambda: system) + monkeypatch.setattr("platform.machine", lambda: machine) + + assert vale_tool._release_asset_name("3.15.2") == expected + + +def test_vale_binary_uses_versioned_repository_cache(tmp_path: Path) -> None: + assert vale_tool.vale_binary_path(tmp_path, version="3.15.2") == ( + tmp_path / ".cache" / "raes-sdl" / "tooling" / "vale" / "3.15.2" / "vale" + ) + + +def test_vale_assets_have_repository_pinned_checksums() -> None: + assert vale_tool.VALE_ARCHIVE_SHA256 == { + "vale_3.15.2_Linux_64-bit.tar.gz": "fc72e64454d6bd7af91905d4faebbf411bae3eec17bb572f4101311212bc0d9e", + "vale_3.15.2_Linux_arm64.tar.gz": "e8240a3304e2c07b0476d30423f241a80296865cf6d2b78b128fb7e4e14cbb69", + "vale_3.15.2_macOS_64-bit.tar.gz": "5d56b292f1612758f6d9e8d735dd739aec4e475830d0ba8c1e0ef7d8f08fa198", + "vale_3.15.2_macOS_arm64.tar.gz": "d3f613ff9226935ace08895fc8557206f309cdbd3a81881d86b6ab5b8b408757", + } + + +def test_vale_rejects_an_asset_without_a_repository_pin(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + monkeypatch.setattr(vale_tool, "_release_asset_name", lambda _version: "vale_unpinned.tar.gz") + + with pytest.raises(RuntimeError, match="no repository-pinned checksum"): + vale_tool.ensure_vale(tmp_path, version="3.15.2") + + +def test_vale_extraction_rejects_archive_without_root_binary(tmp_path: Path) -> None: + payload = io.BytesIO() + with tarfile.open(fileobj=payload, mode="w:gz") as archive: + info = tarfile.TarInfo("../vale") + body = b"unsafe" + info.size = len(body) + archive.addfile(info, io.BytesIO(body)) + + with pytest.raises(RuntimeError, match="root vale binary"): + vale_tool._extract_binary(payload.getvalue(), tmp_path / "vale") diff --git a/implementations/python/tests/test_verification_plan.py b/implementations/python/tests/test_verification_plan.py index 9c1844df6..4def6f554 100644 --- a/implementations/python/tests/test_verification_plan.py +++ b/implementations/python/tests/test_verification_plan.py @@ -65,7 +65,23 @@ def test_executable_contract_configuration_and_unknown_paths_fail_closed(path: s assert plan.contracts assert plan.regression assert plan.fuzz - assert not plan.docs + assert plan.docs + + +@pytest.mark.parametrize( + "path", + [ + ".vale.ini", + "styles/RAES/PlainWords.yml", + "tools/check_public_docs.py", + "tools/vale_tool.py", + ".github/workflows/docs.yml", + ".readthedocs.yaml", + "noxfile.py", + ], +) +def test_documentation_tooling_changes_select_docs_graph(path: str) -> None: + assert _plan(path).docs @pytest.mark.parametrize("status", ["D", "R", "C", "T"]) diff --git a/implementations/python/uv.lock b/implementations/python/uv.lock index bb46d727e..a5c92b510 100644 --- a/implementations/python/uv.lock +++ b/implementations/python/uv.lock @@ -1272,6 +1272,7 @@ docs = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-autobuild" }, { name = "sphinx-copybutton" }, + { name = "sphinx-reredirects" }, ] [package.metadata] @@ -1300,6 +1301,7 @@ requires-dist = [ { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.3.0" }, { name = "sphinx-autobuild", marker = "extra == 'docs'", specifier = ">=2024.4.16" }, { name = "sphinx-copybutton", marker = "extra == 'docs'", specifier = ">=0.5.2" }, + { name = "sphinx-reredirects", marker = "extra == 'docs'", specifier = "==1.1.0" }, { name = "sse-starlette", specifier = ">=2.0.0" }, { name = "typer", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, @@ -1617,6 +1619,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, ] +[[package]] +name = "sphinx-reredirects" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/8d/0e39fe2740d7d71417edf9a6424aa80ca2c27c17fc21282cdc39f90d5a40/sphinx_reredirects-1.1.0.tar.gz", hash = "sha256:fb9b195335ab14b43f8273287d0c7eeb637ba6c56c66581c11b47202f6718b29", size = 614624, upload-time = "2025-12-22T08:28:02.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/81/b5dd07067f3daac6d23687ec737b2d593740671ebcd145830c8f92d381c5/sphinx_reredirects-1.1.0-py3-none-any.whl", hash = "sha256:4b5692273c72cd2d4d917f4c6f87d5919e4d6114a752d4be033f7f5f6310efd9", size = 6351, upload-time = "2025-12-22T08:27:59.724Z" }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" diff --git a/noxfile.py b/noxfile.py index 90ffa8e02..232d628eb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from pathlib import Path from time import perf_counter +import shutil import subprocess import sys import tempfile @@ -22,6 +23,7 @@ run_osv_scanner, ) from tools.tool_versions import PRE_COMMIT_HOOKS_TOOL_SPEC, RUFF_TOOL_SPEC +from tools.vale_tool import ensure_vale from tools.verification_plan import ( collect_git_changes, plan_for_changes, @@ -29,6 +31,21 @@ ) PROJECT_ROOT = REPO_ROOT / "implementations" / "python" +PUBLIC_DOCS_ROOT = REPO_ROOT / "docs" / "public" +DOCS_BUILD_ROOT = REPO_ROOT / "docs" / "_build" +PUBLIC_DOCS_ENTRYPOINTS = ( + "README.md", + "CONTRIBUTING.md", + "CODE_OF_CONDUCT.md", + "GOVERNANCE.md", + "MAINTAINERS.md", + "SECURITY.md", + "SUPPORT.md", +) +PUBLIC_DOCS_EXAMPLE_TESTS = ( + "implementations/python/tests/test_public_docs_policy.py::test_checked_in_quickstart_scenario_parses", + "implementations/python/tests/test_public_docs_policy.py::test_readme_quickstart_matches_checked_in_scenario", +) RUFF_CONFIG = PROJECT_ROOT / "pyproject.toml" OSV_LOCKFILE_PATH = PROJECT_ROOT / "uv.lock" OSV_REPORT_PATH = PROJECT_ROOT / "osv-scanner-report.json" @@ -41,6 +58,9 @@ "implementations/python/tests/test_concept_authority_governance.py", "implementations/python/tests/test_agent_guidance_policy.py", "implementations/python/tests/test_example_library_policy.py", + "implementations/python/tests/test_public_docs_policy.py", + "implementations/python/tests/test_public_project_readiness.py", + "implementations/python/tests/test_vale_tool.py", "implementations/python/tests/test_verification_plan.py", ] CONTRACT_TRIGGER_PREFIXES = ( @@ -858,11 +878,13 @@ def _scan() -> None: def _run_docs(session: nox.Session, reporter: SessionReporter) -> None: _sync_project(session) - docs_dir = REPO_ROOT / "docs" - build_dir = docs_dir / "_build" / "html" - reporter.run( - "docs / sphinx-build", - lambda: _run( + html_dir = DOCS_BUILD_ROOT / "html" + linkcheck_dir = DOCS_BUILD_ROOT / "linkcheck" + + def _build(builder: str, output_dir: Path, *, clean: bool = False) -> None: + if clean: + shutil.rmtree(output_dir, ignore_errors=True) + _run( session, "uv", "run", @@ -870,12 +892,64 @@ def _run_docs(session: nox.Session, reporter: SessionReporter) -> None: str(PROJECT_ROOT), "--frozen", "sphinx-build", + "-W", + "--keep-going", "-b", - "html", - str(docs_dir), - str(build_dir), + builder, + str(PUBLIC_DOCS_ROOT), + str(output_dir), + ) + + reporter.run( + "docs / public source boundary", + lambda: _run_project_python(session, "tools/check_public_docs.py"), + ) + reporter.run( + "docs / Vale reader style", + lambda: _run( + session, + str(ensure_vale(REPO_ROOT)), + "--config=.vale.ini", + "--glob=*.md", + *PUBLIC_DOCS_ENTRYPOINTS, + str(PUBLIC_DOCS_ROOT), + ), + detail="Stripe-inspired RAES style", + ) + reporter.run( + "docs / executable quickstart", + lambda: _run( + session, + "uv", + "run", + "--project", + str(PROJECT_ROOT), + "--frozen", + "python", + "-m", + "pytest", + "-q", + *PUBLIC_DOCS_EXAMPLE_TESTS, ), - detail=f"{docs_dir.relative_to(REPO_ROOT)} -> {build_dir.relative_to(REPO_ROOT)}", + ) + reporter.run( + "docs / Sphinx HTML", + lambda: _build("html", html_dir, clean=True), + detail=f"{PUBLIC_DOCS_ROOT.relative_to(REPO_ROOT)} -> {html_dir.relative_to(REPO_ROOT)}", + ) + reporter.run( + "docs / public output inventory", + lambda: _run_project_python( + session, + "tools/check_public_docs.py", + "--output", + str(html_dir), + ), + ) + reporter.run( + "docs / Sphinx link check", + lambda: _build("linkcheck", linkcheck_dir), + detail=str(PUBLIC_DOCS_ROOT.relative_to(REPO_ROOT)), ) diff --git a/specs/agent-guidance/agent-guidance.yaml b/specs/agent-guidance/agent-guidance.yaml index afb202806..3599f157e 100644 --- a/specs/agent-guidance/agent-guidance.yaml +++ b/specs/agent-guidance/agent-guidance.yaml @@ -49,7 +49,7 @@ guidance: start a live range. source_refs: - implementations/python/packages/raes_mcp/tools/operations.py - - docs/explain/getting-started.md + - docs/public/quickstart.md - docs/explain/sdl/language-service.md - id: reference-implementation-not-authority @@ -202,7 +202,7 @@ guidance: controls. source_refs: - implementations/python/packages/raes_mcp/tools/operations.py - - docs/explain/getting-started.md + - docs/public/quickstart.md - id: do-not-carry-secrets audience: [contributor, operator] @@ -224,4 +224,4 @@ guidance: backed. source_refs: - implementations/python/packages/raes_mcp/tools/operations.py - - docs/explain/getting-started.md + - docs/public/quickstart.md diff --git a/specs/authority/authority-boundary.yaml b/specs/authority/authority-boundary.yaml index d87bb6ea4..c6d328101 100644 --- a/specs/authority/authority-boundary.yaml +++ b/specs/authority/authority-boundary.yaml @@ -104,6 +104,10 @@ non_normative_roots: root: tools/ note: maintenance, codegen, and release helpers + - id: documentation_styles + root: styles/ + note: non-normative documentation lint rules and project vocabulary + # Top-level directories explicitly transitioned out by ADR-009. The gate fails # if any of these reappears at the repo root. legacy_top_level_dirs: diff --git a/styles/RAES/AvoidDismissive.yml b/styles/RAES/AvoidDismissive.yml new file mode 100644 index 000000000..bb57baf45 --- /dev/null +++ b/styles/RAES/AvoidDismissive.yml @@ -0,0 +1,6 @@ +extends: existence +message: "Remove dismissive wording ('%s') and state the action directly." +level: error +ignorecase: true +tokens: + - '\b(?:simply|just|obviously|trivial(?:ly)?|easily|easy|straightforward)\b' diff --git a/styles/RAES/AvoidHype.yml b/styles/RAES/AvoidHype.yml new file mode 100644 index 000000000..fb9fd7064 --- /dev/null +++ b/styles/RAES/AvoidHype.yml @@ -0,0 +1,6 @@ +extends: existence +message: "Replace promotional wording ('%s') with a measurable current-state claim." +level: error +ignorecase: true +tokens: + - '\b(?:powerful|seamless|world-class|production-ready|comprehensive|state[- ]of[- ]the[- ]art)\b' diff --git a/styles/RAES/HeadingCase.yml b/styles/RAES/HeadingCase.yml new file mode 100644 index 000000000..af1f6be1d --- /dev/null +++ b/styles/RAES/HeadingCase.yml @@ -0,0 +1,21 @@ +extends: capitalization +message: "Use sentence case for headings: found '%s'; expected '%s'." +level: error +scope: heading +match: $sentence +exceptions: + - API + - CLI + - Conformance + - GitHub + - MCP + - OpenSSF + - PyPI + - Python + - RAES + - Reproducible Agentic Environments System + - Release + - Please + - SDL + - SARIF + - Vale diff --git a/styles/RAES/HeadingPunctuation.yml b/styles/RAES/HeadingPunctuation.yml new file mode 100644 index 000000000..02e14d970 --- /dev/null +++ b/styles/RAES/HeadingPunctuation.yml @@ -0,0 +1,7 @@ +extends: existence +message: "Do not end a heading with punctuation." +level: error +scope: heading +nonword: true +tokens: + - '[.!?:;]$' diff --git a/styles/RAES/PlainWords.yml b/styles/RAES/PlainWords.yml new file mode 100644 index 000000000..a8888f1c2 --- /dev/null +++ b/styles/RAES/PlainWords.yml @@ -0,0 +1,15 @@ +extends: substitution +message: "Use '%s' instead of '%s'." +level: error +ignorecase: true +swap: + '\bin order to\b': to + '\bprior to\b': before + '\bsubsequent to\b': after + '\butili[sz]e\b': use + '\butili[sz]es\b': uses + '\butili[sz]ing\b': using + '\bfacilitate\b': help + '\bleverage\b': use + '\bcommence\b': start + '\bterminate\b': stop diff --git a/styles/RAES/ReadingLevel.yml b/styles/RAES/ReadingLevel.yml new file mode 100644 index 000000000..a81c02647 --- /dev/null +++ b/styles/RAES/ReadingLevel.yml @@ -0,0 +1,6 @@ +extends: metric +message: "Keep the Flesch-Kincaid grade level at 11 or below (found %s)." +level: error +formula: | + (0.39 * (words / sentences)) + (11.8 * (syllables / words)) - 15.59 +condition: "> 11.0" diff --git a/styles/RAES/SentenceLength.yml b/styles/RAES/SentenceLength.yml new file mode 100644 index 000000000..62df04b3b --- /dev/null +++ b/styles/RAES/SentenceLength.yml @@ -0,0 +1,6 @@ +extends: occurrence +message: "Keep sentences to 32 words or fewer (found %s)." +level: error +scope: sentence +max: 32 +token: '\b[\p{L}\p{N}_''-]+\b' diff --git a/styles/config/vocabularies/RAES/accept.txt b/styles/config/vocabularies/RAES/accept.txt new file mode 100644 index 000000000..8c6e8195c --- /dev/null +++ b/styles/config/vocabularies/RAES/accept.txt @@ -0,0 +1,26 @@ +autodoc +backend +backends +bestpractices +conformance +Flesch +GitGuardian +intersphinx +libvirt +MyST +nox +OpenSSF +provenance +PyPI +quickstart +Read the Docs +release-please +reStructuredText +SARIF +Scorecard +SDL +Sphinx +toctree +Typer +uv +Vale diff --git a/tools/check_project_positioning.py b/tools/check_project_positioning.py index ac2b4df44..b0c680708 100644 --- a/tools/check_project_positioning.py +++ b/tools/check_project_positioning.py @@ -33,14 +33,14 @@ PRIMARY_ENTRYPOINTS = ( "README.md", - "docs/index.md", - "docs/explain/getting-started.md", + "docs/public/index.md", ) SUPPORTING_DOCS = ( - "docs/explain/reference/glossary.md", - "docs/explain/reference/canonical-reference-map.md", - "docs/explain/sdl/index.md", - "docs/explain/sdl/runtime-architecture.md", + "docs/public/quickstart.md", + "docs/public/concepts.md", + "docs/public/sdl/index.md", + "docs/public/backends.md", + "docs/public/research.md", "examples/README.md", ) APPLICATION_AREAS = ( @@ -121,11 +121,11 @@ def _validate_entrypoints( ) for relative_path in SUPPORTING_DOCS: - if _missing_terms(surfaces[relative_path], ("agentic environment",)): + if _missing_terms(surfaces[relative_path], ("RAES",)): failures.append( PolicyFailure( RULE_FRAMING, - "supporting entrypoint must connect its subject to agentic environments", + "supporting entrypoint must connect its subject to RAES", relative_path, ) ) @@ -141,30 +141,20 @@ def _validate_entrypoints( ) ) - getting_started = surfaces["docs/explain/getting-started.md"] + concepts = surfaces["docs/public/concepts.md"] required_boundaries = ( "authored scenario", "realized environment", - "bounded reproduction attempt", - "does not guarantee", + "bounded reproduction", + "does not", "exact replay", ) - if _missing_terms(getting_started, required_boundaries): + if _missing_terms(concepts, required_boundaries): failures.append( PolicyFailure( RULE_CLAIM_BOUNDARY, - "getting-started guidance must retain the bounded reproduction and nonclaim boundary", - "docs/explain/getting-started.md", - ) - ) - - glossary = surfaces["docs/explain/reference/glossary.md"] - if _missing_terms(glossary, ("**RAES**", "**Agentic environment**", "**Reproducibility support**")): - failures.append( - PolicyFailure( - RULE_FRAMING, - "glossary must define RAES, agentic environment, and reproducibility support", - "docs/explain/reference/glossary.md", + "concept guidance must retain the bounded reproduction and nonclaim boundary", + "docs/public/concepts.md", ) ) @@ -187,7 +177,7 @@ def _validate_structured_metadata(repo_root: Path, failures: list[PolicyFailure] ) ) - conf_path = "docs/conf.py" + conf_path = "docs/public/conf.py" conf_text = _read_required(repo_root, conf_path, failures) project_name = _literal_assignment(conf_text, "project") html_title = _literal_assignment(conf_text, "html_title") diff --git a/tools/check_public_docs.py b/tools/check_public_docs.py new file mode 100644 index 000000000..9cba68c2f --- /dev/null +++ b/tools/check_public_docs.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +"""Validate the curated public documentation source and generated inventory.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from pathlib import Path +from urllib.parse import urlparse + +from defusedxml import ElementTree + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from tools.policy.common import PolicyFailure, failures_to_json, safe_repo_path # noqa: E402 + +PUBLIC_DOCS_ROOT = Path("docs/public") +MAX_SOURCE_BYTES = 1_000_000 +MAX_INDEX_BYTES = 5_000_000 +MAX_REDIRECT_BYTES = 100_000 +REQUIRED_PUBLIC_PAGES = ( + "index.md", + "quickstart.md", + "concepts.md", + "tutorials/first-scenario.md", + "sdl/index.md", + "guides/python.md", + "guides/cli.md", + "backends.md", + "research.md", + "limitations.md", + "contributing.md", + "support.md", + "citation.md", +) +REQUIRED_PUBLIC_REDIRECTS = { + "explain/getting-started": "../quickstart.html", + "explain/reference/backend-conformance": "../../backends.html", + "explain/reference/canonical-reference-map": "../../concepts.html", + "explain/reference/documentation-style-guide": "../../contributing.html", + "explain/reference/glossary": "../../concepts.html", + "explain/sdl/index": "../../sdl/index.html", + "explain/sdl/limitations": "../../limitations.html", + "explain/sdl/lineage": "../../research.html", + "explain/sdl/related-work-comparison": "../../research.html", + "explain/sdl/runtime-architecture": "../../backends.html", + "explain/sdl/scientific-scenario-completeness": "../../research.html", + "specs/formal": "../research.html", +} +SOURCE_SUFFIXES = frozenset({".md", ".rst"}) +GENERATED_HTML_ROUTES = frozenset( + { + "genindex.html", + "py-modindex.html", + "search.html", + } +) +LOCAL_DIRECTIVE_PATTERNS = ( + re.compile( + r"^\s*(?:```\{|\.\.\s+)(?:include|literalinclude|download)(?:\}|::)\s+([^\s]+)", + re.MULTILINE | re.IGNORECASE, + ), + re.compile(r"\{download\}`(?:[^`<]*<)?([^>`]+)>?`", re.IGNORECASE), +) + + +def _public_path(repo_root: Path) -> Path | None: + return safe_repo_path(repo_root, PUBLIC_DOCS_ROOT.as_posix()) + + +def _relative(repo_root: Path, path: Path) -> str: + return path.relative_to(repo_root).as_posix() + + +def _source_paths(public_root: Path) -> list[Path]: + return sorted( + path + for path in public_root.rglob("*") + if path.is_file() and not path.is_symlink() and path.suffix.casefold() in SOURCE_SUFFIXES + ) + + +def _redirects_path(public_root: Path) -> Path: + return public_root / "redirects.json" + + +def _load_redirects(public_root: Path) -> dict[str, str]: + path = _redirects_path(public_root) + if not path.is_file() or path.is_symlink() or path.stat().st_size > MAX_REDIRECT_BYTES: + raise ValueError("redirect map is missing, unsafe, or exceeds the inspection limit") + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict) or not all( + isinstance(source, str) and isinstance(target, str) for source, target in payload.items() + ): + raise ValueError("redirect map must contain string source and target pairs") + return payload + + +def _route_for_source(public_root: Path, source: Path) -> str: + relative = source.relative_to(public_root) + return relative.with_suffix(".html").as_posix() + + +def _docname_for_source(public_root: Path, source: Path) -> str: + return source.relative_to(public_root).with_suffix("").as_posix() + + +def _directive_targets(text: str) -> list[str]: + return [ + match.group(1).strip().strip("\"'") for pattern in LOCAL_DIRECTIVE_PATTERNS for match in pattern.finditer(text) + ] + + +def _target_is_contained(public_root: Path, source: Path, target: str) -> bool: + if "://" in target or target.startswith(("mailto:", "#")): + return True + clean_target = target.split("#", 1)[0] + if not clean_target: + return True + candidate = Path(clean_target) + if candidate.is_absolute(): + return False + try: + resolved = (source.parent / candidate).resolve() + resolved.relative_to(public_root.resolve()) + except (OSError, ValueError): + return False + return True + + +def evaluate_public_sources(repo_root: Path = REPO_ROOT) -> list[PolicyFailure]: + """Return publication-boundary failures for checked-in public sources.""" + + failures: list[PolicyFailure] = [] + public_root = _public_path(repo_root) + if public_root is None or not public_root.is_dir(): + return [ + PolicyFailure( + "public-docs-root", + "the curated public documentation root is missing or unsafe", + PUBLIC_DOCS_ROOT.as_posix(), + ) + ] + + for relative_path in REQUIRED_PUBLIC_PAGES: + path = public_root / relative_path + if not path.is_file() or path.is_symlink(): + failures.append( + PolicyFailure( + "public-docs-information-architecture", + "required public page is missing", + (PUBLIC_DOCS_ROOT / relative_path).as_posix(), + ) + ) + + redirects_path = _redirects_path(public_root) + try: + redirects = _load_redirects(public_root) + except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc: + failures.append( + PolicyFailure( + "public-docs-redirect-map", + str(exc), + _relative(repo_root, redirects_path), + ) + ) + else: + if redirects != REQUIRED_PUBLIC_REDIRECTS: + failures.append( + PolicyFailure( + "public-docs-redirect-map", + "redirect map must preserve the complete established public-route contract", + _relative(repo_root, redirects_path), + ) + ) + + for current, directories, files in os.walk(public_root, followlinks=False): + current_path = Path(current) + for name in [*directories, *files]: + path = current_path / name + if path.is_symlink(): + failures.append( + PolicyFailure( + "public-docs-symlink", + "public documentation must not contain symlinks", + _relative(repo_root, path), + ) + ) + + for source in _source_paths(public_root): + relative_path = _relative(repo_root, source) + try: + if source.stat().st_size > MAX_SOURCE_BYTES: + failures.append( + PolicyFailure( + "public-docs-source-size", + f"public source exceeds the {MAX_SOURCE_BYTES}-byte inspection limit", + relative_path, + ) + ) + continue + text = source.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + failures.append( + PolicyFailure( + "public-docs-source-readable", + "public source must be readable UTF-8 text", + relative_path, + ) + ) + continue + if any(not _target_is_contained(public_root, source, target) for target in _directive_targets(text)): + failures.append( + PolicyFailure( + "public-docs-source-escape", + "include, literalinclude, and download targets must stay inside docs/public", + relative_path, + ) + ) + + return sorted(failures, key=lambda failure: (failure.path or "", failure.rule_id)) + + +def _search_docnames(search_index_path: Path) -> set[str]: + if not search_index_path.is_file() or search_index_path.stat().st_size > MAX_INDEX_BYTES: + raise ValueError("searchindex.js is missing or exceeds the inspection limit") + text = search_index_path.read_text(encoding="utf-8") + match = re.fullmatch(r"\s*Search\.setIndex\((.*)\)\s*;?\s*", text, re.DOTALL) + if match is None: + raise ValueError("searchindex.js does not contain a recognized Sphinx index") + payload = json.loads(match.group(1)) + docnames = payload.get("docnames") + if not isinstance(docnames, list) or not all(isinstance(item, str) for item in docnames): + raise ValueError("searchindex.js has no string docnames inventory") + return set(docnames) + + +def _sitemap_routes(sitemap_path: Path) -> set[str]: + tree = ElementTree.parse(sitemap_path) + routes: set[str] = set() + for location in tree.findall(".//{*}loc"): + if location.text: + routes.add(urlparse(location.text).path.strip("/") or "index.html") + return routes + + +def evaluate_public_output(repo_root: Path, output_root: Path) -> list[PolicyFailure]: + """Return failures when generated routes exceed the curated source inventory.""" + + public_root = _public_path(repo_root) + if public_root is None or not public_root.is_dir(): + return evaluate_public_sources(repo_root) + try: + output_root.resolve().relative_to(repo_root.resolve()) + except (OSError, ValueError): + return [ + PolicyFailure( + "public-docs-output-root", + "generated documentation output must stay inside the repository", + output_root.as_posix(), + ) + ] + + expected_routes = {_route_for_source(public_root, source) for source in _source_paths(public_root)} + redirect_routes = {f"{source}.html" for source in REQUIRED_PUBLIC_REDIRECTS} + expected_docnames = {_docname_for_source(public_root, source) for source in _source_paths(public_root)} + failures: list[PolicyFailure] = [] + actual_routes = { + path.relative_to(output_root).as_posix() + for path in output_root.rglob("*.html") + if "_static" not in path.relative_to(output_root).parts + } + for route in sorted(actual_routes - expected_routes - redirect_routes - GENERATED_HTML_ROUTES): + failures.append( + PolicyFailure( + "public-docs-output-route", + "generated HTML route has no source beneath docs/public", + route, + ) + ) + + for source, target in REQUIRED_PUBLIC_REDIRECTS.items(): + route = f"{source}.html" + redirect_path = output_root / route + try: + if not redirect_path.is_file() or redirect_path.stat().st_size > MAX_REDIRECT_BYTES: + raise ValueError("redirect page is missing or exceeds the inspection limit") + redirect_html = redirect_path.read_text(encoding="utf-8") + normalized_html = re.sub(r"\s+", " ", redirect_html.casefold()) + if 'http-equiv="refresh"' not in normalized_html or f"url={target.casefold()}" not in normalized_html: + raise ValueError("redirect page does not point to its curated public target") + except (OSError, UnicodeDecodeError, ValueError) as exc: + failures.append( + PolicyFailure( + "public-docs-redirect-output", + str(exc), + route, + ) + ) + + try: + search_docnames = _search_docnames(output_root / "searchindex.js") + except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc: + failures.append( + PolicyFailure( + "public-docs-search-index", + str(exc), + (output_root / "searchindex.js").as_posix(), + ) + ) + else: + for docname in sorted(search_docnames - expected_docnames): + failures.append( + PolicyFailure( + "public-docs-search-route", + "search document has no source beneath docs/public", + docname, + ) + ) + + sitemap_path = output_root / "sitemap.xml" + if sitemap_path.is_file(): + try: + sitemap_routes = _sitemap_routes(sitemap_path) + except (OSError, ElementTree.ParseError) as exc: + failures.append( + PolicyFailure( + "public-docs-sitemap", + f"invalid sitemap: {exc}", + sitemap_path.as_posix(), + ) + ) + else: + allowed_routes = expected_routes | redirect_routes | GENERATED_HTML_ROUTES + mappings: list[tuple[str, str, str]] = [] + unexpected_routes: set[str] = set() + for route in sitemap_routes: + matches = [allowed for allowed in allowed_routes if route == allowed or route.endswith(f"/{allowed}")] + if not matches: + unexpected_routes.add(route) + continue + matched = max(matches, key=len) + mappings.append((route, matched, route[: -len(matched)])) + expected_prefixes = {prefix for _route, matched, prefix in mappings if matched in expected_routes} + if len(expected_prefixes) > 1: + common_prefix = max( + expected_prefixes, + key=lambda prefix: sum(item_prefix == prefix for _, _, item_prefix in mappings), + ) + unexpected_routes.update( + route + for route, matched, prefix in mappings + if matched in expected_routes and prefix != common_prefix + ) + represented = {matched for _, matched, _ in mappings if matched in expected_routes} + if expected_routes - represented: + unexpected_routes.add("missing public source routes") + for route in sorted(unexpected_routes): + failures.append( + PolicyFailure( + "public-docs-sitemap-route", + "sitemap route has no source beneath docs/public", + route, + ) + ) + return failures + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, help="Also validate a generated Sphinx HTML directory.") + parser.add_argument("--json", action="store_true", help="Emit failures as JSON.") + args = parser.parse_args() + + failures = evaluate_public_sources(REPO_ROOT) + if not failures and args.output is not None: + failures.extend(evaluate_public_output(REPO_ROOT, args.output)) + if args.json: + print(failures_to_json(failures)) + else: + for failure in failures: + print(failure.render()) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/policy/requirement_order.yaml b/tools/policy/requirement_order.yaml index e94cfcf13..5d159e7a7 100644 --- a/tools/policy/requirement_order.yaml +++ b/tools/policy/requirement_order.yaml @@ -62,6 +62,7 @@ phases: - AUT-807 - AUT-809 - ASR-516 + - DOC-928 blocked_until: - gov-concept-authority - id: project-identity-migration @@ -186,11 +187,43 @@ ownership: - specs/formal - CHANGELOG.md documentation-surfaces: + - .bestpractices.json + - .readthedocs.yaml + - .vale.ini + - .github/ISSUE_TEMPLATE + - .github/PULL_REQUEST_TEMPLATE.md + - .github/workflows/docs.yml + - .github/workflows/scorecard.yml + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - GOVERNANCE.md + - MAINTAINERS.md + - README.md + - SECURITY.md + - SUPPORT.md - docs + - examples/library/catalog.yaml - implementations/python/pyproject.toml - implementations/python/uv.lock - implementations/python/packages/raes_mcp + - implementations/python/tests/test_project_positioning.py + - implementations/python/tests/test_public_docs_policy.py + - implementations/python/tests/test_public_project_readiness.py + - implementations/python/tests/test_repo_policy_tools.py + - implementations/python/tests/test_vale_tool.py + - implementations/python/tests/test_verification_plan.py - implementations/python/tests/test_mcp_server.py + - noxfile.py + - specs/agent-guidance + - specs/authority/authority-boundary.yaml + - styles/RAES + - styles/config/vocabularies/RAES + - tools/check_project_positioning.py + - tools/check_public_docs.py + - tools/policy/requirement_order.yaml + - tools/tool_versions.py + - tools/vale_tool.py + - tools/verification_plan.py - CHANGELOG.md project-identity-migration: - .github diff --git a/tools/tool_versions.py b/tools/tool_versions.py index 4b80f6c2a..1c1701f8c 100644 --- a/tools/tool_versions.py +++ b/tools/tool_versions.py @@ -7,3 +7,4 @@ CONTFEST_VERSION = "0.68.0" GITLEAKS_VERSION = "8.30.1" OSV_SCANNER_VERSION = "2.4.0" +VALE_VERSION = "3.15.2" diff --git a/tools/vale_tool.py b/tools/vale_tool.py new file mode 100644 index 000000000..f713e9194 --- /dev/null +++ b/tools/vale_tool.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import io +import platform +import shutil +import stat +import tarfile +from hashlib import sha256 +from pathlib import Path +from urllib.error import HTTPError, URLError +from urllib.request import urlopen + +from tools.tool_versions import VALE_VERSION + +REPO_ROOT = Path(__file__).resolve().parents[1] + +VALE_ARCHIVE_SHA256 = { + "vale_3.15.2_Linux_64-bit.tar.gz": "fc72e64454d6bd7af91905d4faebbf411bae3eec17bb572f4101311212bc0d9e", + "vale_3.15.2_Linux_arm64.tar.gz": "e8240a3304e2c07b0476d30423f241a80296865cf6d2b78b128fb7e4e14cbb69", + "vale_3.15.2_macOS_64-bit.tar.gz": "5d56b292f1612758f6d9e8d735dd739aec4e475830d0ba8c1e0ef7d8f08fa198", + "vale_3.15.2_macOS_arm64.tar.gz": "d3f613ff9226935ace08895fc8557206f309cdbd3a81881d86b6ab5b8b408757", +} + + +def _release_base_url(version: str = VALE_VERSION) -> str: + return f"https://github.com/errata-ai/vale/releases/download/v{version}" + + +def _release_asset_name(version: str = VALE_VERSION) -> str: + system = platform.system() + machine = platform.machine().lower() + arch_map = { + "x86_64": "64-bit", + "amd64": "64-bit", + "arm64": "arm64", + "aarch64": "arm64", + } + platform_map = { + "Linux": "Linux", + "Darwin": "macOS", + } + arch = arch_map.get(machine) + platform_name = platform_map.get(system) + if arch is None: + raise RuntimeError(f"unsupported Vale architecture: {machine}") + if platform_name is None: + raise RuntimeError(f"unsupported Vale platform: {system}") + return f"vale_{version}_{platform_name}_{arch}.tar.gz" + + +def vale_binary_path(repo_root: Path = REPO_ROOT, *, version: str = VALE_VERSION) -> Path: + return repo_root / ".cache" / "raes-sdl" / "tooling" / "vale" / version / "vale" + + +def _extract_binary(archive_bytes: bytes, binary_path: Path) -> None: + with tarfile.open(fileobj=io.BytesIO(archive_bytes), mode="r:gz") as archive: + try: + member = archive.getmember("vale") + except KeyError as exc: + raise RuntimeError("Vale archive does not contain a root vale binary") from exc + if not member.isfile(): + raise RuntimeError("Vale archive root vale member is not a regular file") + extracted = archive.extractfile(member) + if extracted is None: + raise RuntimeError("Vale archive root vale binary cannot be read") + binary_bytes = extracted.read() + + binary_path.parent.mkdir(parents=True, exist_ok=True) + temporary_path = binary_path.with_suffix(".download") + temporary_path.write_bytes(binary_bytes) + temporary_path.chmod(temporary_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + shutil.move(temporary_path, binary_path) + + +def ensure_vale(repo_root: Path = REPO_ROOT, *, version: str = VALE_VERSION) -> Path: + binary_path = vale_binary_path(repo_root, version=version) + if binary_path.exists(): + return binary_path + + asset_name = _release_asset_name(version) + expected = VALE_ARCHIVE_SHA256.get(asset_name) + if expected is None: + raise RuntimeError(f"no repository-pinned checksum for Vale asset {asset_name}") + base_url = _release_base_url(version) + asset_url = f"{base_url}/{asset_name}" + try: + with urlopen(asset_url) as response: # noqa: S310 - pinned HTTPS release asset + archive_bytes = response.read() + except (HTTPError, URLError) as exc: + raise RuntimeError(f"failed to download Vale from {asset_url}: {exc}") from exc + actual = sha256(archive_bytes).hexdigest() + if actual != expected: + raise RuntimeError(f"Vale checksum mismatch for {asset_name}: expected {expected}, got {actual}") + + _extract_binary(archive_bytes, binary_path) + return binary_path diff --git a/tools/verification_plan.py b/tools/verification_plan.py index 58156a057..337c55607 100644 --- a/tools/verification_plan.py +++ b/tools/verification_plan.py @@ -35,7 +35,7 @@ class VerificationPlan: contracts=True, regression=True, fuzz=True, - docs=False, + docs=True, reason="full verification required", ) From 6cd4cef7c693c8647cf2375143f149dae6b412c4 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 05:44:15 +0200 Subject: [PATCH 40/55] feat: define portable artifact requirement satisfaction --- .../invalid/constrained-without-domain.json | 23 + .../dangling-materialization-input.json | 38 + .../invalid/exact-selector-mismatch.json | 32 + .../invalid/exact-with-fallback.json | 40 + .../invalid/missing-artifact-requirement.json | 7 + .../invalid/open-with-candidate.json | 34 + .../invalid/open-with-constraint.json | 32 + .../invalid/open-with-materialization.json | 35 + .../invalid/ungoverned-mechanism.json | 23 + .../invalid/unknown-field.json | 24 + .../valid/constrained-materialization.json | 51 + .../valid/exact-artifact.json | 32 + .../backend-manifest-v2/valid/stub.json | 2 + .../entries/artifact-requirement-v1.json | 10 + .../entries/backend-manifest-v2.json | 6 +- .../entries/backend-profile-v1.json | 6 +- .../instantiated-scenario-snapshot-v1.json | 6 +- .../entries/instantiated-scenario-v1.json | 6 +- .../entries/runtime-snapshot-v1.json | 6 +- .../scenario-satisfiability-evidence-v1.json | 6 +- .../entries/sdl-authoring-input-v1.json | 6 +- .../artifact-requirement-v1.json | 1141 +++++++++++++++++ .../backend-manifest/backend-manifest-v2.json | 127 ++ .../schemas/profiles/backend-profile-v1.json | 1 + .../scenario-satisfiability-evidence-v1.json | 534 ++++++++ .../instantiated-scenario-snapshot-v1.json | 597 +++++++++ .../schemas/sdl/instantiated-scenario-v1.json | 597 +++++++++ .../schemas/sdl/sdl-authoring-input-v1.json | 544 ++++++++ .../snapshots/runtime-snapshot-v1.json | 276 ++++ docs/decisions/adrs/README.md | 2 + ...table-artifact-requirement-satisfaction.md | 143 +++ docs/decisions/adrs/adr-index.yaml | 3 + ...fact-requirement-satisfaction-preflight.md | 376 ++++++ .../execution-snapshot-v1.2.json | 4 +- .../python/packages/raes/_source.py | 352 ++++- .../packages/raes/artifact_requirements.py | 25 + .../raes_backend_protocols/manifest.py | 2 + .../packages/raes_contracts/apparatus.py | 16 + .../raes_contracts/artifact_requirements.py | 352 +++++ .../raes_contracts/contracts/__init__.py | 12 + .../raes_contracts/contracts/_exports.py | 10 + .../raes_contracts/contracts/bundle.py | 3 + .../raes_contracts/contracts/capabilities.py | 13 + .../contracts/realization_plans.py | 5 + .../raes_contracts/manifest_authority.py | 1 + .../packages/raes_contracts/runtime_state.py | 2 + .../packages/raes_contracts/versions.py | 1 + .../compiler/realization_requirements.py | 90 ++ .../raes_processor/models/runtime_model.py | 4 + .../packages/raes_processor/planner/core.py | 9 + .../semantics/artifact_realization.py | 363 ++++++ .../raes_processor/semantics/realization.py | 96 +- .../semantics/realization_concerns.py | 54 + .../packages/raes_runtime/backend_calls.py | 10 + .../raes_runtime/control_plane_api_models.py | 5 + .../raes_runtime/control_plane_store.py | 11 + .../python/packages/raes_runtime/manager.py | 5 + .../test_artifact_requirement_satisfaction.py | 1042 +++++++++++++++ specs/README.md | 5 +- specs/supply-chain/README.md | 7 + .../artifact-requirement-satisfaction.md | 200 +++ tools/generate_contract_schemas.py | 2 + tools/policy/historical_identity_records.json | 6 +- 63 files changed, 7380 insertions(+), 93 deletions(-) create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/constrained-without-domain.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/dangling-materialization-input.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-selector-mismatch.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-with-fallback.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/missing-artifact-requirement.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-candidate.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-constraint.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-materialization.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/ungoverned-mechanism.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/unknown-field.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/constrained-materialization.json create mode 100644 contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/exact-artifact.json create mode 100644 contracts/schema-publication/entries/artifact-requirement-v1.json create mode 100644 contracts/schemas/artifact-requirements/artifact-requirement-v1.json create mode 100644 docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md create mode 100644 docs/decisions/issue-920-artifact-requirement-satisfaction-preflight.md create mode 100644 implementations/python/packages/raes/artifact_requirements.py create mode 100644 implementations/python/packages/raes_contracts/artifact_requirements.py create mode 100644 implementations/python/packages/raes_processor/semantics/artifact_realization.py create mode 100644 implementations/python/packages/raes_processor/semantics/realization_concerns.py create mode 100644 implementations/python/tests/test_artifact_requirement_satisfaction.py create mode 100644 specs/supply-chain/artifact-requirement-satisfaction.md diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/constrained-without-domain.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/constrained-without-domain.json new file mode 100644 index 000000000..2cc6303b3 --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/constrained-without-domain.json @@ -0,0 +1,23 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "linux-image", + "version": "*", + "artifact_requirement": { + "requirement_id": "empty-domain", + "explicitness": "constrained", + "permitted_routes": [ + { + "mechanism": { + "mechanism": "published-candidate", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "import", + "timing": "pack-ingestion" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/dangling-materialization-input.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/dangling-materialization-input.json new file mode 100644 index 000000000..3a1058fed --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/dangling-materialization-input.json @@ -0,0 +1,38 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "composed-image", + "version": "1", + "artifact_requirement": { + "requirement_id": "composed-image", + "explicitness": "constrained", + "materialization_specifications": [ + { + "specification_id": "cloud-image", + "profile": { + "mechanism": "materialization-specification", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "locked_input_ids": [ + "missing-rootfs" + ] + } + ], + "permitted_routes": [ + { + "mechanism": { + "mechanism": "materialization-specification", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "none", + "timing": "backend-preparation" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-selector-mismatch.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-selector-mismatch.json new file mode 100644 index 000000000..d010212e9 --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-selector-mismatch.json @@ -0,0 +1,32 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "different-image", + "version": "24.04.1", + "artifact_requirement": { + "requirement_id": "web-image", + "explicitness": "exact", + "exact_artifact": { + "artifact_id": "ubuntu-server", + "version": "24.04.1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "media_type": "application/vnd.oci.image.manifest.v1+json" + }, + "permitted_routes": [ + { + "mechanism": { + "mechanism": "exact-artifact", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "pull", + "timing": "realization" + } + ], + "trust_policy_refs": [ + "reusable-asset-trust-policy-v1#reusable_scenario" + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-with-fallback.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-with-fallback.json new file mode 100644 index 000000000..f66199ade --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/exact-with-fallback.json @@ -0,0 +1,40 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "ubuntu-server", + "version": "24.04.1", + "artifact_requirement": { + "requirement_id": "web-image", + "explicitness": "exact", + "exact_artifact": { + "artifact_id": "ubuntu-server", + "version": "24.04.1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "media_type": "application/vnd.oci.image.manifest.v1+json" + }, + "candidates": [ + { + "candidate_id": "fallback", + "artifact": { + "artifact_id": "ubuntu-server", + "version": "24.04.2", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "media_type": "application/vnd.oci.image.manifest.v1+json" + } + } + ], + "permitted_routes": [ + { + "mechanism": { + "mechanism": "exact-artifact", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "pull", + "timing": "realization" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/missing-artifact-requirement.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/missing-artifact-requirement.json new file mode 100644 index 000000000..b1b40b7b5 --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/missing-artifact-requirement.json @@ -0,0 +1,7 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "ubuntu-server", + "version": "24.04.1" + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-candidate.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-candidate.json new file mode 100644 index 000000000..235760337 --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-candidate.json @@ -0,0 +1,34 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "backend-selected", + "version": "*", + "artifact_requirement": { + "requirement_id": "open-candidate", + "explicitness": "open", + "candidates": [ + { + "candidate_id": "candidate-a", + "artifact": { + "artifact_id": "ubuntu-server", + "version": "24.04.1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "media_type": "application/vnd.oci.image.manifest.v1+json" + } + } + ], + "permitted_routes": [ + { + "mechanism": { + "mechanism": "dynamic-composition", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "none", + "timing": "realization" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-constraint.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-constraint.json new file mode 100644 index 000000000..3890d81bf --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-constraint.json @@ -0,0 +1,32 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "backend-selected", + "version": "*", + "artifact_requirement": { + "requirement_id": "open-constraint", + "explicitness": "open", + "constraints": [ + { + "constraint_id": "platform", + "kind": "artifact-class", + "allowed_values": [ + "linux-vm-image" + ] + } + ], + "permitted_routes": [ + { + "mechanism": { + "mechanism": "dynamic-composition", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "none", + "timing": "realization" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-materialization.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-materialization.json new file mode 100644 index 000000000..cae9eed2a --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/open-with-materialization.json @@ -0,0 +1,35 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "backend-selected", + "version": "*", + "artifact_requirement": { + "requirement_id": "open-materialization", + "explicitness": "open", + "materialization_specifications": [ + { + "specification_id": "cloud-image", + "profile": { + "mechanism": "materialization-specification", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + ], + "permitted_routes": [ + { + "mechanism": { + "mechanism": "dynamic-composition", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "none", + "timing": "realization" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/ungoverned-mechanism.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/ungoverned-mechanism.json new file mode 100644 index 000000000..d0c2c00f5 --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/ungoverned-mechanism.json @@ -0,0 +1,23 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "backend-selected", + "version": "*", + "artifact_requirement": { + "requirement_id": "bad-extension", + "explicitness": "open", + "permitted_routes": [ + { + "mechanism": { + "mechanism": "arbitrary mechanism", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "none", + "timing": "realization" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/unknown-field.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/unknown-field.json new file mode 100644 index 000000000..ac777bfd5 --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/invalid/unknown-field.json @@ -0,0 +1,24 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "backend-selected", + "version": "*", + "artifact_requirement": { + "requirement_id": "unknown-field", + "explicitness": "open", + "provider_region": "eu-central-1", + "permitted_routes": [ + { + "mechanism": { + "mechanism": "backend-owned-artifact", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "local-lookup", + "timing": "backend-preparation" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/constrained-materialization.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/constrained-materialization.json new file mode 100644 index 000000000..89c834f9e --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/constrained-materialization.json @@ -0,0 +1,51 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "portable-cloud-image", + "version": "*", + "artifact_requirement": { + "requirement_id": "materialized-image", + "explicitness": "constrained", + "locked_inputs": [ + { + "input_id": "rootfs", + "artifact": { + "artifact_id": "linux-rootfs", + "version": "1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "media_type": "application/vnd.oci.image.layer.v1.tar+gzip" + }, + "associated_artifact_manifest_ref": "associated-artifact-manifest-v1:rootfs", + "trust_policy_ref": "reusable-asset-trust-policy-v1#associated_artifact_set" + } + ], + "materialization_specifications": [ + { + "specification_id": "cloud-image", + "profile": { + "mechanism": "materialization-specification", + "profile": "raes-cloud-image", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "locked_input_ids": [ + "rootfs" + ] + } + ], + "permitted_routes": [ + { + "mechanism": { + "mechanism": "materialization-specification", + "profile": "raes-cloud-image", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "none", + "timing": "backend-preparation" + } + ] + } + } +} diff --git a/contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/exact-artifact.json b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/exact-artifact.json new file mode 100644 index 000000000..0e0c0ed8e --- /dev/null +++ b/contracts/fixtures/artifact-requirements/artifact-requirement-v1/valid/exact-artifact.json @@ -0,0 +1,32 @@ +{ + "schema_version": "artifact-requirement/v1", + "source": { + "name": "ubuntu-server", + "version": "24.04.1", + "artifact_requirement": { + "requirement_id": "web-image", + "explicitness": "exact", + "exact_artifact": { + "artifact_id": "ubuntu-server", + "version": "24.04.1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "media_type": "application/vnd.oci.image.manifest.v1+json" + }, + "permitted_routes": [ + { + "mechanism": { + "mechanism": "exact-artifact", + "profile": "raes-artifact-satisfaction", + "version": "1", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "acquisition": "pull", + "timing": "realization" + } + ], + "trust_policy_refs": [ + "reusable-asset-trust-policy-v1#reusable_scenario" + ] + } + } +} 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 f08a2df5e..db253f1dc 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -354,6 +354,7 @@ }, "realization_support": [ { + "artifact_mechanisms": [], "constraints": {}, "disclosure_kinds": [ "backend-manifest-v2", @@ -378,6 +379,7 @@ "schema_version": "backend-manifest/v2", "supported_contract_versions": [ "backend-manifest-v2", + "artifact-requirement-v1", "provisioning-plan-v1", "orchestration-plan-v1", "evaluation-plan-v1", diff --git a/contracts/schema-publication/entries/artifact-requirement-v1.json b/contracts/schema-publication/entries/artifact-requirement-v1.json new file mode 100644 index 000000000..5edd4e8cb --- /dev/null +++ b/contracts/schema-publication/entries/artifact-requirement-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "artifact-requirement-v1", + "schema_path": "contracts/schemas/artifact-requirements/artifact-requirement-v1.json", + "stability": "draft", + "content_hash": "2f287a0b78b61cb5045c9481a6f76e6b7d279caf084110d56f709c8e5fee7e23", + "last_change": { + "summary": "Published portable exact, constrained, and open Source artifact requirements with governed satisfaction routes for issue #920.", + "content_hash": "2f287a0b78b61cb5045c9481a6f76e6b7d279caf084110d56f709c8e5fee7e23" + } +} diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index 33455c47d..cef1faf24 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": "4d842923928a9e80e92fa31559fd557a846c4854ac41c30e4d88fa53288c2b22", + "content_hash": "95e0b07360d92eec0b930ca73762f3fa7646d38ab32c5f9d35e9acd38fd61cca", "last_change": { - "summary": "Combined current autonomous activity and policy capabilities with exact action-to-target bindings, lifecycle control, and bounded concurrent participant-service declarations for issue #898.", - "content_hash": "4d842923928a9e80e92fa31559fd557a846c4854ac41c30e4d88fa53288c2b22" + "summary": "Added mechanism-indexed portable artifact satisfaction capabilities without Cartesian acquisition and timing overclaims for issue #920.", + "content_hash": "95e0b07360d92eec0b930ca73762f3fa7646d38ab32c5f9d35e9acd38fd61cca" } } diff --git a/contracts/schema-publication/entries/backend-profile-v1.json b/contracts/schema-publication/entries/backend-profile-v1.json index 1cd4e841b..9427fcf40 100644 --- a/contracts/schema-publication/entries/backend-profile-v1.json +++ b/contracts/schema-publication/entries/backend-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "736ef55570ccbfa97b966cfdf121eea0b75e42d8a1e94e0897371aa6a6502a47", + "content_hash": "feeec868e943507ba5dbcf7e079a54fd24e14eccc6aedf2f098de64c15b6629c", "last_change": { - "summary": "Combined participant control and crossing occurrence contracts with execution binding, lifecycle control, and service-state contracts for issue #898.", - "content_hash": "736ef55570ccbfa97b966cfdf121eea0b75e42d8a1e94e0897371aa6a6502a47" + "summary": "Advertised the portable artifact requirement contract in backend contract profiles for issue #920.", + "content_hash": "feeec868e943507ba5dbcf7e079a54fd24e14eccc6aedf2f098de64c15b6629c" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json index 1b39345b9..89d272a18 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": "812a22bf742131ea7c255c1305bafd8b695a2ad0ef50535361bd0dbde58c7e68", + "content_hash": "37ca088682e5a7a8c321df6949b6df2a3439f6a96ad5107af2ec027529e09cd6", "last_change": { - "summary": "Published the explicit autonomous activity v2 policy union in immutable instantiated scenario snapshots for issue #897.", - "content_hash": "812a22bf742131ea7c255c1305bafd8b695a2ad0ef50535361bd0dbde58c7e68" + "summary": "Published portable Source artifact requirements in immutable instantiated scenario snapshots for issue #920.", + "content_hash": "37ca088682e5a7a8c321df6949b6df2a3439f6a96ad5107af2ec027529e09cd6" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-v1.json b/contracts/schema-publication/entries/instantiated-scenario-v1.json index 8fb00be25..2039d74b6 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": "7c228bd1b4b1004c5f71c3e8b9b17dd63fdb146ac84fdc5ad5437d1c63075eda", + "content_hash": "288912cc33394ee5741129af749181e760f4d640cebb92439de9d24dba7cd7b1", "last_change": { - "summary": "Published the explicit autonomous activity v2 policy union in instantiated scenarios for issue #897.", - "content_hash": "7c228bd1b4b1004c5f71c3e8b9b17dd63fdb146ac84fdc5ad5437d1c63075eda" + "summary": "Published portable Source artifact requirements in instantiated scenarios for issue #920.", + "content_hash": "288912cc33394ee5741129af749181e760f4d640cebb92439de9d24dba7cd7b1" } } diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index acd7ee1c4..3116365ee 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": "eb09a73c33e562958bfb5dc606aa4ac52f8ffa9025cd6a4a0db206ffc3000f50", + "content_hash": "00ac4ed35acfbf30c318d982544436598f94800cf1f2ad8cca92f904d9ef5f4a", "last_change": { - "summary": "Combined autonomous activity v2 continuation and occurrence provenance with generation-fenced execution service health, readiness, capacity, shared-time provenance, and lifecycle evidence for issue #898.", - "content_hash": "eb09a73c33e562958bfb5dc606aa4ac52f8ffa9025cd6a4a0db206ffc3000f50" + "summary": "Added typed artifact satisfaction evidence to existing realization provenance entries for issue #920.", + "content_hash": "00ac4ed35acfbf30c318d982544436598f94800cf1f2ad8cca92f904d9ef5f4a" } } diff --git a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json index 6dc3a7166..d3b5a50f4 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": "b67869d52a1c78da43d56d01465e85f09023e348ffd185b71e276b20f3798765", + "content_hash": "725d837a9f9f131192f138957c2970b3751aa196e75aeb5635828c1aa7996431", "last_change": { - "summary": "Carried the explicit autonomous activity v2 policy union through satisfiability evidence scenario definitions for issue #897.", - "content_hash": "b67869d52a1c78da43d56d01465e85f09023e348ffd185b71e276b20f3798765" + "summary": "Carried portable Source artifact requirements through scenario satisfiability evidence definitions for issue #920.", + "content_hash": "725d837a9f9f131192f138957c2970b3751aa196e75aeb5635828c1aa7996431" } } diff --git a/contracts/schema-publication/entries/sdl-authoring-input-v1.json b/contracts/schema-publication/entries/sdl-authoring-input-v1.json index 2a1865c5c..cb200ff87 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": "f1bc1cfb91a66b739f94ee34ff5ee13070a9a062f4071a2693e8639e09a5ba92", + "content_hash": "4b4b52beb243f70bc6c7e1728c66fb3198572eb040cfdb9a078ae584bb596d40", "last_change": { - "summary": "Published the explicit autonomous activity v2 authoring profile with governed windows, timing, weighted candidates, retries, cooldowns, and bursts for issue #897.", - "content_hash": "f1bc1cfb91a66b739f94ee34ff5ee13070a9a062f4071a2693e8639e09a5ba92" + "summary": "Published portable exact, constrained, and open Source artifact authoring for issue #920.", + "content_hash": "4b4b52beb243f70bc6c7e1728c66fb3198572eb040cfdb9a078ae584bb596d40" } } diff --git a/contracts/schemas/artifact-requirements/artifact-requirement-v1.json b/contracts/schemas/artifact-requirements/artifact-requirement-v1.json new file mode 100644 index 000000000..d90d89530 --- /dev/null +++ b/contracts/schemas/artifact-requirements/artifact-requirement-v1.json @@ -0,0 +1,1141 @@ +{ + "$defs": { + "ArtifactCandidate": { + "additionalProperties": false, + "description": "One immutable candidate explicitly admitted by the author.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "candidate_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Candidate Id", + "type": "string" + } + }, + "required": [ + "candidate_id", + "artifact" + ], + "title": "ArtifactCandidate", + "type": "object" + }, + "ArtifactConstraint": { + "additionalProperties": false, + "description": "One typed, named bound on a constrained artifact selection.", + "properties": { + "allowed_values": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Allowed Values", + "type": "array" + }, + "constraint_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Constraint Id", + "type": "string" + }, + "kind": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "constraint_id", + "kind", + "allowed_values" + ], + "title": "ArtifactConstraint", + "type": "object" + }, + "ArtifactIdentity": { + "additionalProperties": false, + "description": "Immutable provider-neutral identity for one artifact payload.", + "properties": { + "artifact_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Artifact Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "media_type": { + "maxLength": 256, + "minLength": 1, + "title": "Media Type", + "type": "string" + }, + "version": { + "maxLength": 256, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "artifact_id", + "version", + "digest", + "media_type" + ], + "title": "ArtifactIdentity", + "type": "object" + }, + "ArtifactLockedInput": { + "additionalProperties": false, + "description": "One immutable materialization input joined to existing trust contracts.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "associated_artifact_manifest_ref": { + "maxLength": 1024, + "minLength": 1, + "title": "Associated Artifact Manifest Ref", + "type": "string" + }, + "input_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Input Id", + "type": "string" + }, + "trust_policy_ref": { + "maxLength": 1024, + "minLength": 1, + "title": "Trust Policy Ref", + "type": "string" + } + }, + "required": [ + "input_id", + "artifact", + "associated_artifact_manifest_ref", + "trust_policy_ref" + ], + "title": "ArtifactLockedInput", + "type": "object" + }, + "ArtifactMaterializationSpecification": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "profile": { + "allOf": [ + { + "properties": { + "mechanism": { + "anyOf": [ + { + "const": "materialization-specification" + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ] + } + }, + "required": [ + "mechanism" + ] + } + ] + } + } + } + ], + "description": "Reference to a closed executable materialization profile.\n\nThis is a digest-bound specification reference, not shell text, a\nDockerfile, an environment map, or a reinterpretation of ``Source.build``.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "locked_input_ids": { + "items": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "type": "string" + }, + "title": "Locked Input Ids", + "type": "array", + "uniqueItems": true + }, + "profile": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "specification_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Specification Id", + "type": "string" + } + }, + "required": [ + "specification_id", + "profile", + "digest" + ], + "title": "ArtifactMaterializationSpecification", + "type": "object" + }, + "ArtifactMechanismProfile": { + "additionalProperties": false, + "description": "Versioned portable mechanism profile.\n\nPortable mechanisms use the governed base names above or a namespaced\n``x-:`` extension. The profile digest binds the exact\nmechanism contract without making the mechanism set a closed union.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "mechanism": { + "anyOf": [ + { + "enum": [ + "backend-owned-artifact", + "dynamic-composition", + "exact-artifact", + "materialization-specification", + "published-candidate" + ] + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ], + "maxLength": 128, + "minLength": 1, + "title": "Mechanism", + "type": "string" + }, + "profile": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Profile", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mechanism", + "profile", + "version", + "digest" + ], + "title": "ArtifactMechanismProfile", + "type": "object" + }, + "ArtifactRequirement": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "explicitness": { + "const": "exact" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "not": { + "type": "null" + } + }, + "locked_inputs": { + "maxItems": 0 + }, + "materialization_specifications": { + "maxItems": 0 + }, + "permitted_routes": { + "items": { + "properties": { + "mechanism": { + "properties": { + "mechanism": { + "const": "exact-artifact" + } + } + } + } + } + } + }, + "required": [ + "exact_artifact" + ] + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "constrained" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "constraints": { + "minItems": 1 + } + }, + "required": [ + "constraints" + ] + }, + { + "properties": { + "candidates": { + "minItems": 1 + } + }, + "required": [ + "candidates" + ] + }, + { + "properties": { + "locked_inputs": { + "minItems": 1 + } + }, + "required": [ + "locked_inputs" + ] + }, + { + "properties": { + "materialization_specifications": { + "minItems": 1 + } + }, + "required": [ + "materialization_specifications" + ] + } + ], + "properties": { + "exact_artifact": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "open" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "type": "null" + }, + "materialization_specifications": { + "maxItems": 0 + } + } + } + } + ], + "description": "Author-owned artifact requirement attached to a ``Source`` selector.", + "properties": { + "associated_artifact_manifest_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "title": "Associated Artifact Manifest Refs", + "type": "array", + "uniqueItems": true + }, + "candidates": { + "items": { + "$ref": "#/$defs/ArtifactCandidate" + }, + "title": "Candidates", + "type": "array", + "uniqueItems": true + }, + "constraints": { + "items": { + "$ref": "#/$defs/ArtifactConstraint" + }, + "title": "Constraints", + "type": "array", + "uniqueItems": true + }, + "exact_artifact": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactIdentity" + }, + { + "type": "null" + } + ], + "default": null + }, + "explicitness": { + "$ref": "#/$defs/ExplicitnessClass" + }, + "locked_inputs": { + "items": { + "$ref": "#/$defs/ArtifactLockedInput" + }, + "title": "Locked Inputs", + "type": "array", + "uniqueItems": true + }, + "materialization_specifications": { + "items": { + "$ref": "#/$defs/ArtifactMaterializationSpecification" + }, + "title": "Materialization Specifications", + "type": "array", + "uniqueItems": true + }, + "permitted_routes": { + "items": { + "$ref": "#/$defs/ArtifactSatisfactionRoute" + }, + "minItems": 1, + "title": "Permitted Routes", + "type": "array", + "uniqueItems": true + }, + "requirement_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Requirement Id", + "type": "string" + }, + "trust_policy_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "title": "Trust Policy Refs", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "requirement_id", + "explicitness", + "permitted_routes" + ], + "title": "ArtifactRequirement", + "type": "object" + }, + "ArtifactRequirementSource": { + "additionalProperties": false, + "description": "Contract-specific source that cannot omit portable artifact demand.", + "properties": { + "artifact_requirement": { + "$ref": "#/$defs/ArtifactRequirement" + }, + "build": { + "anyOf": [ + { + "$ref": "#/$defs/ContainerImageBuildProvenance" + }, + { + "type": "null" + } + ], + "default": null + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "default": "*", + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "artifact_requirement" + ], + "title": "ArtifactRequirementSource", + "type": "object" + }, + "ArtifactSatisfactionRoute": { + "additionalProperties": false, + "description": "One permitted mechanism/acquisition/timing combination.", + "properties": { + "acquisition": { + "enum": [ + "pull", + "copy", + "import", + "local-lookup", + "none" + ], + "title": "Acquisition", + "type": "string" + }, + "mechanism": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "timing": { + "enum": [ + "publication", + "pack-ingestion", + "backend-preparation", + "realization" + ], + "title": "Timing", + "type": "string" + } + }, + "required": [ + "mechanism", + "acquisition", + "timing" + ], + "title": "ArtifactSatisfactionRoute", + "type": "object" + }, + "ContainerImageBuildProvenance": { + "additionalProperties": false, + "description": "Observed build/provenance facts for a custom container image artifact.", + "properties": { + "attestation": { + "anyOf": [ + { + "$ref": "#/$defs/ImageAttestation" + }, + { + "type": "null" + } + ], + "default": null + }, + "base_image": { + "default": "", + "title": "Base Image", + "type": "string" + }, + "base_image_digest": { + "default": "", + "title": "Base Image Digest", + "type": "string" + }, + "build_args": { + "items": { + "$ref": "#/$defs/ImageBuildArg" + }, + "title": "Build Args", + "type": "array" + }, + "config": { + "anyOf": [ + { + "$ref": "#/$defs/ImageConfig" + }, + { + "type": "null" + } + ], + "default": null + }, + "copied_sources": { + "items": { + "$ref": "#/$defs/ImageCopiedSource" + }, + "title": "Copied Sources", + "type": "array" + }, + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "dockerfile_path": { + "default": "", + "title": "Dockerfile Path", + "type": "string" + }, + "instructions": { + "items": { + "$ref": "#/$defs/DockerfileInstruction" + }, + "title": "Instructions", + "type": "array" + }, + "layers": { + "items": { + "$ref": "#/$defs/ImageLayer" + }, + "title": "Layers", + "type": "array" + }, + "source_inputs": { + "items": { + "$ref": "#/$defs/ImageSourceInput" + }, + "title": "Source Inputs", + "type": "array" + } + }, + "title": "ContainerImageBuildProvenance", + "type": "object" + }, + "DockerfileInstruction": { + "additionalProperties": false, + "description": "A structured record of one container build-recipe instruction.\n\nThe instruction is kept as a typed kind plus tokenized ``arguments``\nrather than raw recipe text: raw Dockerfile/shell syntax can contain\n``${...}`` strings that collide with RAES variable substitution.", + "properties": { + "arguments": { + "items": { + "type": "string" + }, + "title": "Arguments", + "type": "array" + }, + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "instruction": { + "anyOf": [ + { + "$ref": "#/$defs/DockerfileInstructionKind" + }, + { + "type": "string" + } + ], + "title": "Instruction" + } + }, + "required": [ + "instruction" + ], + "title": "DockerfileInstruction", + "type": "object" + }, + "DockerfileInstructionKind": { + "description": "Observed kind of a structured container build-recipe instruction.", + "enum": [ + "from", + "arg", + "env", + "run", + "copy", + "add", + "workdir", + "entrypoint", + "cmd", + "healthcheck", + "label", + "expose", + "user", + "volume", + "shell", + "stopsignal", + "onbuild", + "maintainer", + "other" + ], + "title": "DockerfileInstructionKind", + "type": "string" + }, + "ExplicitnessClass": { + "description": "SEM-218 author-intent class for a declaration.", + "enum": [ + "exact", + "constrained", + "open" + ], + "title": "ExplicitnessClass", + "type": "string" + }, + "ImageAttestation": { + "additionalProperties": false, + "description": "Observed build-attestation availability and verification result.\n\nAttestation *availability* (``status``) and *verification result*\n(``verification``) are deliberately separate facts: a mutable local image\ntag with no registry-visible OCI/in-toto/SLSA attestation is not the same\nstate as a failed verification (ADR-023 \u00a75).", + "properties": { + "attestation_type": { + "anyOf": [ + { + "$ref": "#/$defs/ImageAttestationType" + }, + { + "type": "string" + } + ], + "default": "unknown", + "title": "Attestation Type" + }, + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "evidence_reference": { + "default": "", + "title": "Evidence Reference", + "type": "string" + }, + "predicate_type": { + "default": "", + "title": "Predicate Type", + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/ImageAttestationStatus" + }, + { + "type": "string" + } + ], + "default": "unknown", + "title": "Status" + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/ImageVerificationStatus" + }, + { + "type": "string" + } + ], + "default": "unknown", + "title": "Verification" + } + }, + "title": "ImageAttestation", + "type": "object" + }, + "ImageAttestationStatus": { + "description": "Availability of a registry-visible build attestation for an image.", + "enum": [ + "present", + "absent", + "unknown" + ], + "title": "ImageAttestationStatus", + "type": "string" + }, + "ImageAttestationType": { + "description": "Format of an observed image build attestation.", + "enum": [ + "oci", + "in_toto", + "slsa", + "other", + "none", + "unknown" + ], + "title": "ImageAttestationType", + "type": "string" + }, + "ImageBuildArg": { + "additionalProperties": false, + "description": "An observed build argument with value-sensitivity classification.", + "properties": { + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "default": "", + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeEnvironmentValueClassification" + }, + { + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + } + }, + "required": [ + "name" + ], + "title": "ImageBuildArg", + "type": "object" + }, + "ImageConfig": { + "additionalProperties": false, + "description": "Observed image-default configuration baked into the image artifact.\n\nThese are image defaults, distinct from runtime-effective container facts\nrecorded under ``Node.runtime.container``.", + "properties": { + "command": { + "items": { + "type": "string" + }, + "title": "Command", + "type": "array" + }, + "default_environment": { + "items": { + "$ref": "#/$defs/ImageEnvironmentDefault" + }, + "title": "Default Environment", + "type": "array" + }, + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "entrypoint": { + "items": { + "type": "string" + }, + "title": "Entrypoint", + "type": "array" + }, + "exposed_ports": { + "items": { + "type": "string" + }, + "title": "Exposed Ports", + "type": "array" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "title": "Labels", + "type": "object" + }, + "working_directory": { + "default": "", + "title": "Working Directory", + "type": "string" + } + }, + "title": "ImageConfig", + "type": "object" + }, + "ImageCopiedSource": { + "additionalProperties": false, + "description": "A source path copied into the image and its in-image destination.", + "properties": { + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "destination_path": { + "title": "Destination Path", + "type": "string" + }, + "from_stage": { + "default": "", + "title": "From Stage", + "type": "string" + }, + "source_path": { + "title": "Source Path", + "type": "string" + } + }, + "required": [ + "source_path", + "destination_path" + ], + "title": "ImageCopiedSource", + "type": "object" + }, + "ImageEnvironmentDefault": { + "additionalProperties": false, + "description": "An image-default environment variable with sensitivity classification.", + "properties": { + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "default": "", + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeEnvironmentValueClassification" + }, + { + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + } + }, + "required": [ + "name" + ], + "title": "ImageEnvironmentDefault", + "type": "object" + }, + "ImageLayer": { + "additionalProperties": false, + "description": "An observed layer in the image layer chain.\n\nEmpty/metadata-only layers (e.g. ``ENV`` instructions) legitimately carry\nno ``digest``; the layer is still recorded for build-history correlation.", + "properties": { + "created_by": { + "default": "", + "title": "Created By", + "type": "string" + }, + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "digest": { + "default": "", + "title": "Digest", + "type": "string" + }, + "empty": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Empty" + }, + "size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Size" + } + }, + "title": "ImageLayer", + "type": "object" + }, + "ImageSourceInput": { + "additionalProperties": false, + "description": "A source-package input and its source-to-runtime destination mapping.\n\n``source_path`` uses the same source-path dialect as\n``RuntimeFilesystemEntry.source_path`` so the build-time input and the\nrealized runtime entry can be correlated.", + "properties": { + "checksum": { + "default": "", + "title": "Checksum", + "type": "string" + }, + "checksum_algorithm": { + "default": "", + "title": "Checksum Algorithm", + "type": "string" + }, + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "destination_path": { + "default": "", + "title": "Destination Path", + "type": "string" + }, + "identifier": { + "title": "Identifier", + "type": "string" + }, + "source_path": { + "default": "", + "title": "Source Path", + "type": "string" + } + }, + "required": [ + "identifier" + ], + "title": "ImageSourceInput", + "type": "object" + }, + "ImageVerificationStatus": { + "description": "Result of verifying an image's build attestation.", + "enum": [ + "verified", + "failed", + "unverified", + "not_applicable", + "unknown" + ], + "title": "ImageVerificationStatus", + "type": "string" + }, + "RuntimeEnvironmentValueClassification": { + "description": "Sensitivity classification for a required runtime environment value.", + "enum": [ + "plain", + "redacted", + "secret_fixture", + "operator_secret", + "unknown", + "other" + ], + "title": "RuntimeEnvironmentValueClassification", + "type": "string" + } + }, + "$id": "https://raes.dev/schemas/artifact-requirement-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Published source-artifact requirement contract.", + "properties": { + "schema_version": { + "const": "artifact-requirement/v1", + "default": "artifact-requirement/v1", + "title": "Schema Version", + "type": "string" + }, + "source": { + "$ref": "#/$defs/ArtifactRequirementSource" + } + }, + "required": [ + "source" + ], + "title": "ArtifactRequirementContractModel", + "type": "object", + "x-raes-invariants": [ + { + "description": "An exact requirement's immutable artifact id must equal the enclosing Source selector name.", + "id": "exact-source-artifact-id-match", + "inputs": [ + { + "contract_id": "artifact-requirement-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.artifact_requirements.validate_artifact_requirement_invariants" + }, + { + "description": "An exact requirement's immutable artifact version must equal the enclosing Source selector version.", + "id": "exact-source-version-match", + "inputs": [ + { + "contract_id": "artifact-requirement-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.artifact_requirements.validate_artifact_requirement_invariants" + }, + { + "description": "Every materialization locked_input_id must resolve to a locked input declared by the same artifact requirement.", + "id": "materialization-locked-input-join", + "inputs": [ + { + "contract_id": "artifact-requirement-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.artifact_requirements.validate_artifact_requirement_invariants" + } + ], + "x-raes-semantic-profile": { + "contract_id": "artifact-requirement-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://raes.dev/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 caa38bd18..3b813d9f0 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -21,6 +21,125 @@ "title": "ApparatusIdentityModel", "type": "object" }, + "ArtifactAcquisitionTimingModel": { + "additionalProperties": false, + "description": "One exact acquisition/timing combination supported by a backend.", + "properties": { + "acquisition": { + "enum": [ + "pull", + "copy", + "import", + "local-lookup", + "none" + ], + "title": "Acquisition", + "type": "string" + }, + "timing": { + "enum": [ + "publication", + "pack-ingestion", + "backend-preparation", + "realization" + ], + "title": "Timing", + "type": "string" + } + }, + "required": [ + "acquisition", + "timing" + ], + "title": "ArtifactAcquisitionTimingModel", + "type": "object" + }, + "ArtifactMechanismCapability": { + "additionalProperties": false, + "description": "Mechanism-indexed backend support without Cartesian-product overclaim.", + "properties": { + "mechanism": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "supported_requirement_kinds": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Requirement Kinds", + "type": "array", + "uniqueItems": true + }, + "supported_routes": { + "items": { + "$ref": "#/$defs/ArtifactAcquisitionTimingModel" + }, + "minItems": 1, + "title": "Supported Routes", + "type": "array" + } + }, + "required": [ + "mechanism", + "supported_requirement_kinds", + "supported_routes" + ], + "title": "ArtifactMechanismCapability", + "type": "object" + }, + "ArtifactMechanismProfile": { + "additionalProperties": false, + "description": "Versioned portable mechanism profile.\n\nPortable mechanisms use the governed base names above or a namespaced\n``x-:`` extension. The profile digest binds the exact\nmechanism contract without making the mechanism set a closed union.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "mechanism": { + "anyOf": [ + { + "enum": [ + "backend-owned-artifact", + "dynamic-composition", + "exact-artifact", + "materialization-specification", + "published-candidate" + ] + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ], + "maxLength": 128, + "minLength": 1, + "title": "Mechanism", + "type": "string" + }, + "profile": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Profile", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mechanism", + "profile", + "version", + "digest" + ], + "title": "ArtifactMechanismProfile", + "type": "object" + }, "BackendCapabilitiesV2Model": { "additionalProperties": false, "properties": { @@ -1522,6 +1641,13 @@ } ], "properties": { + "artifact_mechanisms": { + "items": { + "$ref": "#/$defs/ArtifactMechanismCapability" + }, + "title": "Artifact Mechanisms", + "type": "array" + }, "constraints": { "additionalProperties": { "type": "string" @@ -1948,6 +2074,7 @@ "items": { "enum": [ "backend-manifest-v2", + "artifact-requirement-v1", "experiment-binding-descriptors-v1", "realization-envelope-v1", "provisioning-plan-v1", diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index b3f263203..f2d86ed2d 100644 --- a/contracts/schemas/profiles/backend-profile-v1.json +++ b/contracts/schemas/profiles/backend-profile-v1.json @@ -13,6 +13,7 @@ "items": { "enum": [ "backend-manifest-v2", + "artifact-requirement-v1", "experiment-binding-descriptors-v1", "realization-envelope-v1", "provisioning-plan-v1", diff --git a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json index fb3988c24..947c28be3 100644 --- a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json +++ b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json @@ -267,6 +267,529 @@ "title": "Agent", "type": "object" }, + "ArtifactCandidate": { + "additionalProperties": false, + "description": "One immutable candidate explicitly admitted by the author.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "candidate_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Candidate Id", + "type": "string" + } + }, + "required": [ + "candidate_id", + "artifact" + ], + "title": "ArtifactCandidate", + "type": "object" + }, + "ArtifactConstraint": { + "additionalProperties": false, + "description": "One typed, named bound on a constrained artifact selection.", + "properties": { + "allowed_values": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Allowed Values", + "type": "array" + }, + "constraint_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Constraint Id", + "type": "string" + }, + "kind": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "constraint_id", + "kind", + "allowed_values" + ], + "title": "ArtifactConstraint", + "type": "object" + }, + "ArtifactIdentity": { + "additionalProperties": false, + "description": "Immutable provider-neutral identity for one artifact payload.", + "properties": { + "artifact_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Artifact Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "media_type": { + "maxLength": 256, + "minLength": 1, + "title": "Media Type", + "type": "string" + }, + "version": { + "maxLength": 256, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "artifact_id", + "version", + "digest", + "media_type" + ], + "title": "ArtifactIdentity", + "type": "object" + }, + "ArtifactLockedInput": { + "additionalProperties": false, + "description": "One immutable materialization input joined to existing trust contracts.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "associated_artifact_manifest_ref": { + "maxLength": 1024, + "minLength": 1, + "title": "Associated Artifact Manifest Ref", + "type": "string" + }, + "input_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Input Id", + "type": "string" + }, + "trust_policy_ref": { + "maxLength": 1024, + "minLength": 1, + "title": "Trust Policy Ref", + "type": "string" + } + }, + "required": [ + "input_id", + "artifact", + "associated_artifact_manifest_ref", + "trust_policy_ref" + ], + "title": "ArtifactLockedInput", + "type": "object" + }, + "ArtifactMaterializationSpecification": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "profile": { + "allOf": [ + { + "properties": { + "mechanism": { + "anyOf": [ + { + "const": "materialization-specification" + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ] + } + }, + "required": [ + "mechanism" + ] + } + ] + } + } + } + ], + "description": "Reference to a closed executable materialization profile.\n\nThis is a digest-bound specification reference, not shell text, a\nDockerfile, an environment map, or a reinterpretation of ``Source.build``.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "locked_input_ids": { + "items": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "type": "string" + }, + "title": "Locked Input Ids", + "type": "array", + "uniqueItems": true + }, + "profile": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "specification_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Specification Id", + "type": "string" + } + }, + "required": [ + "specification_id", + "profile", + "digest" + ], + "title": "ArtifactMaterializationSpecification", + "type": "object" + }, + "ArtifactMechanismProfile": { + "additionalProperties": false, + "description": "Versioned portable mechanism profile.\n\nPortable mechanisms use the governed base names above or a namespaced\n``x-:`` extension. The profile digest binds the exact\nmechanism contract without making the mechanism set a closed union.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "mechanism": { + "anyOf": [ + { + "enum": [ + "backend-owned-artifact", + "dynamic-composition", + "exact-artifact", + "materialization-specification", + "published-candidate" + ] + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ], + "maxLength": 128, + "minLength": 1, + "title": "Mechanism", + "type": "string" + }, + "profile": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Profile", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mechanism", + "profile", + "version", + "digest" + ], + "title": "ArtifactMechanismProfile", + "type": "object" + }, + "ArtifactRequirement": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "explicitness": { + "const": "exact" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "not": { + "type": "null" + } + }, + "locked_inputs": { + "maxItems": 0 + }, + "materialization_specifications": { + "maxItems": 0 + }, + "permitted_routes": { + "items": { + "properties": { + "mechanism": { + "properties": { + "mechanism": { + "const": "exact-artifact" + } + } + } + } + } + } + }, + "required": [ + "exact_artifact" + ] + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "constrained" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "constraints": { + "minItems": 1 + } + }, + "required": [ + "constraints" + ] + }, + { + "properties": { + "candidates": { + "minItems": 1 + } + }, + "required": [ + "candidates" + ] + }, + { + "properties": { + "locked_inputs": { + "minItems": 1 + } + }, + "required": [ + "locked_inputs" + ] + }, + { + "properties": { + "materialization_specifications": { + "minItems": 1 + } + }, + "required": [ + "materialization_specifications" + ] + } + ], + "properties": { + "exact_artifact": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "open" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "type": "null" + }, + "materialization_specifications": { + "maxItems": 0 + } + } + } + } + ], + "description": "Author-owned artifact requirement attached to a ``Source`` selector.", + "properties": { + "associated_artifact_manifest_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "title": "Associated Artifact Manifest Refs", + "type": "array", + "uniqueItems": true + }, + "candidates": { + "items": { + "$ref": "#/$defs/ArtifactCandidate" + }, + "title": "Candidates", + "type": "array", + "uniqueItems": true + }, + "constraints": { + "items": { + "$ref": "#/$defs/ArtifactConstraint" + }, + "title": "Constraints", + "type": "array", + "uniqueItems": true + }, + "exact_artifact": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactIdentity" + }, + { + "type": "null" + } + ], + "default": null + }, + "explicitness": { + "$ref": "#/$defs/ExplicitnessClass" + }, + "locked_inputs": { + "items": { + "$ref": "#/$defs/ArtifactLockedInput" + }, + "title": "Locked Inputs", + "type": "array", + "uniqueItems": true + }, + "materialization_specifications": { + "items": { + "$ref": "#/$defs/ArtifactMaterializationSpecification" + }, + "title": "Materialization Specifications", + "type": "array", + "uniqueItems": true + }, + "permitted_routes": { + "items": { + "$ref": "#/$defs/ArtifactSatisfactionRoute" + }, + "minItems": 1, + "title": "Permitted Routes", + "type": "array", + "uniqueItems": true + }, + "requirement_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Requirement Id", + "type": "string" + }, + "trust_policy_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "title": "Trust Policy Refs", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "requirement_id", + "explicitness", + "permitted_routes" + ], + "title": "ArtifactRequirement", + "type": "object" + }, + "ArtifactSatisfactionRoute": { + "additionalProperties": false, + "description": "One permitted mechanism/acquisition/timing combination.", + "properties": { + "acquisition": { + "enum": [ + "pull", + "copy", + "import", + "local-lookup", + "none" + ], + "title": "Acquisition", + "type": "string" + }, + "mechanism": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "timing": { + "enum": [ + "publication", + "pack-ingestion", + "backend-preparation", + "realization" + ], + "title": "Timing", + "type": "string" + } + }, + "required": [ + "mechanism", + "acquisition", + "timing" + ], + "title": "ArtifactSatisfactionRoute", + "type": "object" + }, "Assertion": { "additionalProperties": false, "description": "A typed use of one proposition at a governed semantic boundary.", @@ -19880,6 +20403,17 @@ "additionalProperties": false, "description": "Provider-neutral artifact reference.\n\nShorthand: ``source: \"package-name\"`` (version defaults to ``\"*\"``).\nLonghand: ``source: {name: \"package-name\", version: \"1.2.3\"}``.", "properties": { + "artifact_requirement": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactRequirement" + }, + { + "type": "null" + } + ], + "default": null + }, "build": { "anyOf": [ { diff --git a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json index f6dc83966..a61cb23c7 100644 --- a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -358,6 +358,592 @@ "title": "Agent", "type": "object" }, + "ArtifactCandidate": { + "additionalProperties": false, + "description": "One immutable candidate explicitly admitted by the author.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "candidate_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Candidate Id", + "type": "string" + } + }, + "required": [ + "candidate_id", + "artifact" + ], + "title": "ArtifactCandidate", + "type": "object" + }, + "ArtifactConstraint": { + "additionalProperties": false, + "description": "One typed, named bound on a constrained artifact selection.", + "properties": { + "allowed_values": { + "items": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Allowed Values", + "type": "array" + }, + "constraint_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Constraint Id", + "type": "string" + }, + "kind": { + "maxLength": 128, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "constraint_id", + "kind", + "allowed_values" + ], + "title": "ArtifactConstraint", + "type": "object" + }, + "ArtifactIdentity": { + "additionalProperties": false, + "description": "Immutable provider-neutral identity for one artifact payload.", + "properties": { + "artifact_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Artifact Id", + "type": "string" + }, + "digest": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "media_type": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Media Type", + "type": "string" + }, + "version": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "artifact_id", + "version", + "digest", + "media_type" + ], + "title": "ArtifactIdentity", + "type": "object" + }, + "ArtifactLockedInput": { + "additionalProperties": false, + "description": "One immutable materialization input joined to existing trust contracts.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "associated_artifact_manifest_ref": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Associated Artifact Manifest Ref", + "type": "string" + }, + "input_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Input Id", + "type": "string" + }, + "trust_policy_ref": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Trust Policy Ref", + "type": "string" + } + }, + "required": [ + "input_id", + "artifact", + "associated_artifact_manifest_ref", + "trust_policy_ref" + ], + "title": "ArtifactLockedInput", + "type": "object" + }, + "ArtifactMaterializationSpecification": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "profile": { + "allOf": [ + { + "properties": { + "mechanism": { + "anyOf": [ + { + "const": "materialization-specification" + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ] + } + }, + "required": [ + "mechanism" + ] + } + ] + } + } + } + ], + "description": "Reference to a closed executable materialization profile.\n\nThis is a digest-bound specification reference, not shell text, a\nDockerfile, an environment map, or a reinterpretation of ``Source.build``.", + "properties": { + "digest": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "locked_input_ids": { + "items": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "type": "string" + }, + "title": "Locked Input Ids", + "type": "array", + "uniqueItems": true + }, + "profile": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "specification_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Specification Id", + "type": "string" + } + }, + "required": [ + "specification_id", + "profile", + "digest" + ], + "title": "ArtifactMaterializationSpecification", + "type": "object" + }, + "ArtifactMechanismProfile": { + "additionalProperties": false, + "description": "Versioned portable mechanism profile.\n\nPortable mechanisms use the governed base names above or a namespaced\n``x-:`` extension. The profile digest binds the exact\nmechanism contract without making the mechanism set a closed union.", + "properties": { + "digest": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "mechanism": { + "anyOf": [ + { + "enum": [ + "backend-owned-artifact", + "dynamic-composition", + "exact-artifact", + "materialization-specification", + "published-candidate" + ] + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ], + "maxLength": 128, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mechanism", + "type": "string" + }, + "profile": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Profile", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mechanism", + "profile", + "version", + "digest" + ], + "title": "ArtifactMechanismProfile", + "type": "object" + }, + "ArtifactRequirement": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "explicitness": { + "const": "exact" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "not": { + "type": "null" + } + }, + "locked_inputs": { + "maxItems": 0 + }, + "materialization_specifications": { + "maxItems": 0 + }, + "permitted_routes": { + "items": { + "properties": { + "mechanism": { + "properties": { + "mechanism": { + "const": "exact-artifact" + } + } + } + } + } + } + }, + "required": [ + "exact_artifact" + ] + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "constrained" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "constraints": { + "minItems": 1 + } + }, + "required": [ + "constraints" + ] + }, + { + "properties": { + "candidates": { + "minItems": 1 + } + }, + "required": [ + "candidates" + ] + }, + { + "properties": { + "locked_inputs": { + "minItems": 1 + } + }, + "required": [ + "locked_inputs" + ] + }, + { + "properties": { + "materialization_specifications": { + "minItems": 1 + } + }, + "required": [ + "materialization_specifications" + ] + } + ], + "properties": { + "exact_artifact": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "open" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "type": "null" + }, + "materialization_specifications": { + "maxItems": 0 + } + } + } + } + ], + "description": "Author-owned artifact requirement attached to a ``Source`` selector.", + "properties": { + "associated_artifact_manifest_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Associated Artifact Manifest Refs", + "type": "array", + "uniqueItems": true + }, + "candidates": { + "items": { + "$ref": "#/$defs/ArtifactCandidate" + }, + "title": "Candidates", + "type": "array", + "uniqueItems": true + }, + "constraints": { + "items": { + "$ref": "#/$defs/ArtifactConstraint" + }, + "title": "Constraints", + "type": "array", + "uniqueItems": true + }, + "exact_artifact": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactIdentity" + }, + { + "type": "null" + } + ], + "default": null + }, + "explicitness": { + "$ref": "#/$defs/ExplicitnessClass" + }, + "locked_inputs": { + "items": { + "$ref": "#/$defs/ArtifactLockedInput" + }, + "title": "Locked Inputs", + "type": "array", + "uniqueItems": true + }, + "materialization_specifications": { + "items": { + "$ref": "#/$defs/ArtifactMaterializationSpecification" + }, + "title": "Materialization Specifications", + "type": "array", + "uniqueItems": true + }, + "permitted_routes": { + "items": { + "$ref": "#/$defs/ArtifactSatisfactionRoute" + }, + "minItems": 1, + "title": "Permitted Routes", + "type": "array", + "uniqueItems": true + }, + "requirement_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Requirement Id", + "type": "string" + }, + "trust_policy_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Trust Policy Refs", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "requirement_id", + "explicitness", + "permitted_routes" + ], + "title": "ArtifactRequirement", + "type": "object" + }, + "ArtifactSatisfactionRoute": { + "additionalProperties": false, + "description": "One permitted mechanism/acquisition/timing combination.", + "properties": { + "acquisition": { + "enum": [ + "pull", + "copy", + "import", + "local-lookup", + "none" + ], + "title": "Acquisition", + "type": "string" + }, + "mechanism": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "timing": { + "enum": [ + "publication", + "pack-ingestion", + "backend-preparation", + "realization" + ], + "title": "Timing", + "type": "string" + } + }, + "required": [ + "mechanism", + "acquisition", + "timing" + ], + "title": "ArtifactSatisfactionRoute", + "type": "object" + }, "Assertion": { "additionalProperties": false, "description": "A typed use of one proposition at a governed semantic boundary.", @@ -24664,6 +25250,17 @@ "additionalProperties": false, "description": "Provider-neutral artifact reference.\n\nShorthand: ``source: \"package-name\"`` (version defaults to ``\"*\"``).\nLonghand: ``source: {name: \"package-name\", version: \"1.2.3\"}``.", "properties": { + "artifact_requirement": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactRequirement" + }, + { + "type": "null" + } + ], + "default": null + }, "build": { "anyOf": [ { diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index b5bd8a815..56f07fefa 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -358,6 +358,592 @@ "title": "Agent", "type": "object" }, + "ArtifactCandidate": { + "additionalProperties": false, + "description": "One immutable candidate explicitly admitted by the author.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "candidate_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Candidate Id", + "type": "string" + } + }, + "required": [ + "candidate_id", + "artifact" + ], + "title": "ArtifactCandidate", + "type": "object" + }, + "ArtifactConstraint": { + "additionalProperties": false, + "description": "One typed, named bound on a constrained artifact selection.", + "properties": { + "allowed_values": { + "items": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Allowed Values", + "type": "array" + }, + "constraint_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Constraint Id", + "type": "string" + }, + "kind": { + "maxLength": 128, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "constraint_id", + "kind", + "allowed_values" + ], + "title": "ArtifactConstraint", + "type": "object" + }, + "ArtifactIdentity": { + "additionalProperties": false, + "description": "Immutable provider-neutral identity for one artifact payload.", + "properties": { + "artifact_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Artifact Id", + "type": "string" + }, + "digest": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "media_type": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Media Type", + "type": "string" + }, + "version": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "artifact_id", + "version", + "digest", + "media_type" + ], + "title": "ArtifactIdentity", + "type": "object" + }, + "ArtifactLockedInput": { + "additionalProperties": false, + "description": "One immutable materialization input joined to existing trust contracts.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "associated_artifact_manifest_ref": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Associated Artifact Manifest Ref", + "type": "string" + }, + "input_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Input Id", + "type": "string" + }, + "trust_policy_ref": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Trust Policy Ref", + "type": "string" + } + }, + "required": [ + "input_id", + "artifact", + "associated_artifact_manifest_ref", + "trust_policy_ref" + ], + "title": "ArtifactLockedInput", + "type": "object" + }, + "ArtifactMaterializationSpecification": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "profile": { + "allOf": [ + { + "properties": { + "mechanism": { + "anyOf": [ + { + "const": "materialization-specification" + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ] + } + }, + "required": [ + "mechanism" + ] + } + ] + } + } + } + ], + "description": "Reference to a closed executable materialization profile.\n\nThis is a digest-bound specification reference, not shell text, a\nDockerfile, an environment map, or a reinterpretation of ``Source.build``.", + "properties": { + "digest": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "locked_input_ids": { + "items": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "type": "string" + }, + "title": "Locked Input Ids", + "type": "array", + "uniqueItems": true + }, + "profile": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "specification_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Specification Id", + "type": "string" + } + }, + "required": [ + "specification_id", + "profile", + "digest" + ], + "title": "ArtifactMaterializationSpecification", + "type": "object" + }, + "ArtifactMechanismProfile": { + "additionalProperties": false, + "description": "Versioned portable mechanism profile.\n\nPortable mechanisms use the governed base names above or a namespaced\n``x-:`` extension. The profile digest binds the exact\nmechanism contract without making the mechanism set a closed union.", + "properties": { + "digest": { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "mechanism": { + "anyOf": [ + { + "enum": [ + "backend-owned-artifact", + "dynamic-composition", + "exact-artifact", + "materialization-specification", + "published-candidate" + ] + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ], + "maxLength": 128, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mechanism", + "type": "string" + }, + "profile": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Profile", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mechanism", + "profile", + "version", + "digest" + ], + "title": "ArtifactMechanismProfile", + "type": "object" + }, + "ArtifactRequirement": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "explicitness": { + "const": "exact" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "not": { + "type": "null" + } + }, + "locked_inputs": { + "maxItems": 0 + }, + "materialization_specifications": { + "maxItems": 0 + }, + "permitted_routes": { + "items": { + "properties": { + "mechanism": { + "properties": { + "mechanism": { + "const": "exact-artifact" + } + } + } + } + } + } + }, + "required": [ + "exact_artifact" + ] + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "constrained" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "constraints": { + "minItems": 1 + } + }, + "required": [ + "constraints" + ] + }, + { + "properties": { + "candidates": { + "minItems": 1 + } + }, + "required": [ + "candidates" + ] + }, + { + "properties": { + "locked_inputs": { + "minItems": 1 + } + }, + "required": [ + "locked_inputs" + ] + }, + { + "properties": { + "materialization_specifications": { + "minItems": 1 + } + }, + "required": [ + "materialization_specifications" + ] + } + ], + "properties": { + "exact_artifact": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "open" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "type": "null" + }, + "materialization_specifications": { + "maxItems": 0 + } + } + } + } + ], + "description": "Author-owned artifact requirement attached to a ``Source`` selector.", + "properties": { + "associated_artifact_manifest_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Associated Artifact Manifest Refs", + "type": "array", + "uniqueItems": true + }, + "candidates": { + "items": { + "$ref": "#/$defs/ArtifactCandidate" + }, + "title": "Candidates", + "type": "array", + "uniqueItems": true + }, + "constraints": { + "items": { + "$ref": "#/$defs/ArtifactConstraint" + }, + "title": "Constraints", + "type": "array", + "uniqueItems": true + }, + "exact_artifact": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactIdentity" + }, + { + "type": "null" + } + ], + "default": null + }, + "explicitness": { + "$ref": "#/$defs/ExplicitnessClass" + }, + "locked_inputs": { + "items": { + "$ref": "#/$defs/ArtifactLockedInput" + }, + "title": "Locked Inputs", + "type": "array", + "uniqueItems": true + }, + "materialization_specifications": { + "items": { + "$ref": "#/$defs/ArtifactMaterializationSpecification" + }, + "title": "Materialization Specifications", + "type": "array", + "uniqueItems": true + }, + "permitted_routes": { + "items": { + "$ref": "#/$defs/ArtifactSatisfactionRoute" + }, + "minItems": 1, + "title": "Permitted Routes", + "type": "array", + "uniqueItems": true + }, + "requirement_id": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Requirement Id", + "type": "string" + }, + "trust_policy_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Trust Policy Refs", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "requirement_id", + "explicitness", + "permitted_routes" + ], + "title": "ArtifactRequirement", + "type": "object" + }, + "ArtifactSatisfactionRoute": { + "additionalProperties": false, + "description": "One permitted mechanism/acquisition/timing combination.", + "properties": { + "acquisition": { + "enum": [ + "pull", + "copy", + "import", + "local-lookup", + "none" + ], + "title": "Acquisition", + "type": "string" + }, + "mechanism": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "timing": { + "enum": [ + "publication", + "pack-ingestion", + "backend-preparation", + "realization" + ], + "title": "Timing", + "type": "string" + } + }, + "required": [ + "mechanism", + "acquisition", + "timing" + ], + "title": "ArtifactSatisfactionRoute", + "type": "object" + }, "Assertion": { "additionalProperties": false, "description": "A typed use of one proposition at a governed semantic boundary.", @@ -24021,6 +24607,17 @@ "additionalProperties": false, "description": "Provider-neutral artifact reference.\n\nShorthand: ``source: \"package-name\"`` (version defaults to ``\"*\"``).\nLonghand: ``source: {name: \"package-name\", version: \"1.2.3\"}``.", "properties": { + "artifact_requirement": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactRequirement" + }, + { + "type": "null" + } + ], + "default": null + }, "build": { "anyOf": [ { diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index 1b4e6ca82..9b13e6e34 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -309,6 +309,529 @@ "title": "AlternativeVariationPoint", "type": "object" }, + "ArtifactCandidate": { + "additionalProperties": false, + "description": "One immutable candidate explicitly admitted by the author.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "candidate_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Candidate Id", + "type": "string" + } + }, + "required": [ + "candidate_id", + "artifact" + ], + "title": "ArtifactCandidate", + "type": "object" + }, + "ArtifactConstraint": { + "additionalProperties": false, + "description": "One typed, named bound on a constrained artifact selection.", + "properties": { + "allowed_values": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Allowed Values", + "type": "array" + }, + "constraint_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Constraint Id", + "type": "string" + }, + "kind": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "constraint_id", + "kind", + "allowed_values" + ], + "title": "ArtifactConstraint", + "type": "object" + }, + "ArtifactIdentity": { + "additionalProperties": false, + "description": "Immutable provider-neutral identity for one artifact payload.", + "properties": { + "artifact_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Artifact Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "media_type": { + "maxLength": 256, + "minLength": 1, + "title": "Media Type", + "type": "string" + }, + "version": { + "maxLength": 256, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "artifact_id", + "version", + "digest", + "media_type" + ], + "title": "ArtifactIdentity", + "type": "object" + }, + "ArtifactLockedInput": { + "additionalProperties": false, + "description": "One immutable materialization input joined to existing trust contracts.", + "properties": { + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "associated_artifact_manifest_ref": { + "maxLength": 1024, + "minLength": 1, + "title": "Associated Artifact Manifest Ref", + "type": "string" + }, + "input_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Input Id", + "type": "string" + }, + "trust_policy_ref": { + "maxLength": 1024, + "minLength": 1, + "title": "Trust Policy Ref", + "type": "string" + } + }, + "required": [ + "input_id", + "artifact", + "associated_artifact_manifest_ref", + "trust_policy_ref" + ], + "title": "ArtifactLockedInput", + "type": "object" + }, + "ArtifactMaterializationSpecification": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "profile": { + "allOf": [ + { + "properties": { + "mechanism": { + "anyOf": [ + { + "const": "materialization-specification" + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ] + } + }, + "required": [ + "mechanism" + ] + } + ] + } + } + } + ], + "description": "Reference to a closed executable materialization profile.\n\nThis is a digest-bound specification reference, not shell text, a\nDockerfile, an environment map, or a reinterpretation of ``Source.build``.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "locked_input_ids": { + "items": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "type": "string" + }, + "title": "Locked Input Ids", + "type": "array", + "uniqueItems": true + }, + "profile": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "specification_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Specification Id", + "type": "string" + } + }, + "required": [ + "specification_id", + "profile", + "digest" + ], + "title": "ArtifactMaterializationSpecification", + "type": "object" + }, + "ArtifactMechanismProfile": { + "additionalProperties": false, + "description": "Versioned portable mechanism profile.\n\nPortable mechanisms use the governed base names above or a namespaced\n``x-:`` extension. The profile digest binds the exact\nmechanism contract without making the mechanism set a closed union.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "mechanism": { + "anyOf": [ + { + "enum": [ + "backend-owned-artifact", + "dynamic-composition", + "exact-artifact", + "materialization-specification", + "published-candidate" + ] + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ], + "maxLength": 128, + "minLength": 1, + "title": "Mechanism", + "type": "string" + }, + "profile": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Profile", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mechanism", + "profile", + "version", + "digest" + ], + "title": "ArtifactMechanismProfile", + "type": "object" + }, + "ArtifactRequirement": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "explicitness": { + "const": "exact" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "not": { + "type": "null" + } + }, + "locked_inputs": { + "maxItems": 0 + }, + "materialization_specifications": { + "maxItems": 0 + }, + "permitted_routes": { + "items": { + "properties": { + "mechanism": { + "properties": { + "mechanism": { + "const": "exact-artifact" + } + } + } + } + } + } + }, + "required": [ + "exact_artifact" + ] + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "constrained" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "constraints": { + "minItems": 1 + } + }, + "required": [ + "constraints" + ] + }, + { + "properties": { + "candidates": { + "minItems": 1 + } + }, + "required": [ + "candidates" + ] + }, + { + "properties": { + "locked_inputs": { + "minItems": 1 + } + }, + "required": [ + "locked_inputs" + ] + }, + { + "properties": { + "materialization_specifications": { + "minItems": 1 + } + }, + "required": [ + "materialization_specifications" + ] + } + ], + "properties": { + "exact_artifact": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "explicitness": { + "const": "open" + } + }, + "required": [ + "explicitness" + ] + }, + "then": { + "properties": { + "candidates": { + "maxItems": 0 + }, + "constraints": { + "maxItems": 0 + }, + "exact_artifact": { + "type": "null" + }, + "materialization_specifications": { + "maxItems": 0 + } + } + } + } + ], + "description": "Author-owned artifact requirement attached to a ``Source`` selector.", + "properties": { + "associated_artifact_manifest_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "title": "Associated Artifact Manifest Refs", + "type": "array", + "uniqueItems": true + }, + "candidates": { + "items": { + "$ref": "#/$defs/ArtifactCandidate" + }, + "title": "Candidates", + "type": "array", + "uniqueItems": true + }, + "constraints": { + "items": { + "$ref": "#/$defs/ArtifactConstraint" + }, + "title": "Constraints", + "type": "array", + "uniqueItems": true + }, + "exact_artifact": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactIdentity" + }, + { + "type": "null" + } + ], + "default": null + }, + "explicitness": { + "$ref": "#/$defs/ExplicitnessClass" + }, + "locked_inputs": { + "items": { + "$ref": "#/$defs/ArtifactLockedInput" + }, + "title": "Locked Inputs", + "type": "array", + "uniqueItems": true + }, + "materialization_specifications": { + "items": { + "$ref": "#/$defs/ArtifactMaterializationSpecification" + }, + "title": "Materialization Specifications", + "type": "array", + "uniqueItems": true + }, + "permitted_routes": { + "items": { + "$ref": "#/$defs/ArtifactSatisfactionRoute" + }, + "minItems": 1, + "title": "Permitted Routes", + "type": "array", + "uniqueItems": true + }, + "requirement_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Requirement Id", + "type": "string" + }, + "trust_policy_refs": { + "items": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "title": "Trust Policy Refs", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "requirement_id", + "explicitness", + "permitted_routes" + ], + "title": "ArtifactRequirement", + "type": "object" + }, + "ArtifactSatisfactionRoute": { + "additionalProperties": false, + "description": "One permitted mechanism/acquisition/timing combination.", + "properties": { + "acquisition": { + "enum": [ + "pull", + "copy", + "import", + "local-lookup", + "none" + ], + "title": "Acquisition", + "type": "string" + }, + "mechanism": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "timing": { + "enum": [ + "publication", + "pack-ingestion", + "backend-preparation", + "realization" + ], + "title": "Timing", + "type": "string" + } + }, + "required": [ + "mechanism", + "acquisition", + "timing" + ], + "title": "ArtifactSatisfactionRoute", + "type": "object" + }, "Assertion": { "additionalProperties": false, "description": "A typed use of one proposition at a governed semantic boundary.", @@ -2952,6 +3475,16 @@ "title": "ExerciseRole", "type": "string" }, + "ExplicitnessClass": { + "description": "SEM-218 author-intent class for a declaration.", + "enum": [ + "exact", + "constrained", + "open" + ], + "title": "ExplicitnessClass", + "type": "string" + }, "ExternalMappingLoss": { "additionalProperties": false, "description": "Loss-labeled mapping from an external vocabulary to RAES semantics.", @@ -19930,6 +20463,17 @@ "additionalProperties": false, "description": "Provider-neutral artifact reference.\n\nShorthand: ``source: \"package-name\"`` (version defaults to ``\"*\"``).\nLonghand: ``source: {name: \"package-name\", version: \"1.2.3\"}``.", "properties": { + "artifact_requirement": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactRequirement" + }, + { + "type": "null" + } + ], + "default": null + }, "build": { "anyOf": [ { diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index e3273f292..70b26d294 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -1,5 +1,270 @@ { "$defs": { + "ApparatusIdentity": { + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "ApparatusIdentity", + "type": "object" + }, + "ArtifactIdentity": { + "additionalProperties": false, + "description": "Immutable provider-neutral identity for one artifact payload.", + "properties": { + "artifact_id": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Artifact Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "media_type": { + "maxLength": 256, + "minLength": 1, + "title": "Media Type", + "type": "string" + }, + "version": { + "maxLength": 256, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "artifact_id", + "version", + "digest", + "media_type" + ], + "title": "ArtifactIdentity", + "type": "object" + }, + "ArtifactMechanismProfile": { + "additionalProperties": false, + "description": "Versioned portable mechanism profile.\n\nPortable mechanisms use the governed base names above or a namespaced\n``x-:`` extension. The profile digest binds the exact\nmechanism contract without making the mechanism set a closed union.", + "properties": { + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "mechanism": { + "anyOf": [ + { + "enum": [ + "backend-owned-artifact", + "dynamic-composition", + "exact-artifact", + "materialization-specification", + "published-candidate" + ] + }, + { + "pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" + } + ], + "maxLength": 128, + "minLength": 1, + "title": "Mechanism", + "type": "string" + }, + "profile": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$", + "title": "Profile", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mechanism", + "profile", + "version", + "digest" + ], + "title": "ArtifactMechanismProfile", + "type": "object" + }, + "ArtifactSatisfactionDisclosureModel": { + "additionalProperties": false, + "description": "Typed realized artifact and mechanism disclosure.\n\nMutable provider/account/project/region/registry/channel locations are\ndeliberately absent. They belong to operational evidence, not artifact or\nscenario identity.", + "properties": { + "acquisition": { + "enum": [ + "pull", + "copy", + "import", + "local-lookup", + "none" + ], + "title": "Acquisition", + "type": "string" + }, + "admission_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Admission Refs", + "type": "array", + "uniqueItems": true + }, + "artifact": { + "$ref": "#/$defs/ArtifactIdentity" + }, + "authenticity_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Authenticity Refs", + "type": "array", + "uniqueItems": true + }, + "backend": { + "$ref": "#/$defs/ApparatusIdentity" + }, + "candidate_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Candidate Id" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array", + "uniqueItems": true + }, + "integrity_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Integrity Refs", + "type": "array", + "uniqueItems": true + }, + "locked_input_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Locked Input Ids", + "type": "array", + "uniqueItems": true + }, + "materialization_specification_digest": { + "anyOf": [ + { + "pattern": "^sha256:[a-f0-9]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Materialization Specification Digest" + }, + "materialization_specification_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Materialization Specification Id" + }, + "mechanism": { + "$ref": "#/$defs/ArtifactMechanismProfile" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array", + "uniqueItems": true + }, + "requirement_id": { + "minLength": 1, + "title": "Requirement Id", + "type": "string" + }, + "satisfied_constraint_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Satisfied Constraint Ids", + "type": "array", + "uniqueItems": true + }, + "timing": { + "enum": [ + "publication", + "pack-ingestion", + "backend-preparation", + "realization" + ], + "title": "Timing", + "type": "string" + } + }, + "required": [ + "requirement_id", + "artifact", + "mechanism", + "acquisition", + "timing", + "backend", + "integrity_refs", + "provenance_refs" + ], + "title": "ArtifactSatisfactionDisclosureModel", + "type": "object" + }, "ClockTransitionEventModel": { "additionalProperties": false, "properties": { @@ -6401,6 +6666,17 @@ "title": "Address", "type": "string" }, + "artifact_satisfaction": { + "anyOf": [ + { + "$ref": "#/$defs/ArtifactSatisfactionDisclosureModel" + }, + { + "type": "null" + } + ], + "default": null + }, "domain": { "minLength": 1, "title": "Domain", diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index 6a733c128..bc9453474 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -140,6 +140,7 @@ adr-093-raes-rename-and-compatibility-boundaries adr-094-authoritative-cross-plane-experiment-bindings adr-095-participant-decision-epoch-state-cut-and-delivery-semantics adr-096-identity-cutover-and-historical-record-boundary +adr-097-portable-artifact-requirement-satisfaction ``` | ADR | Title | Status | Date | @@ -240,3 +241,4 @@ adr-096-identity-cutover-and-historical-record-boundary | [094](adr-094-authoritative-cross-plane-experiment-bindings.md) | Authoritative Cross-Plane Experiment Bindings | accepted | 2026-07-26 | | [095](adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md) | Participant Decision Epoch, State-Cut, And Delivery Semantics | accepted | 2026-07-26 | | [096](adr-096-identity-cutover-and-historical-record-boundary.md) | Identity Cutover and Historical-Record Boundary | accepted | 2026-07-26 | +| [097](adr-097-portable-artifact-requirement-satisfaction.md) | Portable Artifact Requirement Satisfaction | accepted | 2026-07-27 | diff --git a/docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md b/docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md new file mode 100644 index 000000000..0fb75e8f6 --- /dev/null +++ b/docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md @@ -0,0 +1,143 @@ +# ADR-097: Portable Artifact Requirement Satisfaction + +## Status + +accepted + +## Date + +2026-07-27 + +## Classification + +Classification: FM2 + +Required artifacts: ADR, normative specification, published contracts and +fixtures, compiler/planner/runtime integration, and whole-tree verification. + +Waivers: no provider registry, artifact graph, acquisition adapter, build +service, credential surface, or second realization planner is introduced. + +## Context + +`Source` has historically been a provider-neutral name/version selector. +Different backends may resolve that selector to a local image, imported +package, registry object, or dynamically prepared artifact, but the SDL has not +been able to distinguish an exact immutable requirement from bounded selection +or backend-owned realization. A name and version alone therefore cannot prove +that two backends realized the same artifact, and `Source.build` records +observed image provenance rather than authoring a future build obligation. + +ADR-070 already defines exact, constrained, and open realization posture, +compiled realization demand, backend capability admission, and runtime +non-approximation disclosure. ADR-071 and ADR-077 already own reusable-asset +trust policy and associated-artifact manifests. Artifact satisfaction must join +those authorities without creating a parallel artifact ontology or treating +mutable provider locations as scenario identity. + +## Decision + +Add an optional, versioned `artifact_requirement` to the existing `Source` +concern. Absence preserves the historical selector semantics. Presence uses the +existing `ExplicitnessClass`: + +- `exact` names exactly one digest-bound artifact identity. It permits no + candidate, constraint, locked-input, or materialization alternative and only + the `exact-artifact` mechanism. Failure to obtain that artifact is rejection, + never fallback, rebuild, or substitution. +- `constrained` declares a non-empty bounded authority domain using typed + constraints, immutable candidates, locked inputs, or digest-bound + materialization specifications. +- `open` delegates output artifact selection to a backend that explicitly + advertises open realization support. It cannot carry candidates, artifact + constraints, or materialization alternatives, though immutable input and + trust requirements remain valid. + +An artifact identity consists of a provider-neutral id, version, SHA-256 +digest, and media type. A `Source` selector is not integrity evidence; for an +exact requirement its name/version must match the immutable artifact identity. +`Source.build` remains observed provenance and cannot satisfy or expand an +authored materialization requirement. + +Satisfaction mechanisms are versioned digest-bound profiles. The contract +governs a small portable base vocabulary and admits namespaced +`x-:` extensions. Acquisition (`pull`, `copy`, `import`, +`local-lookup`, or `none`) and timing (`publication`, `pack-ingestion`, +`backend-preparation`, or `realization`) remain independent properties. Backend +manifests advertise mechanism-indexed acquisition/timing combinations under +the existing `realization_support` declaration so they cannot imply unsupported +Cartesian products. + +The processor lowers a present requirement into the existing +`CompiledRealizationRequirement` graph at the owning node, content, feature +binding, condition binding, inject, or event address. The existing planner +performs capability admission, supplemented by caller-supplied operational +availability facts. Mutable registry, region, account, channel, and locator +facts are not semantic identity and are not persisted in the requirement. +Availability and verified trust facts are partitioned by canonical compiled +address so requirement-local candidate, constraint, and locked-input ids +cannot collide across resources. + +At execution, the backend returns a typed artifact satisfaction disclosure on +the resource payload. The existing runtime non-approximation gate validates it +before snapshot persistence and attaches it to the existing +`RealizationProvenanceEntry`. Exact substitution or omission is +`runtime.backend-contract-invalid`. The disclosure records artifact, +mechanism, acquisition, timing, backend identity, integrity/authenticity, +admission, provenance, and evidence references, but no mutable location or +channel. Admission binds the backend identity and mechanism route to the +selected manifest, candidate ids to their authored immutable identities, +materialization selections to their locked inputs, and every trust/evidence +reference to the processor-owned verified context. A selected materialization +specification discloses its authored specification digest, which must also be +present in the address-scoped trusted availability context. + +The published `artifact-requirement-v1` schema, SDL schemas, +`backend-manifest-v2`, and `runtime-snapshot-v1` are the portable surfaces. +Their reference models, generated schemas, conformance fixtures, and schema +publication ledgers change together. The contract-specific Source schema makes +`artifact_requirement` structurally mandatory. Cross-object equality and +reference joins that JSON Schema cannot express use the existing governed +`x-raes-invariants` profile and importable validator surface. + +## Alternatives Considered + +Treat every `Source` name/version as exact. Rejected: a selector is neither a +content identity nor proof of availability, and `*` is intentionally a +selector. + +Add a top-level artifact graph or universal registry. Rejected: the concern is +owned by existing `Source` instances and existing compilation addresses; a +second graph would duplicate identity, planning, and lifecycle authority. + +Use `Source.build` as a materialization request. Rejected: it is observed build +provenance. Reinterpreting it would silently turn evidence into executable +authority. + +Publish separate booleans and lists for mechanisms, acquisition, and timing. +Rejected: their Cartesian product would overclaim backend support. + +Persist provider locations in scenario or satisfaction identity. Rejected: +locations are mutable operational facts, may expose host/account information, +and do not identify artifact bytes. + +## Consequences + +Authors can state portable exact, bounded, or delegated artifact intent without +naming a cloud, hypervisor, registry, image service, or build farm. Backends +must disclose the exact mechanism combinations they implement and must return +evidence sufficient for the runtime gate to reject silent approximation. + +Artifact availability remains an operational input. This decision does not +perform network acquisition, credential lookup, archive extraction, image +building, signature verification, or registry mutation. Those operations stay +with authorized backend and trust-policy owners. + +## References + +- [ADR-008](adr-008-processor-layer-and-execution-artifact-boundaries.md) +- [ADR-061](adr-061-published-schema-evolution-policy.md) +- [ADR-070](adr-070-realization-envelope-semantics.md) +- [ADR-071](adr-071-reusable-asset-trust-and-integrity-policy.md) +- [ADR-077](adr-077-associated-artifact-manifest-boundary.md) +- [Portable Artifact Requirement Satisfaction](../../../specs/supply-chain/artifact-requirement-satisfaction.md) diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 7f2ab9cf9..f82cf73a1 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -449,3 +449,6 @@ adrs: - date: 2026-07-26 ref: "#908" summary: "Retained the existing SonarCloud project key as an exact, content-bound external-service designation rather than a current RAES product identity." + - id: ADR-097 + path: docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md + pin: 01658277a7d10da7f98a38a33e6a9e1aff62bf329a2f5815d498fe99180a71b4 diff --git a/docs/decisions/issue-920-artifact-requirement-satisfaction-preflight.md b/docs/decisions/issue-920-artifact-requirement-satisfaction-preflight.md new file mode 100644 index 000000000..6d5dd815c --- /dev/null +++ b/docs/decisions/issue-920-artifact-requirement-satisfaction-preflight.md @@ -0,0 +1,376 @@ +# Issue 920 Artifact Requirement Satisfaction Preflight + +Date: 2026-07-27 + +Issue: #920. Requirement: none. The GitHub issue is the authoritative delivery +contract. + +This note records architecture guardrails only. It does not add SDL syntax, +publish a schema, implement resolution or acquisition, select a backend +adapter, or prescribe an implementation sequence. + +## Preflight Finding + +The repository already owns the required cross-cutting semantics, but no single +artifact contract joins them: + +- `Source` is a provider-neutral artifact selector and `Source.build` records + observed container build/provenance facts. +- SEM-218 owns exact, constrained, open, and omitted author intent across + validation, compilation, planning, execution, and disclosure. +- backend `realization_support` owns apparatus capability claims. +- associated-artifact manifests and reusable-asset policy own byte integrity, + authenticity, provenance, and admission evidence. +- runtime realization provenance and experiment-run realized-form disclosures + own live and archival disclosure. + +The intended design joins those authorities. It must not turn `Source.build` +into an executable recipe, use acquisition as authority, put mutable registry +facts into scenario identity, or add a second realization, trust, diagnostic, +or persistence stack. + +## Architecture Decisions And Guardrails + +### Publish one semantic family with phase-distinct carriers + +The portable surface is one versioned artifact-requirement-satisfaction +semantic family with closed, phase-appropriate carriers: + +- the **requirement** records author authority and acceptable satisfaction; +- the **capability declaration** records what an apparatus claims it can + satisfy; and +- the **satisfaction disclosure** records the artifact and mechanism actually + used. + +These carriers share identifiers and validation rules but are not one mutable +object. Authored requirements do not acquire backend output fields, backend +manifests do not restate author intent, and runtime/archival records do not +rewrite the instantiated scenario. This preserves ADR-008 and ADR-078 phase +boundaries. + +The contract belongs to the existing `Source` realization concern rather than +to a new top-level artifact graph. `Source` remains the provider-neutral +artifact identity/selector component. The artifact requirement owns authority +over that component; it does not redefine nodes, content, generated artifacts, +associated artifacts, or environment-pack layout. + +### Preserve the four author postures without a second taxonomy + +Artifact posture reuses SEM-218: + +- **exact** identifies exactly one immutable artifact. A backend either uses + that identity or rejects. Rebuilding, substituting, approximating, or choosing + an equivalent artifact is not satisfaction. +- **constrained** declares a non-empty candidate/domain surface from which the + backend may choose. Every selected artifact must satisfy all declared bounds. +- **open** explicitly delegates artifact choice at an admitted `Source` + realization point and may still constrain permitted mechanisms, timing, + trust, or input requirements. +- **absent** is the owning optional `Source` requirement being absent. It is not + another positive union member and does not authorize backend choice. A + backend may use implementation-private artifacts, but they do not become + authored scenario meaning or a satisfaction of a nonexistent requirement. + +Do not add an artifact-only posture enum. `ExplicitnessClass`, +`AuthorRealizationPosture`, the typed designation cascade, and +`ExplicitnessProvenance` remain canonical. The compiler must register the +artifact concern with the existing SEM-218 concern authority instead of +inferring posture from whether a name, tag, URI, build block, or backend image +is present. + +The existing `Source` shorthand and default `version="*"` are selectors, not +proof of immutable exact identity. A contract may classify a selector as +constrained, but it must not call it exact unless the authored form binds one +immutable identity using the existing digest/reference discipline. Any change +to legacy interpretation must be explicit, versioned, fixture-backed, and +reviewed under ADR-061; generic field-presence classification must not silently +promote `"*"` or a mutable tag to exact. + +Alternatives are author authority. An exact branch cannot gain implicit +fallbacks from a catalog, backend policy, materialization capability, or +`Source.build`. Candidates or alternative specifications exist only in a +constrained requirement that names them. + +### Keep satisfaction, acquisition, timing, and availability independent + +A satisfaction mechanism is not an acquisition verb. The core contract must +therefore carry these as separate dimensions: + +- **mechanism**: an exact artifact, backend-owned artifact, published candidate, + dynamic composition, explicitly permitted materialization specification, or + another declared mechanism; +- **acquisition**: pull, copy, import, local lookup, or no transfer; +- **timing**: publication, pack ingestion, explicit backend preparation, or + realization; and +- **availability**: provider/account/project/region/registry/channel facts. + +Mechanisms are an open governed extension surface, not a closed Python/JSON +union. A mechanism has a portable namespaced identifier plus an exact +profile/contract version (and digest where it references a published +specification). An unfamiliar mechanism can remain structurally valid but is +unrealizable unless the selected backend declares that exact mechanism +profile. Mechanism-specific payloads are closed versioned profiles; arbitrary +`options`, callback names, commands, or backend-native dictionaries are not a +portable extension mechanism. + +Acquisition and timing are disclosures/constraints on a satisfaction; neither +selects semantic authority. A pull can acquire an exact artifact, a candidate, +or an input. A dynamically composed artifact may use no transfer. Artifact type +must not imply timing. + +Availability is operational evidence and must not enter the scenario semantic +digest, exact requirement identity, candidate identity, or published-output +identity. Mutable locators and channels may be disclosed through their existing +operational/evidence surface, but identity joins use immutable refs and +digests. Credentials, entitlement, account/project selection, and registry +policy remain operator/product concerns. + +### Extend the existing realization and manifest seams + +Artifact requirements lower into `CompiledRealizationRequirement` (or a typed +artifact payload owned by it) with the canonical owner address, field path, +requirement kind, SEM-218 posture/provenance, governing scope, and requirement +identity. Do not add a second compiled requirement list or an artifact-only +planner. + +Backend support stays under `BackendManifest.realization_support` and +`BackendManifestV2Model.realization_support`. The artifact domain needs +mechanism-indexed capability declarations that preserve the existing exact, +constraint, and open matching rules. Each mechanism entry must declare its +supported requirement kinds and supported timing/acquisition combinations. +A collection of independent mechanism, timing, and acquisition lists is +insufficient because it falsely claims their Cartesian product. + +Portable mechanism/profile identifiers that require cross-backend comparison +must bind to the existing `realization-and-disclosure` and +`tools-and-artifacts` concept authorities. Product catalog policy and backend +native driver names are not RAES mechanism identifiers. + +Planning remains conjunctive. A requirement is realizable only when its posture, +artifact/candidate bounds, locked inputs, permitted mechanism, mechanism +profile, timing, and selected backend declaration all agree. A realization +envelope may further restrict the offer; it does not replace artifact +requirement matching. + +### Keep materialization specifications and build observations distinct + +`Source.build` remains the ADR-023 observation surface. It may support +provenance or trust evidence about a published artifact, but it is not: + +- a universal recipe; +- a backend instruction; +- permission to rebuild an exact artifact; +- a materialization alternative; or +- proof that referenced inputs are present, trusted, or reproducible. + +An executable materialization specification participates only when the authored +requirement explicitly permits its versioned mechanism profile. Its locked +inputs use immutable artifact references/checksums and the existing +associated-artifact/trust validators. It does not embed credentials, registry +tokens, host paths, shell text, environment dumps, or unbounded external fetch +instructions. + +Module `LockRecord` and `ResolvedImportProvenance` are useful digest/portability +incumbents but remain module-specific. Reuse their primitives and fail-closed +rules; do not relabel a module lockfile as a universal artifact input lock. + +### Reuse diagnostics and distinguish realizability failures + +All failures use the existing `Diagnostic` / `DiagnosticModel` envelope and +normal planner/runtime failure flow. The artifact domain must expose stable, +separately testable codes for at least: + +- unavailable exact artifact; +- unsatisfied artifact constraint; +- unsupported open artifact realization; +- missing or unverified locked input; +- unavailable published candidate; and +- unsupported backend mechanism/profile. + +These are not exception subclasses and must not collapse into +`not-found`, `build-failed`, or a generic `unsupported` string. A supported +mechanism that fails during execution still uses the ordinary backend-call or +operation failure surface; capability absence and execution failure are +different facts. + +Diagnostics name the canonical requirement/address, posture, mechanism/profile, +and failure class. They do not echo credentials, signed URLs, raw selectors, +candidate inventories, locked-input contents, backend-native objects, process +output, or exact values that may be sensitive. + +### Disclose satisfaction without changing scenario identity + +A successful satisfaction disclosure binds: + +- the exact requirement identity; +- the immutable realized artifact identity; +- the selected mechanism profile; +- acquisition and timing; +- the satisfying candidate/materialization/input refs where applicable; +- the backend/apparatus identity; and +- provenance, integrity, authenticity, admission, and evidence refs. + +The live carrier is a typed artifact-specific payload on, or exact reference +from, the existing `RealizationProvenanceEntry` ledger. It is not +`RuntimeSnapshot.metadata`, `ApplyResult.details`, a backend-private result, or +a second snapshot sidecar. The runtime non-approximation gate in +`realization_disclosure()` remains the acceptance boundary before a backend +snapshot is persisted. + +Archival runs reuse `ExperimentRunModel.realized_form_disclosures` and their +evidence traceability. If an artifact-specific shared component is needed, both +live and archival carriers reference that one component rather than copying +similar field sets. A new experiment-run root or provenance database is +forbidden. + +The realized/published output identity is realization provenance, not scenario +meaning. Selecting a candidate, composing an artifact, changing a registry +location, or promoting a mutable channel does not rewrite +`canonical_sdl_digest()`, `canonical_instantiated_sdl_digest()`, or the +instantiated scenario snapshot. An exact authored artifact identity remains +part of the requirement's meaning; a backend-selected output remains outside +that meaning. + +## Canonical Incumbents To Reuse + +- **Source and SDL shape:** `raes._source.Source`, the parser's scoped + `_expand_source()` shorthand, `SDLModel(extra="forbid")`, every existing + `Source | None` owner, the authored/instantiated phase models, and the SDL + schema/fixture families. +- **SEM-218 authority:** `raes.explicitness`, `raes.realization_designation`, + `ExpansionProvenance`, `InstantiationProvenance`, + `ExplicitnessProvenanceRecord`, `SemanticValidator`, + `SDLValidationError`, and `SDLInstantiationError`. +- **Compilation and planning:** `CompiledRealizationRequirement`, + `registered_realization_concerns()`, + `_compile_realization_requirements()`, + `realization_support_diagnostics()`, + `realization_envelope_diagnostics()`, `ExecutionPlan`, + `ProvisioningPlan`, and canonical compiled addresses. +- **Backend manifests:** `RealizationSupportMode`, + `RealizationSupportDeclaration`, + `RealizationSupportDeclarationModel`, `BackendManifest`, + `BackendManifestV2Model`, `backend_manifest_v2_model()`, + `backend_manifest_from_v2_model()`, + `BACKEND_SUPPORTED_CONTRACT_IDS`, concept bindings, and backend profiles. +- **Runtime boundary:** `realization_disclosure()`, + `_call_backend_apply()`, `_snapshot_contract_diagnostics()`, `ApplyResult`, + `RuntimeSnapshot`, `RealizationProvenanceEntry`, + `RuntimeSnapshotEnvelopeModel`, operation receipts/statuses, and baseline + snapshot rollback on invalid backend output. +- **Trust and artifacts:** `AssociatedArtifactManifestModel`, + `validate_associated_artifact_manifest()`, + `ExperimentArtifactRefModel`, `ExperimentChecksumModel`, ADR-071 reusable + asset evidence classes/policy, module registry trust/signature verification, + and URI credential/query rejection. +- **Observation and archival provenance:** `ExperimentRunModel`, + `ExperimentRealizedFormDisclosureModel`, experiment evidence refs, + apparatus manifest refs, and ADR-065/066 provenance/evidence separation. +- **Persistence and API:** `ControlPlaneStore`, + `LocalControlPlaneStore._atomic_write()`, snapshot serializers, + `ControlPlaneSecurityConfig`, request-size guards, role authorization, + idempotency fingerprints, `AuditEvent`, and redacted HTTP 500 envelopes. +- **Host execution security:** the reference OCI driver's `ImageTrustPolicy`, + fixed argv, runtime allowlist, bounded timeout, injected runner, rollback, + and native-output redaction. A future mechanism adapter must meet the same + boundary without making that OCI driver normative. +- **Contract publication:** ADR-009/019/061, + `contracts/schemas/`, `contracts/fixtures/`, + `contracts/schema-publication/entries/`, + `contracts/schema-publication-manifest.json`, `ContractModel`, + `schema_bundle()`, and `x-raes-invariants`. +- **Repository workflow:** `.ground-control.yaml`, `.gc/plan-rules.md`, + `noxfile.py`, `tools/check_repo_policy.py`, + `tools/check_requirement_governance.py`, + `tools/check_authority_boundary.py`, + `tools/check_concept_authority_governance.py`, + `tools/check_generated_schemas.py`, + `tools/check_schema_publication.py`, `tools/check_sdl_catalog_parity.py`, + `tools/check_sdl_lineage.py`, `tools/check_json_artifacts.py`, and + `tools/verify_all.py`. + +## Cross-Cutting Layers The Design Must Pass + +| Layer | Required behavior | +|---|---| +| YAML/source parsing | Continue through the bounded source reader, duplicate-key/tag/alias/scalar guards, scoped shorthand normalization, and closed Pydantic models. Nested mechanism/specification fields named `source` must not be accidentally rewritten by `_expand_source()`; update the canonical scoped rule rather than adding a second parser. | +| SDL semantic validation | Validate owner applicability, exact/constrained/open/absent exclusivity, immutable exact identity, non-empty constrained domains, candidate/ref uniqueness, locked-input joins, canonical pointers/addresses, and composition namespace rewriting through `SemanticValidator`. | +| Phase and digest validation | Preserve posture and requirement identity through expansion/instantiation provenance and compiler admission. Backend-selected artifacts, availability, acquisition, and mutable locations remain outside scenario semantic identity. | +| Contract/schema validation | Use closed `ContractModel` shapes, hand-governed published schemas, semantic invariants, valid/invalid fixtures, schema-bundle parity, publication-ledger updates, and ADR-061 compatibility review. Schema validity alone is not realizability or trust. | +| Manifest/config validation | Extend the canonical backend manifest renderer/parser and manifest-authority allowlist. Capability claims must pass model/schema shape, concept binding, contract-version, profile, and backend-profile validation; no backend-local environment flag or prose `constraints` entry substitutes for a typed mechanism declaration. | +| Trust/admission validation | Bind exact artifacts, candidates, specifications, and locked inputs to immutable checksum/digest refs; run associated-artifact byte/size validation and reusable-asset integrity/authenticity/provenance policy. URI or name equality is not byte, signature, entitlement, or admission proof. | +| Planner admission | Match the compiled requirement against one backend declaration without first-match/first-available fallback. Emit the required distinct `Diagnostic` codes before backend I/O when capability, candidate, input, constraint, mechanism, or timing is unsatisfied. | +| Runtime adapter validation | Admit backend results through `_call_backend_apply()` and `realization_disclosure()` before accepting the snapshot. Exact mismatches, undeclared mechanisms, missing satisfaction disclosure, and contradictory artifact identity fail closed and restore the baseline snapshot. | +| Persistence | Round-trip typed satisfaction provenance through `RuntimeSnapshot`, `ControlPlaneStore`, local atomic writes, and published snapshot models. Do not create an artifact resolver repository, sidecar file, cache index, or metadata-only ledger in this issue. | +| HTTP/auth and audit | No new endpoint is required. Any existing snapshot/operation exposure inherits strict identity defaults, role checks, target scoping, request-size limits, idempotency, audit events, and redacted 500 responses. Audit details carry ids/codes only, not selectors, locators, credentials, or raw materialization payloads. | +| Secret/env/config handling | Portable requirements, manifests, fixtures, provenance, and diagnostics are secret-free. Credentials and entitlement stay in operator-owned backend configuration. Do not add credential fields, environment bindings, token refs, signed URLs, or secret materialization parameters to the portable contract. | +| OS/process exposure | Immutable non-secret artifact refs may be discrete argv values only at a backend's impure leaf. Preserve fixed argv, no shell evaluation, bounded timeouts, allowlisted executables/runtime, injected runners, coarse diagnostics, and no credentials or raw backend output in argv/logs. | +| Observability and archival evidence | Use structured `Diagnostic`, `AuditEvent`, realization provenance, experiment evidence, and experiment-run disclosures. Do not add a general logger payload, claim that a manifest proves execution, or treat a planned/echoed value as fresh realization evidence. | +| Conformance/testing | Reuse SEM-218, backend-manifest, runtime-planner, runtime-contract, associated-artifact, trust, schema, and realization-honesty suites. Matrix/property tests must vary posture, mechanism, acquisition, timing, availability, trust, and exactness independently so accidental axis coupling is observable. | + +## Extensibility Seam + +The extension seam is a versioned mechanism-profile reference carried through +the requirement, backend mechanism capability matrix, compiled realization +requirement, and satisfaction disclosure. The matcher is parameterized by +artifact requirement kind, canonical owner/address, mechanism profile, timing, +and immutable input/candidate refs. + +A future artifact class or satisfaction mechanism adds its closed profile, +concept binding, backend declaration, validator, and conformance evidence. It +does not edit a closed pull-versus-build union, add a backend name to SDL, +change `Source.build`, or fork planner/runtime matching. This admits the next +reasonable mechanisms—such as another backend-native composer or prepared +artifact form—without changing the core posture contract. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating pull and build/materialize as opposites or the complete mechanism + set; +- treating acquisition, timing, availability, location, or channel as semantic + authority; +- treating a tag, default `"*"`, URI, backend-local id, or successful lookup as + immutable exact identity; +- allowing an exact requirement to fall back to candidates, composition, + reconstruction, approximation, or an equivalent artifact; +- turning absence into open realization, or an implementation-private artifact + into authored meaning; +- using `Source.build` as executable input or accepting raw Dockerfile/shell + text as a portable mechanism; +- embedding credentials, entitlement, provider/account/project/region, + environment variables, host paths, commands, or opaque options in the + portable contract; +- flat capability lists whose accidental Cartesian product overclaims supported + mechanism/timing/acquisition combinations; +- first-available candidate selection, order-dependent fallback, or + backend-specific policy in RAES semantics; +- putting satisfaction only in `metadata`, `details`, logs, or backend-native + state; +- minting a second explicitness classifier, artifact planner, trust policy, + checksum type, diagnostics hierarchy, exception family, persistence store, + audit log, schema registry, or experiment-run root; +- copying live satisfaction fields independently into archival provenance; +- changing scenario or snapshot semantic identity when only a realized output, + availability fact, registry location, or mutable channel changes; and +- declaring backend conformance from schema validity, a manifest claim, planned + state, or echoed output without runtime rejection tests and provenance. + +## Non-Goals And Implementation Boundaries + +- No registry, catalog, build farm, hosted service, credential broker, + entitlement service, or operated distribution channel. +- No environment-pack layout, release packaging, publication tooling, or + downstream catalog policy. +- No requirement that every scenario/node has an image or that every artifact + is reproducible, materializable, or transferable. +- No product-specific resolution order, candidate ranking, preparation timing, + VM-image policy, or backend adapter implementation. +- No new SDL topology, content, generated-artifact, associated-artifact, trust, + experiment-run, or runtime-lifecycle authority. +- No universal recipe model and no reinterpretation of `Source.build`. +- No new API endpoint, auth mode, secret/config surface, persistence service, + background resolver, or external network operation. +- No implementation code, schema, fixture, manifest, vocabulary, example, or + conformance change in this preflight. 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 c3bbdf0f7..38accf0bf 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": "918862c521a9c5a282b7cbd20ba6fcddd20eb90817ed5bcebacdf3270f91d7ac", + "result_digest": "a94eeb80e26ec38b621b60e6ed90fbe32d9bf9a996d264144b641819c4609823", "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": "c310cf5424ab404673093e5c95801d00e5583947dbcac68c47d4d8c1eb8b6766", + "result_digest": "2515d5a64e0d31564078e2ad7be7aea352849b932357a49a7e9107b7d06939e1", "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/implementations/python/packages/raes/_source.py b/implementations/python/packages/raes/_source.py index 0f5bdb950..1b7ce091d 100644 --- a/implementations/python/packages/raes/_source.py +++ b/implementations/python/packages/raes/_source.py @@ -1,4 +1,4 @@ -"""Shared Source reference type. +"""Shared Source and portable artifact-requirement reference types. A Source identifies an artifact (VM image, package, template) by name and version. Supports shorthand (bare string) and longhand @@ -13,11 +13,346 @@ ADR-023 and ``image_provenance``). """ -from pydantic import Field +import re +from typing import Annotated, Literal + +from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema from ._base import SDLModel +from .explicitness import ExplicitnessClass from .image_provenance import ContainerImageBuildProvenance +_SHA256_DIGEST_PATTERN = r"^sha256:[a-f0-9]{64}$" +_EXTENSION_MECHANISM_PATTERN = re.compile(r"^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$") +_PORTABLE_MECHANISMS = frozenset( + { + "exact-artifact", + "backend-owned-artifact", + "published-candidate", + "dynamic-composition", + "materialization-specification", + } +) +_PORTABLE_IDENTIFIER_PATTERN = r"^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$" + + +def _require_unique(values: list[str], *, field_name: str) -> None: + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must not contain duplicates") + + +class ArtifactIdentity(SDLModel): + """Immutable provider-neutral identity for one artifact payload.""" + + artifact_id: Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + version: Annotated[str, Field(min_length=1, max_length=256)] + digest: Annotated[str, Field(pattern=_SHA256_DIGEST_PATTERN)] + media_type: Annotated[str, Field(min_length=1, max_length=256)] + + +class ArtifactMechanismProfile(SDLModel): + """Versioned portable mechanism profile. + + Portable mechanisms use the governed base names above or a namespaced + ``x-:`` extension. The profile digest binds the exact + mechanism contract without making the mechanism set a closed union. + """ + + mechanism: Annotated[str, Field(min_length=1, max_length=128)] + profile: Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + version: Annotated[str, Field(min_length=1, max_length=128)] + digest: Annotated[str, Field(pattern=_SHA256_DIGEST_PATTERN)] + + @model_validator(mode="after") + def validate_mechanism(self) -> "ArtifactMechanismProfile": + if ( + self.mechanism not in _PORTABLE_MECHANISMS + and _EXTENSION_MECHANISM_PATTERN.fullmatch(self.mechanism) is None + ): + raise ValueError("mechanism must be a portable base mechanism or a governed x-: extension") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler.resolve_ref_schema(handler(core_schema)) + json_schema["properties"]["mechanism"]["anyOf"] = [ + {"enum": sorted(_PORTABLE_MECHANISMS)}, + {"pattern": _EXTENSION_MECHANISM_PATTERN.pattern}, + ] + return json_schema + + +class ArtifactSatisfactionRoute(SDLModel): + """One permitted mechanism/acquisition/timing combination.""" + + mechanism: ArtifactMechanismProfile + acquisition: Literal["pull", "copy", "import", "local-lookup", "none"] + timing: Literal["publication", "pack-ingestion", "backend-preparation", "realization"] + + +class ArtifactConstraint(SDLModel): + """One typed, named bound on a constrained artifact selection.""" + + constraint_id: Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + kind: Annotated[str, Field(min_length=1, max_length=128, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + allowed_values: list[Annotated[str, Field(min_length=1, max_length=1024)]] = Field(min_length=1) + + @model_validator(mode="after") + def validate_values(self) -> "ArtifactConstraint": + _require_unique(self.allowed_values, field_name="artifact constraint allowed_values") + return self + + +class ArtifactCandidate(SDLModel): + """One immutable candidate explicitly admitted by the author.""" + + candidate_id: Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + artifact: ArtifactIdentity + + +class ArtifactLockedInput(SDLModel): + """One immutable materialization input joined to existing trust contracts.""" + + input_id: Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + artifact: ArtifactIdentity + associated_artifact_manifest_ref: Annotated[str, Field(min_length=1, max_length=1024)] + trust_policy_ref: Annotated[str, Field(min_length=1, max_length=1024)] + + +class ArtifactMaterializationSpecification(SDLModel): + """Reference to a closed executable materialization profile. + + This is a digest-bound specification reference, not shell text, a + Dockerfile, an environment map, or a reinterpretation of ``Source.build``. + """ + + specification_id: Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + profile: ArtifactMechanismProfile + digest: Annotated[str, Field(pattern=_SHA256_DIGEST_PATTERN)] + locked_input_ids: list[ + Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + ] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) + + @model_validator(mode="after") + def validate_input_ids(self) -> "ArtifactMaterializationSpecification": + if self.profile.mechanism != "materialization-specification" and not self.profile.mechanism.startswith("x-"): + raise ValueError( + "materialization specifications require the materialization-specification " + "mechanism or a governed extension" + ) + _require_unique(self.locked_input_ids, field_name="materialization locked_input_ids") + 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", []).append( + { + "properties": { + "profile": { + "allOf": [ + { + "properties": { + "mechanism": { + "anyOf": [ + {"const": "materialization-specification"}, + {"pattern": _EXTENSION_MECHANISM_PATTERN.pattern}, + ] + } + }, + "required": ["mechanism"], + } + ] + } + } + } + ) + return json_schema + + +class ArtifactRequirement(SDLModel): + """Author-owned artifact requirement attached to a ``Source`` selector.""" + + requirement_id: Annotated[str, Field(min_length=1, max_length=256, pattern=_PORTABLE_IDENTIFIER_PATTERN)] + explicitness: ExplicitnessClass + exact_artifact: ArtifactIdentity | None = None + constraints: list[ArtifactConstraint] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + candidates: list[ArtifactCandidate] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + locked_inputs: list[ArtifactLockedInput] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + materialization_specifications: list[ArtifactMaterializationSpecification] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + permitted_routes: list[ArtifactSatisfactionRoute] = Field( + min_length=1, + json_schema_extra={"uniqueItems": True}, + ) + trust_policy_refs: list[Annotated[str, Field(min_length=1, max_length=1024)]] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + associated_artifact_manifest_refs: list[Annotated[str, Field(min_length=1, max_length=1024)]] = Field( + default_factory=list, json_schema_extra={"uniqueItems": True} + ) + + @model_validator(mode="after") + def validate_authority(self) -> "ArtifactRequirement": + self._validate_unique_identities() + if self.explicitness is ExplicitnessClass.EXACT: + if self.exact_artifact is None: + raise ValueError("exact artifact requirements require one immutable exact_artifact identity") + if self.constraints or self.candidates or self.locked_inputs or self.materialization_specifications: + raise ValueError( + "exact artifact requirements must not declare alternative constraints, " + "candidates, locked inputs, or materialization specifications" + ) + if any(route.mechanism.mechanism != "exact-artifact" for route in self.permitted_routes): + raise ValueError("exact artifact requirements permit only the exact-artifact mechanism") + elif self.explicitness is ExplicitnessClass.CONSTRAINED: + if self.exact_artifact is not None: + raise ValueError("constrained artifact requirements must not declare exact_artifact") + if not (self.constraints or self.candidates or self.locked_inputs or self.materialization_specifications): + raise ValueError("constrained artifact requirements require a non-empty constraint domain") + else: + if self.exact_artifact is not None: + raise ValueError("open artifact requirements must not declare exact_artifact") + if self.constraints or self.candidates or self.materialization_specifications: + raise ValueError( + "open artifact requirements must not declare constraints, " + "candidates, or materialization specifications" + ) + self._validate_materialization_input_joins() + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler.resolve_ref_schema(handler(core_schema)) + exact_condition = { + "properties": {"explicitness": {"const": ExplicitnessClass.EXACT.value}}, + "required": ["explicitness"], + } + constrained_condition = { + "properties": {"explicitness": {"const": ExplicitnessClass.CONSTRAINED.value}}, + "required": ["explicitness"], + } + open_condition = { + "properties": {"explicitness": {"const": ExplicitnessClass.OPEN.value}}, + "required": ["explicitness"], + } + json_schema.setdefault("allOf", []).extend( + [ + { + "if": exact_condition, + "then": { + "required": ["exact_artifact"], + "properties": { + "exact_artifact": {"not": {"type": "null"}}, + "constraints": {"maxItems": 0}, + "candidates": {"maxItems": 0}, + "locked_inputs": {"maxItems": 0}, + "materialization_specifications": {"maxItems": 0}, + "permitted_routes": { + "items": { + "properties": { + "mechanism": {"properties": {"mechanism": {"const": "exact-artifact"}}} + } + } + }, + }, + }, + }, + { + "if": constrained_condition, + "then": { + "properties": {"exact_artifact": {"type": "null"}}, + "anyOf": [ + {"properties": {"constraints": {"minItems": 1}}, "required": ["constraints"]}, + {"properties": {"candidates": {"minItems": 1}}, "required": ["candidates"]}, + {"properties": {"locked_inputs": {"minItems": 1}}, "required": ["locked_inputs"]}, + { + "properties": {"materialization_specifications": {"minItems": 1}}, + "required": ["materialization_specifications"], + }, + ], + }, + }, + { + "if": open_condition, + "then": { + "properties": { + "exact_artifact": {"type": "null"}, + "constraints": {"maxItems": 0}, + "candidates": {"maxItems": 0}, + "materialization_specifications": {"maxItems": 0}, + } + }, + }, + ] + ) + return json_schema + + def _validate_unique_identities(self) -> None: + for field_name, values in ( + ("constraint ids", [item.constraint_id for item in self.constraints]), + ("candidate ids", [item.candidate_id for item in self.candidates]), + ("locked input ids", [item.input_id for item in self.locked_inputs]), + ( + "materialization specification ids", + [item.specification_id for item in self.materialization_specifications], + ), + ( + "permitted routes", + [ + ( + item.mechanism.mechanism, + item.mechanism.profile, + item.mechanism.version, + item.acquisition, + item.timing, + ) + for item in self.permitted_routes + ], + ), + ("trust policy refs", self.trust_policy_refs), + ("associated artifact manifest refs", self.associated_artifact_manifest_refs), + ): + if len(values) != len(set(values)): + raise ValueError(f"artifact requirement {field_name} must not contain duplicates") + + def _validate_materialization_input_joins(self) -> None: + declared = {item.input_id for item in self.locked_inputs} + referenced = { + input_id + for specification in self.materialization_specifications + for input_id in specification.locked_input_ids + } + missing = sorted(referenced - declared) + if missing: + raise ValueError("materialization specification references missing locked input ids: " + ", ".join(missing)) + class Source(SDLModel): """Provider-neutral artifact reference. @@ -29,3 +364,16 @@ class Source(SDLModel): name: str version: str = Field(default="*") build: ContainerImageBuildProvenance | None = None + artifact_requirement: ArtifactRequirement | None = None + + @model_validator(mode="after") + def validate_exact_requirement_selector(self) -> "Source": + requirement = self.artifact_requirement + if requirement is None or requirement.explicitness is not ExplicitnessClass.EXACT: + return self + assert requirement.exact_artifact is not None + if self.name != requirement.exact_artifact.artifact_id or self.version != requirement.exact_artifact.version: + raise ValueError( + "exact artifact requirement immutable identity must match the Source selector name and version" + ) + return self diff --git a/implementations/python/packages/raes/artifact_requirements.py b/implementations/python/packages/raes/artifact_requirements.py new file mode 100644 index 000000000..9c6339537 --- /dev/null +++ b/implementations/python/packages/raes/artifact_requirements.py @@ -0,0 +1,25 @@ +"""Public SDL artifact-requirement model surface.""" + +from ._source import ( + ArtifactCandidate, + ArtifactConstraint, + ArtifactIdentity, + ArtifactLockedInput, + ArtifactMaterializationSpecification, + ArtifactMechanismProfile, + ArtifactRequirement, + ArtifactSatisfactionRoute, + Source, +) + +__all__ = [ + "ArtifactCandidate", + "ArtifactConstraint", + "ArtifactIdentity", + "ArtifactLockedInput", + "ArtifactMaterializationSpecification", + "ArtifactMechanismProfile", + "ArtifactRequirement", + "ArtifactSatisfactionRoute", + "Source", +] diff --git a/implementations/python/packages/raes_backend_protocols/manifest.py b/implementations/python/packages/raes_backend_protocols/manifest.py index e2da840b4..69a90c16f 100644 --- a/implementations/python/packages/raes_backend_protocols/manifest.py +++ b/implementations/python/packages/raes_backend_protocols/manifest.py @@ -99,6 +99,7 @@ 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), + artifact_mechanisms=list(declaration.artifact_mechanisms), constraints=dict(declaration.constraints), ) for declaration in manifest.realization_support @@ -280,6 +281,7 @@ 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), + artifact_mechanisms=tuple(model.artifact_mechanisms), constraints=dict(model.constraints), ) diff --git a/implementations/python/packages/raes_contracts/apparatus.py b/implementations/python/packages/raes_contracts/apparatus.py index 295b92846..7700ade8d 100644 --- a/implementations/python/packages/raes_contracts/apparatus.py +++ b/implementations/python/packages/raes_contracts/apparatus.py @@ -3,9 +3,13 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import TYPE_CHECKING from .vocabulary import RealizationSupportMode +if TYPE_CHECKING: + from .artifact_requirements import ArtifactMechanismCapability + DECLARED_CAPABILITY_MATCH_REQUIREMENT_KIND = "declared-capability-match" RUNTIME_REALIZATION_DOMAIN = "runtime-realization" @@ -52,6 +56,7 @@ class RealizationSupportDeclaration: supported_constraint_kinds: frozenset[str] = frozenset() supported_exact_requirement_kinds: frozenset[str] = frozenset() disclosure_kinds: frozenset[str] = frozenset() + artifact_mechanisms: tuple[ArtifactMechanismCapability, ...] = () constraints: dict[str, str] = field(default_factory=dict) def __post_init__(self) -> None: @@ -72,3 +77,14 @@ def __post_init__(self) -> None: ) if self.support_mode == RealizationSupportMode.EXACT_ONLY and self.supported_constraint_kinds: raise ValueError("exact-only realization support must not declare supported_constraint_kinds") + identities = [ + ( + capability.mechanism.mechanism, + capability.mechanism.profile, + capability.mechanism.version, + capability.mechanism.digest, + ) + for capability in self.artifact_mechanisms + ] + if len(identities) != len(set(identities)): + raise ValueError("artifact_mechanisms must not contain duplicate mechanism profiles") diff --git a/implementations/python/packages/raes_contracts/artifact_requirements.py b/implementations/python/packages/raes_contracts/artifact_requirements.py new file mode 100644 index 000000000..5edef14dd --- /dev/null +++ b/implementations/python/packages/raes_contracts/artifact_requirements.py @@ -0,0 +1,352 @@ +"""Portable artifact requirement, backend capability, and satisfaction carriers.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Annotated, Literal + +from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema +from raes.artifact_requirements import ( + ArtifactIdentity, + ArtifactMechanismProfile, + ArtifactRequirement, + Source, +) + +from .apparatus import ApparatusIdentity +from .contracts.base import ContractModel, NonEmptyString +from .versions import ARTIFACT_REQUIREMENT_SCHEMA_VERSION + +Sha256DigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] + + +def _require_unique(values: list[object], *, field_name: str) -> None: + normalized = [repr(value) for value in values] + if len(normalized) != len(set(normalized)): + raise ValueError(f"{field_name} must not contain duplicates") + + +def artifact_requirement_invariant_violations(payload: object) -> tuple[str, ...]: + """Evaluate the cross-object invariants published in ``x-raes-invariants``.""" + + if not isinstance(payload, Mapping): + return ("artifact-requirement-document-object",) + source = payload.get("source") + if not isinstance(source, Mapping): + return ("artifact-requirement-source-object",) + requirement = source.get("artifact_requirement") + if not isinstance(requirement, Mapping): + return ("artifact-requirement-present",) + + violations: list[str] = [] + if requirement.get("explicitness") == "exact": + identity = requirement.get("exact_artifact") + if not isinstance(identity, Mapping): + violations.append("exact-artifact-present") + else: + if source.get("name") != identity.get("artifact_id"): + violations.append("exact-source-artifact-id-match") + if source.get("version", "*") != identity.get("version"): + violations.append("exact-source-version-match") + + locked_inputs = requirement.get("locked_inputs", []) + declared_input_ids = { + item.get("input_id") + for item in locked_inputs + if isinstance(item, Mapping) and isinstance(item.get("input_id"), str) + } + specifications = requirement.get("materialization_specifications", []) + for specification in specifications: + if not isinstance(specification, Mapping): + continue + profile = specification.get("profile") + mechanism = profile.get("mechanism") if isinstance(profile, Mapping) else None + if mechanism != "materialization-specification" and not ( + isinstance(mechanism, str) and mechanism.startswith("x-") + ): + violations.append("materialization-profile-mechanism") + referenced = specification.get("locked_input_ids", []) + if isinstance(referenced, list) and not set(referenced).issubset(declared_input_ids): + violations.append("materialization-locked-input-join") + return tuple(dict.fromkeys(violations)) + + +def validate_artifact_requirement_invariants( + payload: Mapping[str, object], +) -> ArtifactRequirementContractModel: + """Enforce the published cross-object semantic-invariant profile.""" + + violations = artifact_requirement_invariant_violations(payload) + if violations: + raise ValueError("artifact requirement semantic invariants failed: " + ", ".join(violations)) + return ArtifactRequirementContractModel.model_validate(payload) + + +class ArtifactRequirementSource(Source): + """Contract-specific source that cannot omit portable artifact demand.""" + + artifact_requirement: ArtifactRequirement + + +class ArtifactRequirementContractModel(ContractModel): + """Published source-artifact requirement contract.""" + + schema_version: Literal[ARTIFACT_REQUIREMENT_SCHEMA_VERSION] = ARTIFACT_REQUIREMENT_SCHEMA_VERSION + source: ArtifactRequirementSource + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler.resolve_ref_schema(handler(core_schema)) + json_schema["x-raes-invariants"] = [ + { + "id": "exact-source-artifact-id-match", + "description": ( + "An exact requirement's immutable artifact id must equal the enclosing Source selector name." + ), + "level": "error", + "validator": ("raes_contracts.artifact_requirements.validate_artifact_requirement_invariants"), + "inputs": [ + { + "contract_id": "artifact-requirement-v1", + "instance_path": "#", + } + ], + }, + { + "id": "exact-source-version-match", + "description": ( + "An exact requirement's immutable artifact version must " + "equal the enclosing Source selector version." + ), + "level": "error", + "validator": ("raes_contracts.artifact_requirements.validate_artifact_requirement_invariants"), + "inputs": [ + { + "contract_id": "artifact-requirement-v1", + "instance_path": "#", + } + ], + }, + { + "id": "materialization-locked-input-join", + "description": ( + "Every materialization locked_input_id must resolve to a " + "locked input declared by the same artifact requirement." + ), + "level": "error", + "validator": ("raes_contracts.artifact_requirements.validate_artifact_requirement_invariants"), + "inputs": [ + { + "contract_id": "artifact-requirement-v1", + "instance_path": "#", + } + ], + }, + ] + return json_schema + + +class ArtifactAcquisitionTimingModel(ContractModel): + """One exact acquisition/timing combination supported by a backend.""" + + acquisition: Literal["pull", "copy", "import", "local-lookup", "none"] + timing: Literal["publication", "pack-ingestion", "backend-preparation", "realization"] + + +class ArtifactMechanismCapability(ContractModel): + """Mechanism-indexed backend support without Cartesian-product overclaim.""" + + mechanism: ArtifactMechanismProfile + supported_requirement_kinds: list[NonEmptyString] = Field( + min_length=1, + json_schema_extra={"uniqueItems": True}, + ) + supported_routes: list[ArtifactAcquisitionTimingModel] = Field(min_length=1) + + @model_validator(mode="after") + def validate_capability(self) -> ArtifactMechanismCapability: + _require_unique( + list(self.supported_requirement_kinds), + field_name="artifact mechanism supported_requirement_kinds", + ) + _require_unique( + [(route.acquisition, route.timing) for route in self.supported_routes], + field_name="artifact mechanism supported_routes", + ) + return self + + +class ArtifactRequirementAvailability(ContractModel): + """Trusted operational facts scoped to one compiled artifact requirement.""" + + address: NonEmptyString + + available_artifact_digests: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + available_candidate_ids: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + verified_locked_input_ids: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + satisfied_constraint_ids: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + available_materialization_specification_digests: list[Sha256DigestString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + verified_integrity_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + verified_authenticity_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + verified_admission_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + verified_provenance_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + verified_evidence_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + + @model_validator(mode="after") + def validate_unique_facts(self) -> ArtifactRequirementAvailability: + for field_name in ( + "available_artifact_digests", + "available_candidate_ids", + "verified_locked_input_ids", + "satisfied_constraint_ids", + "available_materialization_specification_digests", + "verified_integrity_refs", + "verified_authenticity_refs", + "verified_admission_refs", + "verified_provenance_refs", + "verified_evidence_refs", + ): + _require_unique( + list(getattr(self, field_name)), + field_name=f"artifact availability {field_name}", + ) + return self + + +class ArtifactAvailabilityContext(ContractModel): + """Processor-owned artifact facts, partitioned by compiled address.""" + + requirements: list[ArtifactRequirementAvailability] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + + @model_validator(mode="after") + def validate_unique_addresses(self) -> ArtifactAvailabilityContext: + _require_unique( + [item.address for item in self.requirements], + field_name="artifact availability requirement addresses", + ) + return self + + def for_address(self, address: str) -> ArtifactRequirementAvailability: + """Return trusted facts for ``address`` or an empty scoped carrier.""" + + return next( + (item for item in self.requirements if item.address == address), + ArtifactRequirementAvailability(address=address), + ) + + +class ArtifactSatisfactionDisclosureModel(ContractModel): + """Typed realized artifact and mechanism disclosure. + + Mutable provider/account/project/region/registry/channel locations are + deliberately absent. They belong to operational evidence, not artifact or + scenario identity. + """ + + requirement_id: NonEmptyString + artifact: ArtifactIdentity + mechanism: ArtifactMechanismProfile + acquisition: Literal["pull", "copy", "import", "local-lookup", "none"] + timing: Literal["publication", "pack-ingestion", "backend-preparation", "realization"] + backend: ApparatusIdentity + candidate_id: NonEmptyString | None = None + materialization_specification_id: NonEmptyString | None = None + materialization_specification_digest: Sha256DigestString | None = None + satisfied_constraint_ids: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + locked_input_ids: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + integrity_refs: list[NonEmptyString] = Field( + min_length=1, + json_schema_extra={"uniqueItems": True}, + ) + authenticity_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + admission_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + provenance_refs: list[NonEmptyString] = Field( + min_length=1, + json_schema_extra={"uniqueItems": True}, + ) + evidence_refs: list[NonEmptyString] = Field( + default_factory=list, + json_schema_extra={"uniqueItems": True}, + ) + + @model_validator(mode="after") + def validate_references(self) -> ArtifactSatisfactionDisclosureModel: + for field_name in ( + "satisfied_constraint_ids", + "locked_input_ids", + "integrity_refs", + "authenticity_refs", + "admission_refs", + "provenance_refs", + "evidence_refs", + ): + _require_unique( + list(getattr(self, field_name)), + field_name=f"artifact satisfaction {field_name}", + ) + return self + + +__all__ = [ + "ARTIFACT_REQUIREMENT_SCHEMA_VERSION", + "ArtifactAcquisitionTimingModel", + "ArtifactAvailabilityContext", + "ArtifactMechanismCapability", + "ArtifactRequirementContractModel", + "ArtifactRequirementAvailability", + "ArtifactRequirementSource", + "ArtifactSatisfactionDisclosureModel", + "artifact_requirement_invariant_violations", + "validate_artifact_requirement_invariants", +] diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index cb85a7cc6..5c406b682 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -3,7 +3,19 @@ # This module is an intentional re-export facade governed by PUBLIC_EXPORTS. # ruff: noqa: F401, F403 +from ..artifact_requirements import ( + ArtifactAcquisitionTimingModel, + ArtifactAvailabilityContext, + ArtifactMechanismCapability, + ArtifactRequirementAvailability, + ArtifactRequirementContractModel, + ArtifactRequirementSource, + ArtifactSatisfactionDisclosureModel, + artifact_requirement_invariant_violations, + validate_artifact_requirement_invariants, +) from ..versions import ( + ARTIFACT_REQUIREMENT_SCHEMA_VERSION, ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION, ATLAS_TACTICS_SOURCE_SCHEMA_VERSION, ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION, diff --git a/implementations/python/packages/raes_contracts/contracts/_exports.py b/implementations/python/packages/raes_contracts/contracts/_exports.py index ab6a30e5f..2ef87eab7 100644 --- a/implementations/python/packages/raes_contracts/contracts/_exports.py +++ b/implementations/python/packages/raes_contracts/contracts/_exports.py @@ -12,6 +12,16 @@ "AtlasTacticSourceTermModel", "AtlasTacticsSourceModel", "ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION", + "ARTIFACT_REQUIREMENT_SCHEMA_VERSION", + "ArtifactAcquisitionTimingModel", + "ArtifactAvailabilityContext", + "ArtifactMechanismCapability", + "ArtifactRequirementContractModel", + "ArtifactRequirementAvailability", + "ArtifactRequirementSource", + "ArtifactSatisfactionDisclosureModel", + "artifact_requirement_invariant_violations", + "validate_artifact_requirement_invariants", "AssociatedArtifactManifestModel", "AssociatedArtifactParentReferenceModel", "AssociatedArtifactSetDigestString", diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index 94ec48ddd..e3b4489fc 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -9,6 +9,8 @@ from raes.canonical import InstantiatedScenarioSnapshot from raes.scenario import InstantiatedScenario, Scenario +from raes_contracts.artifact_requirements import ArtifactRequirementContractModel + from .associated_artifacts import AssociatedArtifactManifestModel from .catalogs import ( ConceptFamilyCatalogModel, @@ -128,6 +130,7 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "instantiated-scenario-v1": InstantiatedScenario.model_json_schema(), "instantiated-scenario-snapshot-v1": InstantiatedScenarioSnapshot.model_json_schema(), "scenario-instantiation-request-v1": InstantiationRequestModel.model_json_schema(), + "artifact-requirement-v1": ArtifactRequirementContractModel.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(), diff --git a/implementations/python/packages/raes_contracts/contracts/capabilities.py b/implementations/python/packages/raes_contracts/contracts/capabilities.py index 1564cc266..bcce9c368 100644 --- a/implementations/python/packages/raes_contracts/contracts/capabilities.py +++ b/implementations/python/packages/raes_contracts/contracts/capabilities.py @@ -6,6 +6,7 @@ from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema +from ..artifact_requirements import ArtifactMechanismCapability from ..vocabulary import RealizationSupportMode, WorkflowFeature, WorkflowStatePredicateFeature from .base import ContractModel, NonEmptyString from .validators import _validate_controlled_vocabulary_terms @@ -259,6 +260,7 @@ class RealizationSupportDeclarationModel(ContractModel): 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) + artifact_mechanisms: list[ArtifactMechanismCapability] = Field(default_factory=list) constraints: dict[str, str] = Field(default_factory=dict) @model_validator(mode="after") @@ -270,6 +272,17 @@ def _validate_realization_support(self) -> RealizationSupportDeclarationModel: ) if self.support_mode == RealizationSupportMode.EXACT_ONLY and self.supported_constraint_kinds: raise ValueError("exact-only realization support must not declare supported_constraint_kinds") + identities = [ + ( + capability.mechanism.mechanism, + capability.mechanism.profile, + capability.mechanism.version, + capability.mechanism.digest, + ) + for capability in self.artifact_mechanisms + ] + if len(identities) != len(set(identities)): + raise ValueError("artifact_mechanisms must not contain duplicate mechanism profiles") return self @classmethod diff --git a/implementations/python/packages/raes_contracts/contracts/realization_plans.py b/implementations/python/packages/raes_contracts/contracts/realization_plans.py index 9ab633e10..a1fc1141d 100644 --- a/implementations/python/packages/raes_contracts/contracts/realization_plans.py +++ b/implementations/python/packages/raes_contracts/contracts/realization_plans.py @@ -8,6 +8,7 @@ from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance from ..addressing import CompiledAddress +from ..artifact_requirements import ArtifactSatisfactionDisclosureModel from ..planning import RuntimeDomain, require_plan_operation_identity from ..versions import OPERATION_SCHEMA_VERSION, RUNTIME_SNAPSHOT_SCHEMA_VERSION from .base import ContractModel, NonEmptyString @@ -141,6 +142,10 @@ class RealizationProvenanceEntryModel(ContractModel): explicitness: ExplicitnessClass provenance: ExplicitnessProvenance governing_scope: NonEmptyString | None = None + artifact_satisfaction: ArtifactSatisfactionDisclosureModel | None = Field( + default=None, + exclude_if=lambda value: value is None, + ) class RuntimeSnapshotEnvelopeModel(ContractModel): diff --git a/implementations/python/packages/raes_contracts/manifest_authority.py b/implementations/python/packages/raes_contracts/manifest_authority.py index a8a9226b9..9bba413d8 100644 --- a/implementations/python/packages/raes_contracts/manifest_authority.py +++ b/implementations/python/packages/raes_contracts/manifest_authority.py @@ -37,6 +37,7 @@ # separate authority surfaces and do not belong in this declaration field. BACKEND_SUPPORTED_CONTRACT_IDS = ( "backend-manifest-v2", + "artifact-requirement-v1", "experiment-binding-descriptors-v1", "realization-envelope-v1", "provisioning-plan-v1", diff --git a/implementations/python/packages/raes_contracts/runtime_state.py b/implementations/python/packages/raes_contracts/runtime_state.py index 1ab88d1e4..a6dd50e03 100644 --- a/implementations/python/packages/raes_contracts/runtime_state.py +++ b/implementations/python/packages/raes_contracts/runtime_state.py @@ -16,6 +16,7 @@ from raes_contracts.versions import OPERATION_SCHEMA_VERSION, RUNTIME_SNAPSHOT_SCHEMA_VERSION if TYPE_CHECKING: + from raes_contracts.artifact_requirements import ArtifactSatisfactionDisclosureModel from raes_contracts.contracts import RealizationEnvelopeIdentityModel from raes_contracts.contracts.time_model import TimeRuntimeStateModel @@ -69,6 +70,7 @@ class RealizationProvenanceEntry: explicitness: ExplicitnessClass provenance: ExplicitnessProvenance governing_scope: str | None = None + artifact_satisfaction: ArtifactSatisfactionDisclosureModel | None = None @dataclass diff --git a/implementations/python/packages/raes_contracts/versions.py b/implementations/python/packages/raes_contracts/versions.py index 8ca7db44d..0ac576f79 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_REQUIREMENT_SCHEMA_VERSION = "artifact-requirement/v1" BACKEND_MANIFEST_V2_SCHEMA_VERSION = "backend-manifest/v2" REALIZATION_ENVELOPE_SCHEMA_VERSION = "realization-envelope/v1" PROCESSOR_MANIFEST_V2_SCHEMA_VERSION = "processor-manifest/v2" diff --git a/implementations/python/packages/raes_processor/compiler/realization_requirements.py b/implementations/python/packages/raes_processor/compiler/realization_requirements.py index 1d74f1042..0155db67d 100644 --- a/implementations/python/packages/raes_processor/compiler/realization_requirements.py +++ b/implementations/python/packages/raes_processor/compiler/realization_requirements.py @@ -16,15 +16,104 @@ ) from .addresses import ( _account_address, + _condition_binding_address, _content_address, _domain_controller_address, + _event_address, + _feature_binding_address, _generated_artifact_address, + _inject_address, _network_address, _node_address, _persistent_volume_address, ) +def _append_source_artifact_requirement( + requirements: list[CompiledRealizationRequirement], + *, + source: object, + field_path: str, + address: str, + governing_scope: str, +) -> None: + artifact_requirement = getattr(source, "artifact_requirement", None) + if artifact_requirement is None: + return + requirements.append( + CompiledRealizationRequirement( + field_path=field_path, + address=address, + domain=REALIZATION_DOMAIN, + requirement_kind="source-artifact", + explicitness=artifact_requirement.explicitness, + provenance=ExplicitnessProvenance.AUTHOR_DECLARED, + governing_scope=governing_scope, + artifact_requirement=artifact_requirement, + ) + ) + + +def _append_source_artifact_requirements( + requirements: list[CompiledRealizationRequirement], + scenario: InstantiatedScenario, +) -> None: + """Lower every realized ``Source`` carrier into the existing demand graph.""" + + for name, node in scenario.nodes.items(): + if node.source is not None: + _append_source_artifact_requirement( + requirements, + source=node.source, + field_path=f"nodes.{name}.source.artifact_requirement", + address=_network_address(name) if node.type == NodeType.SWITCH else _node_address(name), + governing_scope=f"#/nodes/{name}/source/artifact_requirement", + ) + for name, content in scenario.content.items(): + if content.source is not None: + _append_source_artifact_requirement( + requirements, + source=content.source, + field_path=f"content.{name}.source.artifact_requirement", + address=_content_address(name), + governing_scope=f"#/content/{name}/source/artifact_requirement", + ) + for node_name, node in scenario.nodes.items(): + for feature_name in node.features: + feature = scenario.features.get(feature_name) + if feature is not None and feature.source is not None: + _append_source_artifact_requirement( + requirements, + source=feature.source, + field_path=f"features.{feature_name}.source.artifact_requirement", + address=_feature_binding_address(node_name, feature_name), + governing_scope=f"#/features/{feature_name}/source/artifact_requirement", + ) + for condition_name in node.conditions: + condition = scenario.conditions.get(condition_name) + if condition is not None and condition.source is not None: + _append_source_artifact_requirement( + requirements, + source=condition.source, + field_path=f"conditions.{condition_name}.source.artifact_requirement", + address=_condition_binding_address(node_name, condition_name), + governing_scope=f"#/conditions/{condition_name}/source/artifact_requirement", + ) + for section, declarations, address_factory in ( + ("injects", scenario.injects, _inject_address), + ("events", scenario.events, _event_address), + ): + for name, declaration in declarations.items(): + if declaration.source is not None: + _append_source_artifact_requirement( + requirements, + source=declaration.source, + field_path=f"{section}.{name}.source.artifact_requirement", + address=address_factory(name), + governing_scope=f"#/{section}/{name}/source/artifact_requirement", + ) + + def _realization_requirement_address( scenario: InstantiatedScenario, *, @@ -195,4 +284,5 @@ def _compile_realization_requirements( _append_domain_topology_requirements(requirements, domain_analysis) _append_stateful_resource_requirements(requirements, scenario) _append_service_materialization_requirements(requirements, scenario) + _append_source_artifact_requirements(requirements, scenario) return tuple(requirements) diff --git a/implementations/python/packages/raes_processor/models/runtime_model.py b/implementations/python/packages/raes_processor/models/runtime_model.py index 0e9f3463b..ff5c1e2fa 100644 --- a/implementations/python/packages/raes_processor/models/runtime_model.py +++ b/implementations/python/packages/raes_processor/models/runtime_model.py @@ -6,6 +6,7 @@ from raes.scenario import InstantiatedScenario 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.diagnostics import Diagnostic from raes_contracts.evaluation import EvaluationExecutionContract, EvaluationResultContract from raes_contracts.planning import EvaluationPlan, OrchestrationPlan, ProvisioningPlan @@ -205,6 +206,9 @@ class ExecutionPlan: orchestration: OrchestrationPlan evaluation: EvaluationPlan diagnostics: list[Diagnostic] = field(default_factory=list) + artifact_availability: ArtifactAvailabilityContext = field( + default_factory=ArtifactAvailabilityContext, + ) @property def is_valid(self) -> bool: diff --git a/implementations/python/packages/raes_processor/planner/core.py b/implementations/python/packages/raes_processor/planner/core.py index 0eb1f0ae5..8daac64d0 100644 --- a/implementations/python/packages/raes_processor/planner/core.py +++ b/implementations/python/packages/raes_processor/planner/core.py @@ -11,12 +11,14 @@ ) from raes_backend_protocols.domain_topology import domain_topology_plan_diagnostics from raes_backend_protocols.service_materialization import service_materialization_plan_diagnostics +from raes_contracts.artifact_requirements import ArtifactAvailabilityContext from raes_contracts.diagnostics import Diagnostic from ..compiler.time_model import time_model_contract_model from ..models import ExecutionPlan, RuntimeModel, RuntimeSnapshot from ..semantics.realization import ( ApparatusRealizationDefaultResolver, + artifact_requirement_diagnostics, materialize_realization_requirements, realization_envelope_diagnostics, realization_support_diagnostics, @@ -88,6 +90,7 @@ def plan( *, target_name: str | None = None, apparatus_realization_default: ApparatusRealizationDefaultResolver | None = None, + artifact_availability: ArtifactAvailabilityContext | None = None, ) -> ExecutionPlan: """Reconcile a compiled runtime model against the current snapshot.""" @@ -117,6 +120,11 @@ def plan( effective_requirements, manifest, ), + *artifact_requirement_diagnostics( + effective_requirements, + manifest, + availability=artifact_availability, + ), *envelope_diagnostics, *_ordering_cycle_diagnostics(resources), ] @@ -150,4 +158,5 @@ def plan( orchestration=orchestration, evaluation=evaluation, diagnostics=diagnostics, + artifact_availability=artifact_availability or ArtifactAvailabilityContext(), ) diff --git a/implementations/python/packages/raes_processor/semantics/artifact_realization.py b/implementations/python/packages/raes_processor/semantics/artifact_realization.py new file mode 100644 index 000000000..e5da3d447 --- /dev/null +++ b/implementations/python/packages/raes_processor/semantics/artifact_realization.py @@ -0,0 +1,363 @@ +"""Portable artifact admission and runtime satisfaction semantics.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from raes.artifact_requirements import ArtifactMechanismProfile, ArtifactRequirement +from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance +from raes_backend_protocols.capabilities import BackendManifest +from raes_contracts.artifact_requirements import ( + ArtifactAvailabilityContext, + ArtifactRequirementAvailability, + ArtifactSatisfactionDisclosureModel, +) +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.vocabulary import RealizationSupportMode + +if TYPE_CHECKING: + from .realization import CompiledRealizationRequirement + +_BACKEND_CONTRACT_INVALID = "runtime.backend-contract-invalid" + + +def artifact_requirement_diagnostics( + requirements: tuple[CompiledRealizationRequirement, ...], + manifest: BackendManifest, + *, + availability: ArtifactAvailabilityContext | None = None, +) -> list[Diagnostic]: + """Validate portable artifact demand against facts and backend routes.""" + + facts = availability or ArtifactAvailabilityContext() + diagnostics: list[Diagnostic] = [] + + for compiled in requirements: + requirement = compiled.artifact_requirement + if requirement is None: + continue + scoped_facts = facts.for_address(compiled.address) + available_digests = set(scoped_facts.available_artifact_digests) + available_candidates = set(scoped_facts.available_candidate_ids) + verified_locked_inputs = set(scoped_facts.verified_locked_input_ids) + satisfied_constraints = set(scoped_facts.satisfied_constraint_ids) + declarations = [ + declaration for declaration in manifest.realization_support if declaration.domain == compiled.domain + ] + + if requirement.explicitness is ExplicitnessClass.OPEN and not any( + declaration.support_mode is RealizationSupportMode.OPEN_REALIZATION for declaration in declarations + ): + diagnostics.append( + _artifact_diagnostic( + compiled, + "artifact.unsupported-open-realization", + "Backend declares no open artifact realization support.", + ) + ) + + if ( + requirement.explicitness is ExplicitnessClass.EXACT + and requirement.exact_artifact is not None + and requirement.exact_artifact.digest not in available_digests + ): + diagnostics.append( + _artifact_diagnostic( + compiled, + "artifact.unavailable-exact-artifact", + "The required immutable artifact is not available.", + ) + ) + + for constraint in requirement.constraints: + if constraint.constraint_id not in satisfied_constraints: + diagnostics.append( + _artifact_diagnostic( + compiled, + "artifact.unsatisfied-constraint", + f"Artifact constraint '{constraint.constraint_id}' is not satisfied.", + ) + ) + + for locked_input in requirement.locked_inputs: + if locked_input.input_id not in verified_locked_inputs: + diagnostics.append( + _artifact_diagnostic( + compiled, + "artifact.missing-locked-input", + f"Locked artifact input '{locked_input.input_id}' is not verified.", + ) + ) + + if requirement.candidates and not any( + candidate.candidate_id in available_candidates for candidate in requirement.candidates + ): + diagnostics.append( + _artifact_diagnostic( + compiled, + "artifact.unavailable-candidate", + "No declared artifact candidate is available.", + ) + ) + + if requirement.materialization_specifications and not any( + specification.digest in scoped_facts.available_materialization_specification_digests + for specification in requirement.materialization_specifications + ): + diagnostics.append( + _artifact_diagnostic( + compiled, + "artifact.unavailable-materialization-specification", + "No declared digest-bound materialization specification is available.", + ) + ) + + supported_routes = { + ( + _artifact_mechanism_key(capability.mechanism), + route.acquisition, + route.timing, + ) + for declaration in declarations + for capability in declaration.artifact_mechanisms + if compiled.requirement_kind in capability.supported_requirement_kinds + for route in capability.supported_routes + } + if not any( + ( + _artifact_mechanism_key(route.mechanism), + route.acquisition, + route.timing, + ) + in supported_routes + for route in requirement.permitted_routes + ): + diagnostics.append( + _artifact_diagnostic( + compiled, + "artifact.unsupported-backend-mechanism", + "Backend declares no supported artifact mechanism and acquisition/timing route.", + ) + ) + return diagnostics + + +def evaluate_artifact_realization( + requirement: CompiledRealizationRequirement, + declared_ops: dict[str, ProvisionOp], + returned_snapshot: RuntimeSnapshot, + *, + manifest: BackendManifest | None, + availability: ArtifactAvailabilityContext | None, +) -> tuple[Diagnostic | None, RealizationProvenanceEntry | None]: + """Validate one returned artifact satisfaction before snapshot admission.""" + + contract = requirement.artifact_requirement + op = declared_ops.get(requirement.address) + if contract is None or op is None or op.action is ChangeAction.DELETE: + return None, None + snapshot_entry = returned_snapshot.entries.get(requirement.address) + payload = snapshot_entry.payload.get("artifact_satisfaction") if snapshot_entry is not None else None + try: + satisfaction = ArtifactSatisfactionDisclosureModel.model_validate(payload) + except (TypeError, ValueError): + return _silent_approximation_diagnostic(requirement), None + + facts = (availability or ArtifactAvailabilityContext()).for_address(requirement.address) + permitted_routes = { + ( + _artifact_mechanism_key(route.mechanism), + route.acquisition, + route.timing, + ) + for route in contract.permitted_routes + } + candidate_ids = {candidate.candidate_id for candidate in contract.candidates} + candidates = {candidate.candidate_id: candidate for candidate in contract.candidates} + specifications = { + specification.specification_id: specification for specification in contract.materialization_specifications + } + selected_candidate = candidates.get(satisfaction.candidate_id) if satisfaction.candidate_id is not None else None + selected_specification = ( + specifications.get(satisfaction.materialization_specification_id) + if satisfaction.materialization_specification_id is not None + else None + ) + selected_mechanism = _artifact_mechanism_key(satisfaction.mechanism) + matching_specifications = { + specification.specification_id + for specification in contract.materialization_specifications + if _artifact_mechanism_key(specification.profile) == selected_mechanism + } + locked_input_ids = {locked_input.input_id for locked_input in contract.locked_inputs} + required_locked_input_ids = ( + set(selected_specification.locked_input_ids) if selected_specification is not None else locked_input_ids + ) + constraint_ids = {constraint.constraint_id for constraint in contract.constraints} + invalid = ( + manifest is None + or satisfaction.backend != manifest.identity + or not _backend_route_supported(requirement, satisfaction, manifest) + or satisfaction.requirement_id != contract.requirement_id + or ( + selected_mechanism, + satisfaction.acquisition, + satisfaction.timing, + ) + not in permitted_routes + or (contract.exact_artifact is not None and satisfaction.artifact != contract.exact_artifact) + or ( + contract.exact_artifact is not None + and contract.exact_artifact.digest not in facts.available_artifact_digests + ) + or (satisfaction.candidate_id is not None and selected_candidate is None) + or (bool(candidate_ids) and satisfaction.candidate_id is None) + or (not candidate_ids and satisfaction.candidate_id is not None) + or ( + selected_candidate is not None + and ( + satisfaction.artifact != selected_candidate.artifact + or selected_candidate.candidate_id not in facts.available_candidate_ids + ) + ) + or (satisfaction.materialization_specification_id is not None and selected_specification is None) + or ( + selected_specification is not None + and _artifact_mechanism_key(selected_specification.profile) != selected_mechanism + ) + or (selected_specification is None and satisfaction.materialization_specification_digest is not None) + or ( + selected_specification is not None + and satisfaction.materialization_specification_digest != selected_specification.digest + ) + or ( + selected_specification is not None + and selected_specification.digest not in facts.available_materialization_specification_digests + ) + or (bool(matching_specifications) and satisfaction.materialization_specification_id is None) + or set(satisfaction.locked_input_ids) != required_locked_input_ids + or not required_locked_input_ids.issubset(facts.verified_locked_input_ids) + or set(satisfaction.satisfied_constraint_ids) != constraint_ids + or not constraint_ids.issubset(facts.satisfied_constraint_ids) + or not _trust_claims_verified(contract, satisfaction, facts) + ) + if invalid: + return _silent_approximation_diagnostic(requirement), None + honoured = contract.explicitness is ExplicitnessClass.EXACT and contract.exact_artifact == satisfaction.artifact + return None, RealizationProvenanceEntry( + address=requirement.address, + field_path=requirement.field_path, + domain=requirement.domain, + requirement_kind=requirement.requirement_kind, + explicitness=requirement.explicitness, + provenance=(requirement.provenance if honoured else ExplicitnessProvenance.BACKEND_REALIZED), + governing_scope=requirement.governing_scope, + artifact_satisfaction=satisfaction, + ) + + +def _artifact_diagnostic( + requirement: CompiledRealizationRequirement, + code: str, + reason: str, +) -> Diagnostic: + return Diagnostic( + code=code, + domain=requirement.domain, + address=requirement.address, + message=( + f"{reason} Requirement '{requirement.requirement_kind}' at '{requirement.field_path}' cannot be admitted." + ), + severity=Severity.ERROR, + ) + + +def _artifact_mechanism_key( + mechanism: ArtifactMechanismProfile, +) -> tuple[str, str, str, str]: + return ( + mechanism.mechanism, + mechanism.profile, + mechanism.version, + mechanism.digest, + ) + + +def _backend_route_supported( + requirement: CompiledRealizationRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, + manifest: BackendManifest, +) -> bool: + selected = ( + _artifact_mechanism_key(satisfaction.mechanism), + satisfaction.acquisition, + satisfaction.timing, + ) + return any( + selected + == ( + _artifact_mechanism_key(capability.mechanism), + route.acquisition, + route.timing, + ) + for declaration in manifest.realization_support + if declaration.domain == requirement.domain + for capability in declaration.artifact_mechanisms + if requirement.requirement_kind in capability.supported_requirement_kinds + for route in capability.supported_routes + ) + + +def _trust_claims_verified( + contract: ArtifactRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, + facts: ArtifactRequirementAvailability, +) -> bool: + required_admission_refs = { + *contract.trust_policy_refs, + *(locked_input.trust_policy_ref for locked_input in contract.locked_inputs), + } + required_evidence_refs = { + *contract.associated_artifact_manifest_refs, + *(locked_input.associated_artifact_manifest_ref for locked_input in contract.locked_inputs), + } + disclosed_integrity = set(satisfaction.integrity_refs) + disclosed_authenticity = set(satisfaction.authenticity_refs) + disclosed_admission = set(satisfaction.admission_refs) + disclosed_provenance = set(satisfaction.provenance_refs) + disclosed_evidence = set(satisfaction.evidence_refs) + return ( + satisfaction.artifact.digest in disclosed_integrity + and disclosed_integrity.issubset(facts.verified_integrity_refs) + and disclosed_authenticity.issubset(facts.verified_authenticity_refs) + and required_admission_refs.issubset(disclosed_admission) + and disclosed_admission.issubset(facts.verified_admission_refs) + and disclosed_provenance.issubset(facts.verified_provenance_refs) + and required_evidence_refs.issubset(disclosed_evidence) + and disclosed_evidence.issubset(facts.verified_evidence_refs) + ) + + +def _silent_approximation_diagnostic( + requirement: CompiledRealizationRequirement, +) -> Diagnostic: + return Diagnostic( + code=_BACKEND_CONTRACT_INVALID, + domain=requirement.domain, + address=requirement.address, + message=( + f"Backend did not realize the exact '{requirement.requirement_kind}' requirement at " + f"'{requirement.field_path}' as the author declared it (the realized value is absent " + "or differs); silent approximation or omission of an exact declaration is forbidden " + "(SEM-218 I2)." + ), + severity=Severity.ERROR, + ) + + +__all__ = [ + "artifact_requirement_diagnostics", + "evaluate_artifact_realization", +] diff --git a/implementations/python/packages/raes_processor/semantics/realization.py b/implementations/python/packages/raes_processor/semantics/realization.py index 8cdcb4859..7dba9fd68 100644 --- a/implementations/python/packages/raes_processor/semantics/realization.py +++ b/implementations/python/packages/raes_processor/semantics/realization.py @@ -6,9 +6,10 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable from dataclasses import dataclass, replace +from raes.artifact_requirements import ArtifactRequirement from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance from raes.realization_envelope import effective_constraints, subsumes, tokenize_path from raes_backend_protocols.capabilities import BackendManifest @@ -17,6 +18,7 @@ DECLARED_CAPABILITY_MATCH_REQUIREMENT_KIND, RUNTIME_REALIZATION_DOMAIN, ) +from raes_contracts.artifact_requirements import ArtifactAvailabilityContext from raes_contracts.diagnostics import Diagnostic, Severity from raes_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp from raes_contracts.realization_envelope import ( @@ -29,12 +31,23 @@ from raes_contracts.runtime_state import RealizationProvenanceEntry, RuntimeSnapshot from raes_contracts.vocabulary import Closure, RealizationSupportMode +from .artifact_realization import ( + artifact_requirement_diagnostics, + evaluate_artifact_realization, +) +from .realization_concerns import ( + CONCERN_PAYLOAD_PATH, + registered_realization_concerns, + resolve_realization_concern, +) + __all__ = [ "CONCERN_PAYLOAD_PATH", "EXACT_REQUIREMENT_KIND", "REALIZATION_DOMAIN", "ApparatusRealizationDefaultResolver", "CompiledRealizationRequirement", + "artifact_requirement_diagnostics", "materialize_realization_requirements", "realization_disclosure", "realization_envelope_diagnostics", @@ -56,34 +69,6 @@ # ``supported_exact_requirement_kinds``; one that cannot must reject (I2). EXACT_REQUIREMENT_KIND = DECLARED_CAPABILITY_MATCH_REQUIREMENT_KIND -# Authored realization concerns mapped onto the published constraint-kind -# vocabulary, keyed by (head section, leaf field) of the classifier path. The -# node/content instance name is the wildcard middle segment. This is the -# realization-concern set the planner already validates against backend -# capabilities — not a general per-field designation authority (that is staged -# under the SEM-218 coverage row). -_CONCERN_KIND_BY_PATH: dict[tuple[str, str], str] = { - ("nodes", "type"): "node-type", - ("nodes", "os"): "os-family", - ("content", "type"): "content-type", -} - -# Where each realization concern's realized value lives inside the backend's -# provisioning resource payload (``resource_payload``). The runtime -# non-approximation gate uses this to locate the value the backend realized for -# an exact concern and compare it against the author declaration. Mirrors the -# concern set in ``_CONCERN_KIND_BY_PATH``; a concern absent here is not gated at -# runtime (no published payload slot to compare). -CONCERN_PAYLOAD_PATH: dict[str, tuple[str, ...]] = { - "os-family": ("os_family",), - "node-type": ("node_type",), - "content-type": ("spec", "type"), - "domain-topology": ("domain_topology",), - "generated-artifact": ("spec",), - "persistent-volume": ("spec",), - "service-content-materialization": ("service_materialization",), -} - @dataclass(frozen=True) class CompiledRealizationRequirement: @@ -102,11 +87,17 @@ class CompiledRealizationRequirement: provenance: ExplicitnessProvenance governing_scope: str | None = None delegated: bool = False + artifact_requirement: ArtifactRequirement | 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.artifact_requirement is not None: + if self.requirement_kind != "source-artifact": + raise ValueError("artifact_requirement requires requirement_kind='source-artifact'") + if self.explicitness is not self.artifact_requirement.explicitness: + raise ValueError("compiled artifact requirement explicitness must match its source contract") ApparatusRealizationDefaultResolver = Callable[ @@ -157,39 +148,6 @@ def materialize_realization_requirements( return tuple(materialized) -def registered_realization_concerns( - *, - declaration_names: Mapping[str, Iterable[str]], -) -> tuple[tuple[str, str, str, str], ...]: - """Enumerate ``(section, declaration, leaf, kind)`` registrations.""" - - return tuple( - (section, declaration_name, leaf_field, concern_kind) - for (section, leaf_field), concern_kind in _CONCERN_KIND_BY_PATH.items() - for declaration_name in declaration_names.get(section, ()) - ) - - -def resolve_realization_concern( - field_path: str, - *, - declaration_names: Mapping[str, Iterable[str]], -) -> str | None: - """Return the realization concern kind for a classifier path, or ``None``. - - Only the concerns the planner validates against backend capabilities map to - a kind today; every other authored field is not a realization concern with - a published kind and yields ``None``. - """ - - for section, declaration_name, leaf_field, concern_kind in registered_realization_concerns( - declaration_names=declaration_names - ): - if field_path == f"{section}.{declaration_name}.{leaf_field}": - return concern_kind - return None - - def realization_support_diagnostics( requirements: tuple[CompiledRealizationRequirement, ...], manifest: BackendManifest, @@ -370,6 +328,9 @@ def realization_disclosure( requirements: tuple[CompiledRealizationRequirement, ...], declared_plan: ProvisioningPlan, returned_snapshot: RuntimeSnapshot, + *, + manifest: BackendManifest | None = None, + artifact_availability: ArtifactAvailabilityContext | None = None, ) -> tuple[list[Diagnostic], tuple[RealizationProvenanceEntry, ...]]: """SEM-218 runtime non-approximation gate (I2) + provenance disclosure (I5). @@ -405,7 +366,16 @@ def realization_disclosure( provenance: list[RealizationProvenanceEntry] = [] declared_ops = {op.address: op for op in declared_plan.operations} for requirement in requirements: - diagnostic, entry = _evaluate_realization(requirement, declared_ops, returned_snapshot) + if requirement.artifact_requirement is not None: + diagnostic, entry = evaluate_artifact_realization( + requirement, + declared_ops, + returned_snapshot, + manifest=manifest, + availability=artifact_availability, + ) + else: + diagnostic, entry = _evaluate_realization(requirement, declared_ops, returned_snapshot) if diagnostic is not None: diagnostics.append(diagnostic) if entry is not None: diff --git a/implementations/python/packages/raes_processor/semantics/realization_concerns.py b/implementations/python/packages/raes_processor/semantics/realization_concerns.py new file mode 100644 index 000000000..110b074f9 --- /dev/null +++ b/implementations/python/packages/raes_processor/semantics/realization_concerns.py @@ -0,0 +1,54 @@ +"""Registered realization concern kinds and runtime payload locations.""" + +from collections.abc import Iterable, Mapping + +_CONCERN_KIND_BY_PATH: dict[tuple[str, str], str] = { + ("nodes", "type"): "node-type", + ("nodes", "os"): "os-family", + ("content", "type"): "content-type", +} + +CONCERN_PAYLOAD_PATH: dict[str, tuple[str, ...]] = { + "os-family": ("os_family",), + "node-type": ("node_type",), + "content-type": ("spec", "type"), + "domain-topology": ("domain_topology",), + "generated-artifact": ("spec",), + "persistent-volume": ("spec",), + "service-content-materialization": ("service_materialization",), +} + + +def registered_realization_concerns( + *, + declaration_names: Mapping[str, Iterable[str]], +) -> tuple[tuple[str, str, str, str], ...]: + """Enumerate ``(section, declaration, leaf, kind)`` registrations.""" + + return tuple( + (section, declaration_name, leaf_field, concern_kind) + for (section, leaf_field), concern_kind in _CONCERN_KIND_BY_PATH.items() + for declaration_name in declaration_names.get(section, ()) + ) + + +def resolve_realization_concern( + field_path: str, + *, + declaration_names: Mapping[str, Iterable[str]], +) -> str | None: + """Return the registered realization concern kind for a classifier path.""" + + for section, declaration_name, leaf_field, concern_kind in registered_realization_concerns( + declaration_names=declaration_names + ): + if field_path == f"{section}.{declaration_name}.{leaf_field}": + return concern_kind + return None + + +__all__ = [ + "CONCERN_PAYLOAD_PATH", + "registered_realization_concerns", + "resolve_realization_concern", +] diff --git a/implementations/python/packages/raes_runtime/backend_calls.py b/implementations/python/packages/raes_runtime/backend_calls.py index 35502813b..001fc9a24 100644 --- a/implementations/python/packages/raes_runtime/backend_calls.py +++ b/implementations/python/packages/raes_runtime/backend_calls.py @@ -5,7 +5,9 @@ from collections.abc import Callable, Iterable from copy import deepcopy +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.time_model import validate_time_runtime_transition from raes_contracts.diagnostics import Diagnostic from raes_contracts.planning import ProvisioningPlan @@ -53,6 +55,8 @@ def _call_backend_apply( snapshot: RuntimeSnapshot, realization_requirements: tuple[CompiledRealizationRequirement, ...] = (), realization_plan: ProvisioningPlan | None = None, + backend_manifest: BackendManifest | None = None, + artifact_availability: ArtifactAvailabilityContext | None = None, ) -> ApplyResult: baseline_snapshot = deepcopy(snapshot) backend_snapshot = deepcopy(snapshot) @@ -72,6 +76,8 @@ def _call_backend_apply( baseline_snapshot=baseline_snapshot, realization_requirements=realization_requirements, realization_plan=realization_plan, + backend_manifest=backend_manifest, + artifact_availability=artifact_availability, ) @@ -82,6 +88,8 @@ def _finalize_backend_apply( baseline_snapshot: RuntimeSnapshot, realization_requirements: tuple[CompiledRealizationRequirement, ...], realization_plan: ProvisioningPlan | None, + backend_manifest: BackendManifest | None, + artifact_availability: ArtifactAvailabilityContext | None, ) -> ApplyResult: """Validate a backend's apply result and gate its realized snapshot. @@ -112,6 +120,8 @@ def _finalize_backend_apply( realization_requirements, realization_plan, result.snapshot, + manifest=backend_manifest, + artifact_availability=artifact_availability, ) if contract_diagnostics: return ApplyResult(success=False, snapshot=baseline_snapshot, diagnostics=contract_diagnostics) 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 30caabe38..2e685ea22 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_models.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_models.py @@ -182,6 +182,11 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "explicitness": entry.explicitness.value, "provenance": entry.provenance.value, "governing_scope": entry.governing_scope, + **( + {"artifact_satisfaction": entry.artifact_satisfaction.model_dump(mode="json")} + if entry.artifact_satisfaction is not None + else {} + ), } for entry in snapshot.realization_provenance ], diff --git a/implementations/python/packages/raes_runtime/control_plane_store.py b/implementations/python/packages/raes_runtime/control_plane_store.py index 13326329e..ceab25b39 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store.py +++ b/implementations/python/packages/raes_runtime/control_plane_store.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Protocol +from raes_contracts.artifact_requirements import ArtifactSatisfactionDisclosureModel from raes_contracts.contracts import RealizationEnvelopeIdentityModel from raes_contracts.contracts.time_model import TimeRuntimeStateModel from raes_contracts.diagnostics import Diagnostic, Severity @@ -153,6 +154,11 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: "explicitness": entry.explicitness.value, "provenance": entry.provenance.value, "governing_scope": entry.governing_scope, + "artifact_satisfaction": ( + entry.artifact_satisfaction.model_dump(mode="json") + if entry.artifact_satisfaction is not None + else None + ), } for entry in snapshot.realization_provenance ], @@ -224,6 +230,11 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: str(item.get("provenance", ExplicitnessProvenance.AUTHOR_DECLARED.value)) ), governing_scope=(str(item["governing_scope"]) if item.get("governing_scope") is not None else None), + artifact_satisfaction=( + ArtifactSatisfactionDisclosureModel.model_validate(item["artifact_satisfaction"]) + if item.get("artifact_satisfaction") is not None + else None + ), ) for item in payload.get("realization_provenance", []) if isinstance(item, dict) diff --git a/implementations/python/packages/raes_runtime/manager.py b/implementations/python/packages/raes_runtime/manager.py index 98d8ce5ca..63502cd52 100644 --- a/implementations/python/packages/raes_runtime/manager.py +++ b/implementations/python/packages/raes_runtime/manager.py @@ -3,6 +3,7 @@ from collections.abc import Iterable from dataclasses import dataclass +from raes_contracts.artifact_requirements import ArtifactAvailabilityContext from raes_contracts.contracts import ExperimentStochasticControlModel from raes_contracts.contracts.time_model import TimeModelDeclarationModel from raes_contracts.diagnostics import Diagnostic @@ -117,6 +118,7 @@ def plan( *, parameters: dict[str, object] | None = None, profile: str | None = None, + artifact_availability: ArtifactAvailabilityContext | None = None, ) -> ExecutionPlan: model = compile_scenario_runtime_model(scenario, parameters=parameters, profile=profile) effective_snapshot = snapshot if snapshot is not None else self._snapshot @@ -125,6 +127,7 @@ def plan( self._target.manifest, effective_snapshot, target_name=self._target.name, + artifact_availability=artifact_availability, ) def apply(self, execution_plan: ExecutionPlan) -> ApplyResult: @@ -194,6 +197,8 @@ def _apply_provisioning_phase( snapshot=state.working_snapshot, realization_requirements=execution_plan.model.realization_requirements, realization_plan=execution_plan.provisioning, + backend_manifest=execution_plan.manifest, + artifact_availability=execution_plan.artifact_availability, ) self._record_phase_result(state, provision_result) if not provision_result.success: diff --git a/implementations/python/tests/test_artifact_requirement_satisfaction.py b/implementations/python/tests/test_artifact_requirement_satisfaction.py new file mode 100644 index 000000000..f45c01b78 --- /dev/null +++ b/implementations/python/tests/test_artifact_requirement_satisfaction.py @@ -0,0 +1,1042 @@ +"""Portable artifact requirement, capability, and satisfaction contracts (#920).""" + +from __future__ import annotations + +import json +from dataclasses import replace +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from pydantic import ValidationError +from raes.artifact_requirements import ( + ArtifactCandidate, + ArtifactConstraint, + ArtifactIdentity, + ArtifactLockedInput, + ArtifactMaterializationSpecification, + ArtifactMechanismProfile, + ArtifactRequirement, + ArtifactSatisfactionRoute, + Source, +) +from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance +from raes.parser import parse_sdl +from raes_backend_protocols.capabilities import BackendManifest +from raes_backend_stubs.stubs import create_stub_manifest +from raes_contracts.apparatus import ApparatusIdentity +from raes_contracts.artifact_requirements import ( + ARTIFACT_REQUIREMENT_SCHEMA_VERSION, + ArtifactAvailabilityContext, + ArtifactMechanismCapability, + ArtifactRequirementAvailability, + ArtifactRequirementContractModel, + ArtifactSatisfactionDisclosureModel, + artifact_requirement_invariant_violations, +) +from raes_contracts.contracts import schema_bundle +from raes_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp, RuntimeDomain +from raes_contracts.runtime_state import RealizationProvenanceEntry, RuntimeSnapshot, SnapshotEntry +from raes_contracts.vocabulary import RealizationSupportMode +from raes_processor.compiler import compile_runtime_model +from raes_processor.models import CompiledRealizationRequirement +from raes_processor.planner.core import plan +from raes_processor.semantics.realization import ( + artifact_requirement_diagnostics, + realization_disclosure, +) +from raes_runtime.control_plane_store import _snapshot_from_payload, _snapshot_payload + +_DIGEST_A = "sha256:" + "a" * 64 +_DIGEST_B = "sha256:" + "b" * 64 +_PROFILE_DIGEST = "sha256:" + "c" * 64 +_ADDRESS = "provision.node.web" +_REPO_ROOT = Path(__file__).resolve().parents[3] +_FIXTURE_ROOT = _REPO_ROOT / "contracts" / "fixtures" / "artifact-requirements" / "artifact-requirement-v1" + + +def _identity(*, digest: str = _DIGEST_A) -> ArtifactIdentity: + return ArtifactIdentity( + artifact_id="ubuntu-server", + version="24.04.1", + digest=digest, + media_type="application/vnd.oci.image.manifest.v1+json", + ) + + +def _mechanism(name: str = "exact-artifact") -> ArtifactMechanismProfile: + return ArtifactMechanismProfile( + mechanism=name, + profile="raes-artifact-satisfaction", + version="1", + digest=_PROFILE_DIGEST, + ) + + +def _route( + *, + mechanism: ArtifactMechanismProfile | None = None, + acquisition: str = "pull", + timing: str = "realization", +) -> ArtifactSatisfactionRoute: + return ArtifactSatisfactionRoute( + mechanism=mechanism or _mechanism(), + acquisition=acquisition, + timing=timing, + ) + + +def _exact_requirement() -> ArtifactRequirement: + return ArtifactRequirement( + requirement_id="web-image", + explicitness=ExplicitnessClass.EXACT, + exact_artifact=_identity(), + permitted_routes=[_route()], + trust_policy_refs=["reusable-asset-trust-policy-v1#reusable_scenario"], + ) + + +def _compiled( + requirement: ArtifactRequirement, + *, + address: str = _ADDRESS, +) -> CompiledRealizationRequirement: + return CompiledRealizationRequirement( + field_path="nodes.web.source.artifact_requirement", + address=address, + domain="runtime-realization", + requirement_kind="source-artifact", + explicitness=requirement.explicitness, + provenance=ExplicitnessProvenance.AUTHOR_DECLARED, + governing_scope="#/nodes/web/source/artifact_requirement", + artifact_requirement=requirement, + ) + + +def _availability( + *, + address: str = _ADDRESS, + available_artifact_digests: list[str] | None = None, + available_candidate_ids: list[str] | None = None, + verified_locked_input_ids: list[str] | None = None, + satisfied_constraint_ids: list[str] | None = None, + available_materialization_specification_digests: list[str] | None = None, + verified_integrity_refs: list[str] | None = None, + verified_authenticity_refs: list[str] | None = None, + verified_admission_refs: list[str] | None = None, + verified_provenance_refs: list[str] | None = None, + verified_evidence_refs: list[str] | None = None, +) -> ArtifactAvailabilityContext: + return ArtifactAvailabilityContext( + requirements=[ + ArtifactRequirementAvailability( + address=address, + available_artifact_digests=available_artifact_digests or [], + available_candidate_ids=available_candidate_ids or [], + verified_locked_input_ids=verified_locked_input_ids or [], + satisfied_constraint_ids=satisfied_constraint_ids or [], + available_materialization_specification_digests=(available_materialization_specification_digests or []), + verified_integrity_refs=verified_integrity_refs or [], + verified_authenticity_refs=verified_authenticity_refs or [], + verified_admission_refs=verified_admission_refs or [], + verified_provenance_refs=verified_provenance_refs or [], + verified_evidence_refs=verified_evidence_refs or [], + ) + ] + ) + + +def _capability( + *, + mechanism: ArtifactMechanismProfile | None = None, + acquisition: str = "pull", + timing: str = "realization", +) -> ArtifactMechanismCapability: + return ArtifactMechanismCapability( + mechanism=mechanism or _mechanism(), + supported_requirement_kinds=["source-artifact"], + supported_routes=[ + { + "acquisition": acquisition, + "timing": timing, + } + ], + ) + + +def _manifest( + requirement: ArtifactRequirement, + *, + capability: ArtifactMechanismCapability | None = None, +) -> BackendManifest: + manifest = create_stub_manifest() + declarations = [] + for declaration in manifest.realization_support: + if declaration.domain != "runtime-realization": + declarations.append(declaration) + continue + declarations.append( + replace( + declaration, + artifact_mechanisms=(capability or _capability(),), + supported_constraint_kinds=declaration.supported_constraint_kinds | frozenset({"source-artifact"}), + ) + ) + return replace(manifest, realization_support=tuple(declarations)) + + +def test_exact_requirement_requires_one_immutable_identity_and_no_alternatives() -> None: + source = Source( + name="ubuntu-server", + version="24.04.1", + artifact_requirement=_exact_requirement(), + ) + assert source.artifact_requirement is not None + assert source.artifact_requirement.explicitness is ExplicitnessClass.EXACT + + with pytest.raises(ValidationError, match="immutable|exact"): + ArtifactRequirement( + requirement_id="bad-exact", + explicitness=ExplicitnessClass.EXACT, + permitted_routes=[_route()], + ) + with pytest.raises(ValidationError, match="alternative|candidate|exact"): + ArtifactRequirement( + requirement_id="bad-fallback", + explicitness=ExplicitnessClass.EXACT, + exact_artifact=_identity(), + candidates=[ArtifactCandidate(candidate_id="fallback", artifact=_identity(digest=_DIGEST_B))], + permitted_routes=[_route()], + ) + with pytest.raises(ValidationError, match="selector|identity|match"): + Source( + name="different-image", + version="24.04.1", + artifact_requirement=_exact_requirement(), + ) + + +def test_constrained_open_and_absent_postures_are_independent_of_transport() -> None: + constrained = ArtifactRequirement( + requirement_id="candidate-image", + explicitness=ExplicitnessClass.CONSTRAINED, + constraints=[ + ArtifactConstraint( + constraint_id="linux-image", + kind="artifact-class", + allowed_values=["linux-vm-image"], + ) + ], + candidates=[ArtifactCandidate(candidate_id="candidate-a", artifact=_identity())], + permitted_routes=[ + _route(acquisition="local-lookup", timing="backend-preparation"), + _route( + mechanism=_mechanism("published-candidate"), + acquisition="import", + timing="pack-ingestion", + ), + ], + ) + opened = ArtifactRequirement( + requirement_id="backend-selected", + explicitness=ExplicitnessClass.OPEN, + permitted_routes=[ + _route( + mechanism=_mechanism("dynamic-composition"), + acquisition="none", + timing="realization", + ) + ], + ) + + assert {route.acquisition for route in constrained.permitted_routes} == { + "local-lookup", + "import", + } + assert opened.permitted_routes[0].mechanism.mechanism == "dynamic-composition" + assert Source(name="legacy", version="*").artifact_requirement is None + + +def test_materialization_and_locked_inputs_are_explicit_constrained_authority() -> None: + locked = ArtifactLockedInput( + input_id="rootfs", + artifact=_identity(), + associated_artifact_manifest_ref="associated-artifact-manifest-v1:rootfs", + trust_policy_ref="reusable-asset-trust-policy-v1#associated_artifact_set", + ) + specification = ArtifactMaterializationSpecification( + specification_id="cloud-image", + profile=_mechanism("materialization-specification"), + digest=_PROFILE_DIGEST, + locked_input_ids=["rootfs"], + ) + requirement = ArtifactRequirement( + requirement_id="materialized-image", + explicitness=ExplicitnessClass.CONSTRAINED, + locked_inputs=[locked], + materialization_specifications=[specification], + permitted_routes=[ + _route( + mechanism=_mechanism("materialization-specification"), + acquisition="none", + timing="backend-preparation", + ) + ], + ) + assert requirement.materialization_specifications[0].locked_input_ids == ["rootfs"] + + with pytest.raises(ValidationError, match="locked input"): + ArtifactRequirement( + requirement_id="missing-input-ref", + explicitness=ExplicitnessClass.CONSTRAINED, + materialization_specifications=[specification], + permitted_routes=[_route(mechanism=_mechanism("materialization-specification"))], + ) + + +def test_mechanisms_are_governed_extensible_profiles_not_a_closed_union() -> None: + extension = _mechanism("x-acme:golden-image-compose") + requirement = ArtifactRequirement( + requirement_id="extension", + explicitness=ExplicitnessClass.OPEN, + permitted_routes=[_route(mechanism=extension, acquisition="none")], + ) + assert requirement.permitted_routes[0].mechanism == extension + + with pytest.raises(ValidationError, match="mechanism"): + _mechanism("arbitrary ungoverned mechanism") + + +@pytest.mark.parametrize( + ("requirement", "context", "manifest_mutator", "expected_code"), + [ + ( + _exact_requirement(), + _availability(), + None, + "artifact.unavailable-exact-artifact", + ), + ( + ArtifactRequirement( + requirement_id="constraint", + explicitness=ExplicitnessClass.CONSTRAINED, + constraints=[ + ArtifactConstraint( + constraint_id="linux", + kind="artifact-class", + allowed_values=["linux-vm-image"], + ) + ], + permitted_routes=[_route()], + ), + _availability(available_artifact_digests=[_DIGEST_A]), + None, + "artifact.unsatisfied-constraint", + ), + ( + ArtifactRequirement( + requirement_id="open", + explicitness=ExplicitnessClass.OPEN, + permitted_routes=[_route(mechanism=_mechanism("dynamic-composition"))], + ), + _availability(), + "exact-only", + "artifact.unsupported-open-realization", + ), + ( + ArtifactRequirement( + requirement_id="locked", + explicitness=ExplicitnessClass.CONSTRAINED, + locked_inputs=[ + ArtifactLockedInput( + input_id="rootfs", + artifact=_identity(), + associated_artifact_manifest_ref="associated-artifact-manifest-v1:rootfs", + trust_policy_ref="reusable-asset-trust-policy-v1#associated_artifact_set", + ) + ], + permitted_routes=[_route()], + ), + _availability(available_artifact_digests=[_DIGEST_A]), + None, + "artifact.missing-locked-input", + ), + ( + ArtifactRequirement( + requirement_id="candidate", + explicitness=ExplicitnessClass.CONSTRAINED, + candidates=[ArtifactCandidate(candidate_id="candidate-a", artifact=_identity())], + permitted_routes=[_route(mechanism=_mechanism("published-candidate"))], + ), + _availability(), + None, + "artifact.unavailable-candidate", + ), + ( + ArtifactRequirement( + requirement_id="materialization", + explicitness=ExplicitnessClass.CONSTRAINED, + materialization_specifications=[ + ArtifactMaterializationSpecification( + specification_id="cloud-image", + profile=_mechanism("materialization-specification"), + digest=_PROFILE_DIGEST, + ) + ], + permitted_routes=[_route(mechanism=_mechanism("materialization-specification"))], + ), + _availability(), + None, + "artifact.unavailable-materialization-specification", + ), + ( + ArtifactRequirement( + requirement_id="mechanism", + explicitness=ExplicitnessClass.OPEN, + permitted_routes=[_route(mechanism=_mechanism("x-acme:composer"), acquisition="none")], + ), + _availability(), + "different-mechanism", + "artifact.unsupported-backend-mechanism", + ), + ], +) +def test_realizability_failures_have_stable_distinct_diagnostics( + requirement: ArtifactRequirement, + context: ArtifactAvailabilityContext, + manifest_mutator: str | None, + expected_code: str, +) -> None: + manifest = _manifest(requirement) + if manifest_mutator == "exact-only": + declarations = tuple( + replace( + declaration, + support_mode=RealizationSupportMode.EXACT_ONLY, + supported_constraint_kinds=frozenset(), + ) + if declaration.domain == "runtime-realization" + else declaration + for declaration in manifest.realization_support + ) + manifest = replace(manifest, realization_support=declarations) + elif manifest_mutator == "different-mechanism": + manifest = _manifest(requirement, capability=_capability(mechanism=_mechanism("dynamic-composition"))) + + codes = { + diagnostic.code + for diagnostic in artifact_requirement_diagnostics( + (_compiled(requirement),), + manifest, + availability=context, + ) + } + assert expected_code in codes + + +def test_backend_capability_matrix_does_not_claim_cartesian_product() -> None: + requirement = ArtifactRequirement( + requirement_id="prepared", + explicitness=ExplicitnessClass.CONSTRAINED, + candidates=[ArtifactCandidate(candidate_id="candidate-a", artifact=_identity())], + permitted_routes=[ + _route( + mechanism=_mechanism("published-candidate"), + acquisition="import", + timing="backend-preparation", + ) + ], + ) + capability = _capability( + mechanism=_mechanism("published-candidate"), + acquisition="pull", + timing="publication", + ) + diagnostics = artifact_requirement_diagnostics( + (_compiled(requirement),), + _manifest(requirement, capability=capability), + availability=_availability( + available_candidate_ids=["candidate-a"], + available_artifact_digests=[_DIGEST_A], + ), + ) + assert {diagnostic.code for diagnostic in diagnostics} == {"artifact.unsupported-backend-mechanism"} + + +def test_requirement_local_availability_ids_cannot_collide_across_addresses() -> None: + requirement = ArtifactRequirement( + requirement_id="candidate", + explicitness=ExplicitnessClass.CONSTRAINED, + candidates=[ArtifactCandidate(candidate_id="fallback", artifact=_identity())], + permitted_routes=[_route(mechanism=_mechanism("published-candidate"))], + ) + first = _compiled(requirement, address="provision.node.first") + second = _compiled(requirement, address="provision.node.second") + facts = _availability( + address=first.address, + available_candidate_ids=["fallback"], + ) + + diagnostics = artifact_requirement_diagnostics( + (first, second), + _manifest(requirement), + availability=facts, + ) + + assert [ + diagnostic.address for diagnostic in diagnostics if diagnostic.code == "artifact.unavailable-candidate" + ] == [second.address] + + +def test_source_artifact_requirement_lowers_into_existing_compiled_demand() -> None: + scenario = parse_sdl( + f""" +name: portable-artifact +nodes: + web: + type: vm + os: linux + source: + name: ubuntu-server + version: 24.04.1 + artifact_requirement: + requirement_id: web-image + explicitness: exact + exact_artifact: + artifact_id: ubuntu-server + version: 24.04.1 + digest: {_DIGEST_A} + media_type: application/vnd.oci.image.manifest.v1+json + permitted_routes: + - mechanism: + mechanism: exact-artifact + profile: raes-artifact-satisfaction + version: "1" + digest: {_PROFILE_DIGEST} + acquisition: pull + timing: realization + trust_policy_refs: + - reusable-asset-trust-policy-v1#reusable_scenario +""" + ) + compiled = compile_runtime_model(scenario) + requirements = [ + requirement for requirement in compiled.realization_requirements if requirement.artifact_requirement is not None + ] + assert len(requirements) == 1 + assert requirements[0].address == _ADDRESS + assert requirements[0].artifact_requirement == _exact_requirement() + + +def test_planner_threads_scoped_artifact_availability_into_admission() -> None: + scenario = parse_sdl( + f""" +name: portable-artifact-plan +nodes: + web: + type: vm + os: linux + source: + name: ubuntu-server + version: 24.04.1 + artifact_requirement: + requirement_id: web-image + explicitness: exact + exact_artifact: + artifact_id: ubuntu-server + version: 24.04.1 + digest: {_DIGEST_A} + media_type: application/vnd.oci.image.manifest.v1+json + permitted_routes: + - mechanism: + mechanism: exact-artifact + profile: raes-artifact-satisfaction + version: "1" + digest: {_PROFILE_DIGEST} + acquisition: pull + timing: realization + trust_policy_refs: + - reusable-asset-trust-policy-v1#reusable_scenario +""" + ) + model = compile_runtime_model(scenario) + manifest = _manifest(_exact_requirement()) + + unavailable = plan( + model, + manifest, + artifact_availability=_availability(), + ) + available = plan( + model, + manifest, + artifact_availability=_availability( + available_artifact_digests=[_DIGEST_A], + ), + ) + + assert "artifact.unavailable-exact-artifact" in {diagnostic.code for diagnostic in unavailable.diagnostics} + assert "artifact.unavailable-exact-artifact" not in {diagnostic.code for diagnostic in available.diagnostics} + + +def test_exact_runtime_satisfaction_rejects_substitution_and_discloses_provenance() -> None: + requirement = _exact_requirement() + compiled = _compiled(requirement) + plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=_ADDRESS, + resource_type="node", + payload={"artifact_requirement": requirement.model_dump(mode="json")}, + ) + ] + ) + manifest = _manifest(requirement) + trust_policy_ref = "reusable-asset-trust-policy-v1#reusable_scenario" + wrong = ArtifactSatisfactionDisclosureModel( + requirement_id="web-image", + artifact=_identity(digest=_DIGEST_B), + mechanism=_mechanism(), + acquisition="pull", + timing="realization", + backend=manifest.identity, + integrity_refs=["sha256:" + "b" * 64], + admission_refs=[trust_policy_ref], + provenance_refs=["provenance:stub"], + ) + snapshot = RuntimeSnapshot( + entries={ + _ADDRESS: SnapshotEntry( + address=_ADDRESS, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={"artifact_satisfaction": wrong.model_dump(mode="json")}, + ) + } + ) + + availability = _availability( + available_artifact_digests=[_DIGEST_A], + verified_integrity_refs=[_DIGEST_A, _DIGEST_B], + verified_admission_refs=[trust_policy_ref], + verified_provenance_refs=["provenance:stub"], + ) + diagnostics, provenance = realization_disclosure( + (compiled,), + plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + assert {diagnostic.code for diagnostic in diagnostics} == {"runtime.backend-contract-invalid"} + assert provenance == () + + correct = wrong.model_copy( + update={ + "artifact": _identity(), + "integrity_refs": [_DIGEST_A], + } + ) + snapshot.entries[_ADDRESS].payload["artifact_satisfaction"] = correct.model_dump(mode="json") + diagnostics, provenance = realization_disclosure( + (compiled,), + plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + assert diagnostics == [] + assert len(provenance) == 1 + assert provenance[0].artifact_satisfaction == correct + assert provenance[0].provenance is ExplicitnessProvenance.AUTHOR_DECLARED + + +def test_runtime_rejects_route_not_admitted_by_selected_manifest() -> None: + requirement = _exact_requirement() + compiled = _compiled(requirement) + declared_plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=_ADDRESS, + resource_type="node", + payload={"artifact_requirement": requirement.model_dump(mode="json")}, + ) + ] + ) + manifest = _manifest( + requirement, + capability=_capability(acquisition="copy"), + ) + trust_policy_ref = "reusable-asset-trust-policy-v1#reusable_scenario" + disclosure = ArtifactSatisfactionDisclosureModel( + requirement_id=requirement.requirement_id, + artifact=_identity(), + mechanism=_mechanism(), + acquisition="pull", + timing="realization", + backend=manifest.identity, + integrity_refs=[_DIGEST_A], + admission_refs=[trust_policy_ref], + provenance_refs=["provenance:verified"], + ) + snapshot = RuntimeSnapshot( + entries={ + _ADDRESS: SnapshotEntry( + address=_ADDRESS, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={ + "artifact_satisfaction": disclosure.model_dump(mode="json"), + }, + ) + } + ) + availability = _availability( + available_artifact_digests=[_DIGEST_A], + verified_integrity_refs=[_DIGEST_A], + verified_admission_refs=[trust_policy_ref], + verified_provenance_refs=["provenance:verified"], + ) + + diagnostics, provenance = realization_disclosure( + (compiled,), + declared_plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + + assert {diagnostic.code for diagnostic in diagnostics} == {"runtime.backend-contract-invalid"} + assert provenance == () + + +def test_runtime_rejects_backend_claims_absent_from_verified_trust_context() -> None: + requirement = _exact_requirement() + compiled = _compiled(requirement) + declared_plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=_ADDRESS, + resource_type="node", + payload={"artifact_requirement": requirement.model_dump(mode="json")}, + ) + ] + ) + manifest = _manifest(requirement) + trust_policy_ref = "reusable-asset-trust-policy-v1#reusable_scenario" + disclosure = ArtifactSatisfactionDisclosureModel( + requirement_id=requirement.requirement_id, + artifact=_identity(), + mechanism=_mechanism(), + acquisition="pull", + timing="realization", + backend=manifest.identity, + integrity_refs=[_DIGEST_A], + admission_refs=[trust_policy_ref], + provenance_refs=["provenance:unverified"], + ) + snapshot = RuntimeSnapshot( + entries={ + _ADDRESS: SnapshotEntry( + address=_ADDRESS, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={ + "artifact_satisfaction": disclosure.model_dump(mode="json"), + }, + ) + } + ) + availability = _availability( + available_artifact_digests=[_DIGEST_A], + verified_integrity_refs=[_DIGEST_A], + verified_admission_refs=[trust_policy_ref], + ) + + diagnostics, provenance = realization_disclosure( + (compiled,), + declared_plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + + assert {diagnostic.code for diagnostic in diagnostics} == {"runtime.backend-contract-invalid"} + assert provenance == () + + +def test_runtime_binds_candidate_backend_route_and_verified_evidence() -> None: + requirement = ArtifactRequirement( + requirement_id="candidate-image", + explicitness=ExplicitnessClass.CONSTRAINED, + candidates=[ArtifactCandidate(candidate_id="candidate-a", artifact=_identity())], + permitted_routes=[_route(mechanism=_mechanism("published-candidate"))], + ) + compiled = _compiled(requirement) + declared_plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=_ADDRESS, + resource_type="node", + payload={"artifact_requirement": requirement.model_dump(mode="json")}, + ) + ] + ) + manifest = _manifest( + requirement, + capability=_capability(mechanism=_mechanism("published-candidate")), + ) + disclosure = ArtifactSatisfactionDisclosureModel( + requirement_id=requirement.requirement_id, + artifact=_identity(digest=_DIGEST_B), + mechanism=_mechanism("published-candidate"), + acquisition="pull", + timing="realization", + backend=manifest.identity, + candidate_id="candidate-a", + integrity_refs=[_DIGEST_B], + provenance_refs=["provenance:verified"], + ) + snapshot = RuntimeSnapshot( + entries={ + _ADDRESS: SnapshotEntry( + address=_ADDRESS, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={ + "artifact_satisfaction": disclosure.model_dump(mode="json"), + }, + ) + } + ) + availability = _availability( + available_candidate_ids=["candidate-a"], + verified_integrity_refs=[_DIGEST_A, _DIGEST_B], + verified_provenance_refs=["provenance:verified"], + ) + + diagnostics, _ = realization_disclosure( + (compiled,), + declared_plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + assert {diagnostic.code for diagnostic in diagnostics} == {"runtime.backend-contract-invalid"} + + valid = disclosure.model_copy( + update={ + "artifact": _identity(), + "integrity_refs": [_DIGEST_A], + } + ) + snapshot.entries[_ADDRESS].payload["artifact_satisfaction"] = valid.model_dump(mode="json") + diagnostics, provenance = realization_disclosure( + (compiled,), + declared_plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + assert diagnostics == [] + assert provenance[0].artifact_satisfaction == valid + + wrong_backend = valid.model_copy(update={"backend": ApparatusIdentity(name="forged", version="1")}) + snapshot.entries[_ADDRESS].payload["artifact_satisfaction"] = wrong_backend.model_dump(mode="json") + diagnostics, _ = realization_disclosure( + (compiled,), + declared_plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + assert {diagnostic.code for diagnostic in diagnostics} == {"runtime.backend-contract-invalid"} + + +def test_runtime_binds_materialization_selection_to_verified_specification_digest() -> None: + trust_policy_ref = "reusable-asset-trust-policy-v1#associated_artifact_set" + manifest_ref = "associated-artifact-manifest-v1:rootfs" + requirement = ArtifactRequirement( + requirement_id="materialized-image", + explicitness=ExplicitnessClass.CONSTRAINED, + locked_inputs=[ + ArtifactLockedInput( + input_id="rootfs", + artifact=_identity(), + associated_artifact_manifest_ref=manifest_ref, + trust_policy_ref=trust_policy_ref, + ) + ], + materialization_specifications=[ + ArtifactMaterializationSpecification( + specification_id="cloud-image", + profile=_mechanism("materialization-specification"), + digest=_PROFILE_DIGEST, + locked_input_ids=["rootfs"], + ) + ], + permitted_routes=[ + _route( + mechanism=_mechanism("materialization-specification"), + acquisition="none", + timing="backend-preparation", + ) + ], + ) + compiled = _compiled(requirement) + declared_plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=_ADDRESS, + resource_type="node", + payload={"artifact_requirement": requirement.model_dump(mode="json")}, + ) + ] + ) + manifest = _manifest( + requirement, + capability=_capability( + mechanism=_mechanism("materialization-specification"), + acquisition="none", + timing="backend-preparation", + ), + ) + disclosure = ArtifactSatisfactionDisclosureModel( + requirement_id=requirement.requirement_id, + artifact=_identity(), + mechanism=_mechanism("materialization-specification"), + acquisition="none", + timing="backend-preparation", + backend=manifest.identity, + materialization_specification_id="cloud-image", + materialization_specification_digest=_DIGEST_B, + locked_input_ids=["rootfs"], + integrity_refs=[_DIGEST_A], + admission_refs=[trust_policy_ref], + provenance_refs=["provenance:verified"], + evidence_refs=[manifest_ref], + ) + snapshot = RuntimeSnapshot( + entries={ + _ADDRESS: SnapshotEntry( + address=_ADDRESS, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={ + "artifact_satisfaction": disclosure.model_dump(mode="json"), + }, + ) + } + ) + availability = _availability( + verified_locked_input_ids=["rootfs"], + available_materialization_specification_digests=[_PROFILE_DIGEST], + verified_integrity_refs=[_DIGEST_A], + verified_admission_refs=[trust_policy_ref], + verified_provenance_refs=["provenance:verified"], + verified_evidence_refs=[manifest_ref], + ) + + diagnostics, provenance = realization_disclosure( + (compiled,), + declared_plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + assert {diagnostic.code for diagnostic in diagnostics} == {"runtime.backend-contract-invalid"} + assert provenance == () + + valid = disclosure.model_copy(update={"materialization_specification_digest": _PROFILE_DIGEST}) + snapshot.entries[_ADDRESS].payload["artifact_satisfaction"] = valid.model_dump(mode="json") + diagnostics, provenance = realization_disclosure( + (compiled,), + declared_plan, + snapshot, + manifest=manifest, + artifact_availability=availability, + ) + assert diagnostics == [] + assert provenance[0].artifact_satisfaction == valid + + +def test_satisfaction_roundtrips_on_existing_provenance_carrier() -> None: + disclosure = ArtifactSatisfactionDisclosureModel( + requirement_id="web-image", + artifact=_identity(), + mechanism=_mechanism(), + acquisition="local-lookup", + timing="backend-preparation", + backend={"name": "libvirt", "version": "1"}, + integrity_refs=[_DIGEST_A], + authenticity_refs=["signature:rekor-entry"], + admission_refs=["policy:reusable-asset-trust"], + provenance_refs=["provenance:slsa"], + evidence_refs=["evidence:artifact-resolution"], + ) + entry = RealizationProvenanceEntry( + address=_ADDRESS, + field_path="nodes.web.source.artifact_requirement", + domain="runtime-realization", + requirement_kind="source-artifact", + explicitness=ExplicitnessClass.EXACT, + provenance=ExplicitnessProvenance.AUTHOR_DECLARED, + artifact_satisfaction=disclosure, + ) + assert entry.artifact_satisfaction is disclosure + restored = _snapshot_from_payload(_snapshot_payload(RuntimeSnapshot(realization_provenance=(entry,)))) + assert restored.realization_provenance[0].artifact_satisfaction == disclosure + dumped = ArtifactSatisfactionDisclosureModel.model_validate(disclosure.model_dump(mode="json")) + assert dumped == disclosure + assert "location" not in ArtifactSatisfactionDisclosureModel.model_fields + assert "channel" not in ArtifactSatisfactionDisclosureModel.model_fields + + +def test_contract_is_published_as_a_closed_schema() -> None: + assert ARTIFACT_REQUIREMENT_SCHEMA_VERSION == "artifact-requirement/v1" + model = ArtifactRequirementContractModel.model_validate( + { + "schema_version": ARTIFACT_REQUIREMENT_SCHEMA_VERSION, + "source": Source( + name="ubuntu-server", + version="24.04.1", + artifact_requirement=_exact_requirement(), + ).model_dump(mode="json"), + } + ) + assert model.source.artifact_requirement is not None + schema = schema_bundle()["artifact-requirement-v1"] + assert schema["additionalProperties"] is False + source_schema = schema["$defs"]["ArtifactRequirementSource"] + assert "artifact_requirement" in source_schema["required"] + assert {invariant["id"] for invariant in schema["x-raes-invariants"]} == { + "exact-source-artifact-id-match", + "exact-source-version-match", + "materialization-locked-input-join", + } + + +def test_published_fixtures_match_schema_and_typed_contract_validation() -> None: + schema = schema_bundle()["artifact-requirement-v1"] + validator = Draft202012Validator(schema) + 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: + payload = json.loads(path.read_text(encoding="utf-8")) + validator.validate(payload) + assert artifact_requirement_invariant_violations(payload) == () + ArtifactRequirementContractModel.model_validate(payload) + for path in invalid_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + schema_errors = list(validator.iter_errors(payload)) + invariant_errors = artifact_requirement_invariant_violations(payload) + assert schema_errors or invariant_errors, path + with pytest.raises(ValidationError): + ArtifactRequirementContractModel.model_validate(payload) diff --git a/specs/README.md b/specs/README.md index b391ca013..497fd6480 100644 --- a/specs/README.md +++ b/specs/README.md @@ -45,8 +45,9 @@ hook). stateful subsystems (governed by ADR-007 and ADR-018), including the behavioral-relation taxonomy and claim discipline governed by ADR-081 - `supply-chain/` — normative prose for the Packaging & Supply Chain - wave, including the reusable-asset trust/authenticity/integrity policy - (GOV-913, governed by ADR-071) + wave, including reusable-asset trust/authenticity/integrity, associated + artifact manifests, and portable artifact requirement satisfaction + (governed by ADR-071, ADR-077, and ADR-097) Revision-pinned lineage and derivation records live under the normative `contracts/provenance/` authority registered by ADR-019; explanatory lineage pages consume that record rather than defining a parallel source registry. diff --git a/specs/supply-chain/README.md b/specs/supply-chain/README.md index fc9dc3bbf..6ef1848ab 100644 --- a/specs/supply-chain/README.md +++ b/specs/supply-chain/README.md @@ -11,3 +11,10 @@ here are authoritative independent of any reference implementation. (GOV-913, [ADR-071](../../docs/decisions/adrs/adr-071-reusable-asset-trust-and-integrity-policy.md)). Its machine-checkable surface is the published `reusable-asset-trust-policy-v1` contract under `contracts/schemas/asset-trust/`. +- [`associated-artifact-manifests.md`](associated-artifact-manifests.md) — + portable attachment identities and byte-binding conformance + ([ADR-077](../../docs/decisions/adrs/adr-077-associated-artifact-manifest-boundary.md)). +- [`artifact-requirement-satisfaction.md`](artifact-requirement-satisfaction.md) — + exact, constrained, and open `Source` artifact demand, backend capability, + admission, and runtime satisfaction disclosure + (#920, [ADR-097](../../docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md)). diff --git a/specs/supply-chain/artifact-requirement-satisfaction.md b/specs/supply-chain/artifact-requirement-satisfaction.md new file mode 100644 index 000000000..42cf9abbd --- /dev/null +++ b/specs/supply-chain/artifact-requirement-satisfaction.md @@ -0,0 +1,200 @@ +# Portable Artifact Requirement Satisfaction + +Status: normative + +Requirement: #920 + +Decision: [ADR-097](../../docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md) + +This specification defines how a backend-neutral `Source` can require and +prove a concrete artifact without conflating semantic identity, operational +availability, acquisition, materialization, or observed build provenance. The +machine-readable authority is `artifact-requirement-v1` under +`contracts/schemas/artifact-requirements/`. + +## 1. Contract family and ownership + +The semantic family has three phase-specific carriers: + +1. `Source.artifact_requirement` is author-owned demand. +2. `backend-manifest-v2.realization_support[].artifact_mechanisms` is + backend-owned capability. +3. `RealizationProvenanceEntry.artifact_satisfaction` is backend-produced, + processor-validated runtime disclosure. + +The carriers MUST NOT be collapsed. Operational availability is supplied to +planning separately and MUST NOT be serialized into semantic requirement +identity. Absence of `Source.artifact_requirement` preserves selector-only +`Source` behavior and is not a fourth explicitness posture. + +## 2. Requirement posture + +Every present requirement has a stable `requirement_id`, one existing +explicitness class, and at least one permitted satisfaction route. + +### 2.1 Exact + +An exact requirement MUST: + +- name exactly one immutable `ArtifactIdentity`; +- bind provider-neutral artifact id, version, SHA-256 digest, and media type; +- match its owning `Source.name` and `Source.version`; +- use only the `exact-artifact` mechanism; and +- declare no constraints, candidates, locked inputs, or materialization + specifications. + +An implementation MUST reject unavailable exact bytes. It MUST NOT fall back +to another digest, rebuild the artifact, substitute a compatible version, or +reinterpret a wildcard selector as exact evidence. + +### 2.2 Constrained + +A constrained requirement MUST NOT carry `exact_artifact` and MUST declare a +non-empty bounded authority domain through one or more of: + +- typed constraints with finite allowed values; +- explicitly admitted immutable candidates; +- immutable locked inputs joined to associated-artifact and trust-policy + references; or +- digest-bound materialization specifications whose input ids resolve to those + locked inputs. + +Candidate order is not fallback order. A backend may select only a declared, +available candidate and MUST disclose the selected `candidate_id`. +Materialization authority is a closed profile reference, not shell text, +environment variables, a Dockerfile, or `Source.build`. + +### 2.3 Open + +An open requirement MUST NOT carry `exact_artifact`, candidates, artifact +constraints, or materialization specifications. It may still require immutable +inputs and trust policy. It delegates output selection only to a backend whose +matching realization declaration has +`support_mode: open-realization` and a matching permitted mechanism route. +Open does not waive trust, evidence, or disclosure obligations. + +## 3. Mechanisms, acquisition, and timing + +A mechanism is identified by `mechanism`, `profile`, `version`, and SHA-256 +profile digest. Portable base mechanisms are: + +- `exact-artifact`; +- `backend-owned-artifact`; +- `published-candidate`; +- `dynamic-composition`; and +- `materialization-specification`. + +Extensions MUST use `x-:`. Arbitrary free-form mechanism names +are invalid. + +Acquisition is one of `pull`, `copy`, `import`, `local-lookup`, or `none`. +Timing is one of `publication`, `pack-ingestion`, `backend-preparation`, or +`realization`. These dimensions are orthogonal to posture and mechanism. +Backend capability MUST enumerate exact acquisition/timing pairs under each +mechanism profile; separate unjoined lists are non-conforming because they +claim a Cartesian product. + +## 4. Compilation and admission + +The processor MUST lower a present requirement into the existing compiled +realization-demand graph at every realized owner: + +- node or switch/network; +- content; +- feature binding; +- condition binding; +- inject; or +- event. + +The compiled requirement uses domain `runtime-realization`, kind +`source-artifact`, the authored explicitness/provenance, the canonical resource +address, and the authored requirement payload. It MUST NOT create a second +planner or artifact resource graph. + +Planning admits a requirement only when: + +- the posture is supported by the matching realization declaration; +- one permitted mechanism/acquisition/timing route is advertised; +- the exact digest is available for exact demand; +- every required constraint id is satisfied; +- every locked input id is verified; and +- at least one candidate id is available when candidates are declared; and +- at least one authored materialization-specification digest is available when + materialization alternatives are declared. + +Operational fact inputs MUST be partitioned by canonical compiled address. +Candidate ids, constraint ids, and locked-input ids are local to one +`ArtifactRequirement`; a fact admitted for one address MUST NOT satisfy a +same-named declaration at another address. The processor-owned availability +context also carries the integrity, authenticity, admission, provenance, and +evidence references that its trust boundaries have independently verified. +Registry URLs, cloud regions, account/project ids, channels, credentials, +tokens, and host paths MUST NOT enter semantic identity or diagnostic text. + +Stable admission diagnostics are: + +| Failure | Code | +|---|---| +| Exact artifact unavailable | `artifact.unavailable-exact-artifact` | +| Constraint not satisfied | `artifact.unsatisfied-constraint` | +| Open realization unsupported | `artifact.unsupported-open-realization` | +| Locked input not verified | `artifact.missing-locked-input` | +| No candidate available | `artifact.unavailable-candidate` | +| No materialization specification available | `artifact.unavailable-materialization-specification` | +| No exact supported mechanism route | `artifact.unsupported-backend-mechanism` | + +## 5. Runtime satisfaction and provenance + +Before accepting a returned snapshot, the processor MUST parse +`payload.artifact_satisfaction` as the closed satisfaction contract and check: + +- `requirement_id` equals the compiled requirement; +- the disclosed backend identity equals the selected manifest identity; +- mechanism, acquisition, and timing equal one permitted route and one + mechanism-indexed capability route admitted from that manifest; +- an exact artifact equals the authored immutable identity; +- a disclosed candidate resolves to its declared immutable artifact and was + available for this compiled address; +- a materialization selection resolves to a declared specification, uses that + specification's digest-bound mechanism profile, discloses its exact authored + specification digest, and discloses exactly its locked inputs; +- every disclosed constraint and locked-input id was independently admitted + for this compiled address; +- the realized artifact digest is present in both the disclosure and the + processor-owned verified integrity set; and +- the selected materialization-specification digest is present in the + address-scoped trusted availability context; and +- authenticity, admission, provenance, and evidence references are subsets of + the processor-owned verified sets, including every authored trust-policy or + associated-artifact reference. + +Omission, malformed disclosure, route substitution, or exact artifact +substitution MUST reject the backend result with +`runtime.backend-contract-invalid`. A valid disclosure is attached to the +existing `RealizationProvenanceEntry` before snapshot persistence. Exact +satisfaction retains `author-declared` provenance; backend selection on an +admitted constrained or open surface is `backend-realized`. + +The disclosure may carry authenticity, admission, provenance, and evidence +references. It MUST NOT carry location or channel fields. Backend-provided +reference strings do not prove authenticity or integrity: the runtime gate +accepts them only when they resolve through the processor-owned verification +context established by the applicable trust and evidence contracts. + +## 6. Security and conformance + +All public models and schemas are closed. Implementations MUST reject unknown +fields, duplicate semantic ids/routes, invalid digest syntax, ungoverned +mechanism names, unresolved materialization input joins, and posture/authority +contradictions. The published schema structurally requires +`Source.artifact_requirement`; cross-object equality and local-id joins that +Draft 2020-12 cannot express are published under the governed +`x-raes-invariants` profile with an importable validator. Diagnostics MUST +identify stable requirement, field, and capability terms without echoing +realized values or operational secrets. + +This specification authorizes no network access, registry mutation, +credential lookup, archive extraction, subprocess execution, or artifact +build. Conformance is established by the typed models, Draft 2020-12 published +schemas, valid/invalid fixtures, compiler/planner/runtime tests, and schema +publication ledger parity. diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index 0746eabed..d6e4548b7 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -20,6 +20,8 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: return schemas_dir / "sdl" / f"{name}.json" if name.startswith("scenario-satisfiability-evidence-v"): return schemas_dir / "satisfiability" / f"{name}.json" + if name.startswith("artifact-requirement-v"): + return schemas_dir / "artifact-requirements" / 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"): diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index c0bf38862..9ec8e0c47 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -44,7 +44,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", "occurrences": 4, - "content_sha256": "bcb796f8491d00f1c842cffbfe36dc3c0fccd5838e7353d1d600f6a780bfa184" + "content_sha256": "e0eda7d0e69151a1c46617469ec8ebe78202217bfae49247c6564c1949d802e6" }, { "path": "docs/decisions/adrs/adr-000-use-adrs.md", @@ -499,7 +499,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", "occurrences": 4, - "content_sha256": "52f0eed782027820a62ef271a6c83fa23763d96410185c4278e626312e30a969" + "content_sha256": "e29f225969cda63915a39d4f75bcce0e37330ee008105a0f0c823954e031635b" }, { "path": "docs/decisions/cage-2-replication-design.md", @@ -1528,7 +1528,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": "2a411d210a4d24916290b2e61a7b96acf36b63e648fa8188186d2ce7822c4c10" + "content_sha256": "631b59c4a40ec935a68ab975814c023d3824e5c42e62415e3176b88baacf6519" }, { "path": "docs/research/formal-semantic-validation/execution-snapshot-v1.json", From 03783c5455b329eebd9e7ed4b29175e6cf13414f Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 05:52:12 +0200 Subject: [PATCH 41/55] fix: refresh ADR-097 content pin --- docs/decisions/adrs/adr-index.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index f82cf73a1..9644f3e75 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -451,4 +451,4 @@ adrs: summary: "Retained the existing SonarCloud project key as an exact, content-bound external-service designation rather than a current RAES product identity." - id: ADR-097 path: docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md - pin: 01658277a7d10da7f98a38a33e6a9e1aff62bf329a2f5815d498fe99180a71b4 + pin: 39a8c1b3865355cbd4612e1367b51f8b80a0977a7f5748b68a5165b1228dd417 From e73800a30ec5827b27a4d86758a27772c814dbf3 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 05:57:41 +0200 Subject: [PATCH 42/55] fix: refresh ADR index identity pin --- tools/policy/historical_identity_records.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index 9ec8e0c47..98696ad54 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -499,7 +499,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", "occurrences": 4, - "content_sha256": "e29f225969cda63915a39d4f75bcce0e37330ee008105a0f0c823954e031635b" + "content_sha256": "534fbb9db337f6a9abe186f7ca5048988537a00b3f0e9c619213bb7991b5946f" }, { "path": "docs/decisions/cage-2-replication-design.md", From a5681fe4f17140009ae35180bb03c9aec10b1a7c Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 06:05:43 +0200 Subject: [PATCH 43/55] Fix SonarCloud findings (cycle 1) --- implementations/python/tests/test_vale_tool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/implementations/python/tests/test_vale_tool.py b/implementations/python/tests/test_vale_tool.py index 010174793..93a9f09cd 100644 --- a/implementations/python/tests/test_vale_tool.py +++ b/implementations/python/tests/test_vale_tool.py @@ -67,5 +67,7 @@ def test_vale_extraction_rejects_archive_without_root_binary(tmp_path: Path) -> info.size = len(body) archive.addfile(info, io.BytesIO(body)) + archive_bytes = payload.getvalue() + binary_path = tmp_path / "vale" with pytest.raises(RuntimeError, match="root vale binary"): - vale_tool._extract_binary(payload.getvalue(), tmp_path / "vale") + vale_tool._extract_binary(archive_bytes, binary_path) From e37debc792f48a168fd24154ce826e3dbfbabbff Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 06:11:21 +0200 Subject: [PATCH 44/55] Resolve participant resource quality findings --- .../raes/participant_resource_budgets.py | 85 ++-- .../_participant_resource_budget_owners.py | 148 ++++-- .../participant_capabilities.py | 13 +- .../participant_resource_admission.py | 327 ++++++++----- .../contracts/participant_resource_budgets.py | 138 +----- .../participant_resource_validation.py | 230 +++++++++ .../contracts/realization_plans.py | 118 +++-- .../participant_autonomous_execution.py | 14 +- .../participant_resource_accounting.py | 261 +++++++---- .../participant_resource_budgets.py | 441 ++++++------------ .../participant_resource_reservation.py | 301 ++++++++++++ .../raes_runtime/participant_scheduler.py | 242 +++++----- .../participant_scheduler_lifecycle.py | 105 +++++ .../participant_scheduler_resources.py | 89 ++-- ..._issue_899_participant_resource_budgets.py | 6 +- 15 files changed, 1571 insertions(+), 947 deletions(-) create mode 100644 implementations/python/packages/raes_contracts/contracts/participant_resource_validation.py create mode 100644 implementations/python/packages/raes_runtime/participant_resource_reservation.py create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py diff --git a/implementations/python/packages/raes/participant_resource_budgets.py b/implementations/python/packages/raes/participant_resource_budgets.py index d40a17087..f28d2d566 100644 --- a/implementations/python/packages/raes/participant_resource_budgets.py +++ b/implementations/python/packages/raes/participant_resource_budgets.py @@ -127,6 +127,51 @@ def _validate_dimension(self) -> ParticipantResourceBudgetDimension: return self +def _dimension_semantics(dimension: ParticipantResourceBudgetDimension) -> tuple[object, ...]: + return ( + dimension.resource_kind, + dimension.unit, + dimension.accounting_mode, + dimension.meter_profile_ref, + ) + + +def _visit_parent_budget( + dimensions: dict[PortableIdentifier, ParticipantResourceBudgetDimension], + budget_id: str, + visiting: set[str], + visited: set[str], +) -> None: + if budget_id in visiting: + raise ValueError("resource-budget parent aggregation graph must be acyclic") + if budget_id in visited: + return + visiting.add(budget_id) + dimension = dimensions[budget_id] + parent_ref = dimension.parent_budget_ref + if parent_ref is not None: + parent = dimensions[parent_ref] + if _dimension_semantics(dimension) != _dimension_semantics(parent): + raise ValueError("resource-budget parent must use the same resource, unit, mode, and meter") + if dimension.limit > parent.limit: + raise ValueError("resource-budget child limit cannot exceed its parent") + _visit_parent_budget(dimensions, str(parent_ref), visiting, visited) + visiting.remove(budget_id) + visited.add(budget_id) + + +def _validate_sibling_limits( + dimensions: dict[PortableIdentifier, ParticipantResourceBudgetDimension], +) -> None: + children_by_parent: dict[str, list[ParticipantResourceBudgetDimension]] = {} + for dimension in dimensions.values(): + if dimension.parent_budget_ref is not None: + children_by_parent.setdefault(str(dimension.parent_budget_ref), []).append(dimension) + for parent_id, children in children_by_parent.items(): + if sum(child.limit for child in children) > dimensions[parent_id].limit: + raise ValueError("resource-budget sibling limits cannot exceed their parent limit") + + class ParticipantResourceBudgetPolicy(SDLModel): policy_id: PortableIdentifier owners: dict[PortableIdentifier, ParticipantResourceOwner] = Field(min_length=1, max_length=1024) @@ -174,45 +219,9 @@ def _validate_parent_graph(self) -> None: visiting: set[str] = set() visited: set[str] = set() - def visit(budget_id: str) -> None: - if budget_id in visiting: - raise ValueError("resource-budget parent aggregation graph must be acyclic") - if budget_id in visited: - return - visiting.add(budget_id) - dimension = self.dimensions[budget_id] - parent_ref = dimension.parent_budget_ref - if parent_ref is not None: - parent = self.dimensions[parent_ref] - comparable = ( - dimension.resource_kind, - dimension.unit, - dimension.accounting_mode, - dimension.meter_profile_ref, - ) - parent_comparable = ( - parent.resource_kind, - parent.unit, - parent.accounting_mode, - parent.meter_profile_ref, - ) - if comparable != parent_comparable: - raise ValueError("resource-budget parent must use the same resource, unit, mode, and meter") - if dimension.limit > parent.limit: - raise ValueError("resource-budget child limit cannot exceed its parent") - visit(str(parent_ref)) - visiting.remove(budget_id) - visited.add(budget_id) - for budget_id in self.dimensions: - visit(str(budget_id)) - children_by_parent: dict[str, list[ParticipantResourceBudgetDimension]] = {} - for dimension in self.dimensions.values(): - if dimension.parent_budget_ref is not None: - children_by_parent.setdefault(str(dimension.parent_budget_ref), []).append(dimension) - for parent_id, children in children_by_parent.items(): - if sum(child.limit for child in children) > self.dimensions[parent_id].limit: - raise ValueError("resource-budget sibling limits cannot exceed their parent limit") + _visit_parent_budget(self.dimensions, str(budget_id), visiting, visited) + _validate_sibling_limits(self.dimensions) __all__ = [ diff --git a/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py b/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py index 36c535f90..a3de24c3e 100644 --- a/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py +++ b/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py @@ -1,6 +1,84 @@ """Semantic validation for authored participant resource-budget owners.""" from collections.abc import Callable, Mapping +from dataclasses import dataclass + +_NODE_PREFIX = "nodes." +_TENANT_PREFIX = "deployment_tenants." + + +@dataclass(frozen=True) +class _OwnerScope: + participant_refs: set[str] + deployment_tenants: Mapping[str, object] + target_tenants: set[str] + action_targets: set[str] + declared_tenants: set[str] + shared_permissions: set[tuple[str, str]] + + +def _action_targets(policy: object, action_contracts: Mapping[str, object]) -> set[str]: + action_refs = tuple(getattr(policy, "action_order", ())) or tuple( + getattr(candidate, "action_ref", "") for candidate in getattr(policy, "action_candidates", {}).values() + ) + return { + str(target) + for action_ref in action_refs + for effect in getattr(action_contracts.get(str(action_ref).removeprefix("action_contracts.")), "effects", ()) + for target in getattr(effect, "target_refs", ()) + } + + +def _target_tenants(action_targets: set[str], deployment_cells: Mapping[str, object]) -> set[str]: + target_nodes = { + target.removeprefix(_NODE_PREFIX).split(".services.", 1)[0] + for target in action_targets + if target.startswith(_NODE_PREFIX) + } + return { + str(getattr(cell, "tenant_ref", "")).removeprefix(_TENANT_PREFIX) + for cell in deployment_cells.values() + if target_nodes & {str(node_ref).removeprefix(_NODE_PREFIX) for node_ref in getattr(cell, "node_refs", ())} + } + + +def _shared_permissions(relationships: Mapping[str, object]) -> set[tuple[str, str]]: + return { + ( + str(getattr(relationship, "source", "")).removeprefix(_TENANT_PREFIX), + str(getattr(relationship, "target", "")), + ) + for relationship in relationships.values() + if getattr(getattr(relationship, "type", ""), "value", getattr(relationship, "type", "")) + == "uses_shared_service" + } + + +def _owner_error( + owner: object, + *, + label: str, + scope: _OwnerScope, + split_node_service_ref: Callable[[str], object | None], +) -> str | None: + kind = getattr(getattr(owner, "kind", ""), "value", getattr(owner, "kind", "")) + ref = str(getattr(owner, "ref", "")) + if kind == "participant" and ref.removeprefix("agents.") not in scope.participant_refs: + return f"{label} participant ref '{ref}' is outside the policy participant scope" + if kind == "deployment_tenant": + tenant_ref = ref.removeprefix(_TENANT_PREFIX) + if tenant_ref not in scope.deployment_tenants: + return f"{label} deployment tenant ref '{ref}' is undefined" + if tenant_ref not in scope.target_tenants: + return f"{label} deployment tenant ref '{ref}' does not own an authorized action target" + if kind == "shared_service": + if split_node_service_ref(ref) is None: + return f"{label} shared service ref '{ref}' is undefined" + if ref not in scope.action_targets: + return f"{label} shared service ref '{ref}' is not an exact execution target" + if not any((tenant, ref) in scope.shared_permissions for tenant in scope.declared_tenants): + return f"{label} shared service ref '{ref}' lacks an authorized tenant uses_shared_service edge" + return None def participant_resource_budget_owner_errors( @@ -20,63 +98,33 @@ def participant_resource_budget_owner_errors( if budget is None: continue participant_refs = {str(ref).removeprefix("agents.") for ref in getattr(behavior_spec, "participant_refs", ())} - action_refs = tuple(getattr(policy, "action_order", ())) or tuple( - getattr(candidate, "action_ref", "") for candidate in getattr(policy, "action_candidates", {}).values() - ) - action_targets = { - str(target) - for action_ref in action_refs - for effect in getattr( - action_contracts.get(str(action_ref).removeprefix("action_contracts.")), "effects", () - ) - for target in getattr(effect, "target_refs", ()) - } - target_nodes = { - str(target).removeprefix("nodes.").split(".services.", 1)[0] - for target in action_targets - if str(target).startswith("nodes.") - } - target_tenants = { - str(getattr(cell, "tenant_ref", "")).removeprefix("deployment_tenants.") - for cell in deployment_cells.values() - if target_nodes & {str(node_ref).removeprefix("nodes.") for node_ref in getattr(cell, "node_refs", ())} - } + action_targets = _action_targets(policy, action_contracts) + target_tenants = _target_tenants(action_targets, deployment_cells) declared_tenants = { - str(owner.ref).removeprefix("deployment_tenants.") + str(owner.ref).removeprefix(_TENANT_PREFIX) for owner in budget.owners.values() if getattr(owner.kind, "value", owner.kind) == "deployment_tenant" } - shared_permissions = { - ( - str(getattr(relationship, "source", "")).removeprefix("deployment_tenants."), - str(getattr(relationship, "target", "")), - ) - for relationship in relationships.values() - if getattr(getattr(relationship, "type", ""), "value", getattr(relationship, "type", "")) - == "uses_shared_service" - } + shared_permissions = _shared_permissions(relationships) target_tenants.update(tenant for tenant, target in shared_permissions if target in action_targets) + scope = _OwnerScope( + participant_refs=participant_refs, + deployment_tenants=deployment_tenants, + target_tenants=target_tenants, + action_targets=action_targets, + declared_tenants=declared_tenants, + shared_permissions=shared_permissions, + ) for owner_id, owner in budget.owners.items(): - kind = getattr(owner.kind, "value", owner.kind) - ref = str(owner.ref) label = f"Behavior specification '{spec_name}' resource-budget owner '{owner_id}'" - if kind == "participant" and ref.removeprefix("agents.") not in participant_refs: - errors.append(f"{label} participant ref '{ref}' is outside the policy participant scope") - elif kind == "deployment_tenant": - tenant_ref = ref.removeprefix("deployment_tenants.") - if tenant_ref not in deployment_tenants: - errors.append(f"{label} deployment tenant ref '{ref}' is undefined") - elif tenant_ref not in target_tenants: - errors.append(f"{label} deployment tenant ref '{ref}' does not own an authorized action target") - elif kind == "shared_service": - if split_node_service_ref(ref) is None: - errors.append(f"{label} shared service ref '{ref}' is undefined") - elif ref not in action_targets: - errors.append(f"{label} shared service ref '{ref}' is not an exact execution target") - elif not any((tenant, ref) in shared_permissions for tenant in declared_tenants): - errors.append( - f"{label} shared service ref '{ref}' lacks an authorized tenant uses_shared_service edge" - ) + error = _owner_error( + owner, + label=label, + scope=scope, + split_node_service_ref=split_node_service_ref, + ) + if error is not None: + errors.append(error) return tuple(errors) diff --git a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py index 2ff818160..79aa177c8 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -274,6 +274,14 @@ def _validate_enabled_autonomous_execution(self) -> None: raise ValueError("autonomous execution requires exact supported observation boundaries") if not self.supported_autonomous_policy_profiles: raise ValueError("autonomous execution requires exact supported policy profiles") + self._validate_activity_profiles() + self._validate_autonomous_addresses() + self._validate_execution_control() + for label, value in self._autonomous_limits(): + if value is None or value < 1: + raise ValueError(f"autonomous execution requires positive {label}") + + def _validate_activity_profiles(self) -> None: if { "participant-autonomous-execution/v2", "participant-autonomous-execution/v3", @@ -287,11 +295,6 @@ def _validate_enabled_autonomous_execution(self) -> None: and self.resource_budgets is None ): raise ValueError("autonomous execution v3 requires participant resource-budget capabilities") - self._validate_autonomous_addresses() - self._validate_execution_control() - for label, value in self._autonomous_limits(): - if value is None or value < 1: - raise ValueError(f"autonomous execution requires positive {label}") def _validate_execution_control(self) -> None: self._validate_execution_control_actions() diff --git a/implementations/python/packages/raes_backend_protocols/participant_resource_admission.py b/implementations/python/packages/raes_backend_protocols/participant_resource_admission.py index fe48b18b3..336927528 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_resource_admission.py +++ b/implementations/python/packages/raes_backend_protocols/participant_resource_admission.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Protocol if TYPE_CHECKING: @@ -41,19 +42,47 @@ class ResourceGovernedPolicy(Protocol): resource_fairness: ResourceFairness -def participant_resource_budget_gaps( +class ResourcePool(Protocol): + pool_ref: str + owner_kind: str + owner_ref: str + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + capacity: int + protected_capacity: int + fairness_policy: str + priority_classes: tuple[str, ...] + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + + +class ResourceBudgetCapabilities(Protocol): + support_strength: str + supported_owner_kinds: set[str] + supported_resource_kinds: set[str] + supported_accounting_modes: set[str] + supported_reset_modes: set[str] + supported_fairness_policies: set[str] + realization_contract_ids: set[str] + configured_pools: tuple[ResourcePool, ...] + + +@dataclass +class _AdmissionState: + gaps: list[str] = field(default_factory=list) + aggregate_limits: dict[tuple[str, ...], int] = field(default_factory=dict) + aggregate_protected_limits: dict[tuple[str, ...], int] = field(default_factory=dict) + pools_by_key: dict[tuple[str, ...], ResourcePool] = field(default_factory=dict) + + +def _capability_gaps( manifest: BackendManifest, - capability: ParticipantRuntimeCapabilities, - policies: tuple[ResourceGovernedPolicy, ...], + budgets: ResourceBudgetCapabilities, ) -> list[str]: - """Return atomic capacity, accounting, isolation, and fairness gaps.""" - - governed = tuple(policy for policy in policies if policy.profile == "participant-autonomous-execution/v3") - if not governed: - return [] - budgets = capability.resource_budgets - if budgets is None: - return ["participant-autonomous-execution/v3 requires resource-budget capabilities"] gaps: list[str] = [] if budgets.support_strength not in {"bounded", "exact"}: gaps.append( @@ -66,129 +95,157 @@ def participant_resource_budget_gaps( "participant-resource-budget-state-v1", "participant-resource-budget-event-v1", } - missing_manifest_contracts = sorted(required_manifest_contracts - manifest.supported_contract_versions) - if missing_manifest_contracts: - gaps.append("participant resource budgets missing manifest contracts: " + ", ".join(missing_manifest_contracts)) + missing_manifest = sorted(required_manifest_contracts - manifest.supported_contract_versions) + if missing_manifest: + gaps.append("participant resource budgets missing manifest contracts: " + ", ".join(missing_manifest)) required_contracts = { "participant-resource-budget-state-v1", "participant-resource-budget-event-v1", } - missing_contracts = sorted(required_contracts - budgets.realization_contract_ids) - if missing_contracts: - gaps.append("participant resource budgets missing realization contracts: " + ", ".join(missing_contracts)) - aggregate_limits: dict[tuple[str, ...], int] = {} - aggregate_protected_limits: dict[tuple[str, ...], int] = {} - pools_by_key: dict[tuple[str, ...], object] = {} - for policy in governed: - fairness = policy.resource_fairness - if fairness.policy not in budgets.supported_fairness_policies: - gaps.append(f"unsupported participant resource fairness policy: {fairness.policy}") - policy_pool_keys: set[tuple[str, ...]] = set() - demands_by_id = {demand.budget_id: demand for demand in policy.resource_demands} - children_by_parent: dict[str, list[ResourceDemand]] = {} - for demand in policy.resource_demands: - if demand.parent_budget_ref is not None: - children_by_parent.setdefault(demand.parent_budget_ref, []).append(demand) - for parent_id, children in children_by_parent.items(): - parent = demands_by_id.get(parent_id) - if parent is None or sum(child.limit for child in children) > parent.limit: - gaps.append( - f"participant resource budget parent {parent_id} is missing or overcommitted by child limits" - ) - for demand in policy.resource_demands: - unsupported: list[str] = [] - if demand.owner_kind not in budgets.supported_owner_kinds: - unsupported.append(f"owner kind {demand.owner_kind}") - if demand.resource_kind not in budgets.supported_resource_kinds: - unsupported.append(f"resource kind {demand.resource_kind}") - if demand.accounting_mode not in budgets.supported_accounting_modes: - unsupported.append(f"accounting mode {demand.accounting_mode}") - if demand.reset not in budgets.supported_reset_modes: - unsupported.append(f"reset mode {demand.reset}") - if unsupported: - gaps.append(f"participant resource budget {demand.budget_id} unsupported: " + ", ".join(unsupported)) - continue - exact = tuple( - pool - for pool in budgets.configured_pools - if pool.pool_ref == demand.pool_ref - and pool.owner_kind == demand.owner_kind - and pool.owner_ref == demand.owner_address - and pool.resource_kind == demand.resource_kind - and pool.unit == demand.unit - and pool.accounting_mode == demand.accounting_mode - and pool.meter_profile_ref == demand.meter_profile_ref - ) - if not exact: - gaps.append( - "participant resource budget " - f"{demand.budget_id} ({demand.resource_kind}) has no exact configured " - "owner/unit/accounting/meter capacity" - ) - continue - pool = exact[0] - pool_key = ( - pool.pool_ref, - pool.owner_kind, - pool.owner_ref, - pool.resource_kind, - pool.unit, - pool.accounting_mode, - pool.meter_profile_ref, - ) - if pool_key in policy_pool_keys: - gaps.append(f"participant policy {policy.address} aliases canonical resource pool {pool.pool_ref}") - continue - policy_pool_keys.add(pool_key) - pools_by_key[pool_key] = pool - aggregate_limits[pool_key] = aggregate_limits.get(pool_key, 0) + demand.limit - if fairness.protected: - aggregate_protected_limits[pool_key] = aggregate_protected_limits.get(pool_key, 0) + demand.limit - if pool.capacity < demand.limit: - gaps.append( - "participant resource budget " - f"{demand.budget_id} ({demand.resource_kind}) requires capacity " - f"{demand.limit}; configured capacity is {pool.capacity}" - ) - if pool.fairness_policy != fairness.policy: - gaps.append( - "participant resource budget " - f"{demand.budget_id} ({demand.resource_kind}) requires fairness " - f"{fairness.policy}; configured pool declares {pool.fairness_policy}" - ) - if fairness.priority_class not in pool.priority_classes: - gaps.append( - "participant resource budget " - f"{demand.budget_id} ({demand.resource_kind}) requires priority class " - f"{fairness.priority_class}" - ) - if pool.borrowing != fairness.borrowing or pool.reclaim != fairness.reclaim: - gaps.append( - "participant resource budget " - f"{demand.budget_id} ({demand.resource_kind}) fairness borrowing/reclaim " - "does not match configured pool" - ) - if ( - pool.max_queue_ticks > fairness.max_queue_ticks - or pool.starvation_bound_ticks > fairness.starvation_bound_ticks - ): - gaps.append( - "participant resource budget " - f"{demand.budget_id} ({demand.resource_kind}) configured fairness " - "queue/starvation bounds are weaker than required" - ) - if fairness.protected and pool.protected_capacity < demand.reservation: - gaps.append( - f"participant resource budget {demand.budget_id} ({demand.resource_kind}) lacks protected capacity" - ) - for pool_key, required in aggregate_limits.items(): - pool = pools_by_key[pool_key] + missing_realization = sorted(required_contracts - budgets.realization_contract_ids) + if missing_realization: + gaps.append("participant resource budgets missing realization contracts: " + ", ".join(missing_realization)) + return gaps + + +def _parent_limit_gaps(policy: ResourceGovernedPolicy) -> list[str]: + demands_by_id = {demand.budget_id: demand for demand in policy.resource_demands} + children_by_parent: dict[str, list[ResourceDemand]] = {} + for demand in policy.resource_demands: + if demand.parent_budget_ref is not None: + children_by_parent.setdefault(demand.parent_budget_ref, []).append(demand) + return [ + f"participant resource budget parent {parent_id} is missing or overcommitted by child limits" + for parent_id, children in children_by_parent.items() + if (parent := demands_by_id.get(parent_id)) is None or sum(child.limit for child in children) > parent.limit + ] + + +def _unsupported_attributes( + demand: ResourceDemand, + budgets: ResourceBudgetCapabilities, +) -> list[str]: + unsupported: list[str] = [] + supported_values = ( + ("owner kind", demand.owner_kind, budgets.supported_owner_kinds), + ("resource kind", demand.resource_kind, budgets.supported_resource_kinds), + ("accounting mode", demand.accounting_mode, budgets.supported_accounting_modes), + ("reset mode", demand.reset, budgets.supported_reset_modes), + ) + for label, value, supported in supported_values: + if value not in supported: + unsupported.append(f"{label} {value}") + return unsupported + + +def _matching_pool( + demand: ResourceDemand, + budgets: ResourceBudgetCapabilities, +) -> ResourcePool | None: + return next( + ( + pool + for pool in budgets.configured_pools + if pool.pool_ref == demand.pool_ref + and pool.owner_kind == demand.owner_kind + and pool.owner_ref == demand.owner_address + and pool.resource_kind == demand.resource_kind + and pool.unit == demand.unit + and pool.accounting_mode == demand.accounting_mode + and pool.meter_profile_ref == demand.meter_profile_ref + ), + None, + ) + + +def _pool_key(pool: ResourcePool) -> tuple[str, ...]: + return ( + pool.pool_ref, + pool.owner_kind, + pool.owner_ref, + pool.resource_kind, + pool.unit, + pool.accounting_mode, + pool.meter_profile_ref, + ) + + +def _pool_policy_gaps( + demand: ResourceDemand, + fairness: ResourceFairness, + pool: ResourcePool, +) -> list[str]: + prefix = f"participant resource budget {demand.budget_id} ({demand.resource_kind})" + gaps: list[str] = [] + if pool.capacity < demand.limit: + gaps.append(f"{prefix} requires capacity {demand.limit}; configured capacity is {pool.capacity}") + if pool.fairness_policy != fairness.policy: + gaps.append(f"{prefix} requires fairness {fairness.policy}; configured pool declares {pool.fairness_policy}") + if fairness.priority_class not in pool.priority_classes: + gaps.append(f"{prefix} requires priority class {fairness.priority_class}") + if pool.borrowing != fairness.borrowing or pool.reclaim != fairness.reclaim: + gaps.append(f"{prefix} fairness borrowing/reclaim does not match configured pool") + if pool.max_queue_ticks > fairness.max_queue_ticks or pool.starvation_bound_ticks > fairness.starvation_bound_ticks: + gaps.append(f"{prefix} configured fairness queue/starvation bounds are weaker than required") + if fairness.protected and pool.protected_capacity < demand.reservation: + gaps.append(f"{prefix} lacks protected capacity") + return gaps + + +def _assess_demand( + policy: ResourceGovernedPolicy, + demand: ResourceDemand, + budgets: ResourceBudgetCapabilities, + policy_pool_keys: set[tuple[str, ...]], + state: _AdmissionState, +) -> None: + unsupported = _unsupported_attributes(demand, budgets) + if unsupported: + state.gaps.append(f"participant resource budget {demand.budget_id} unsupported: " + ", ".join(unsupported)) + return + pool = _matching_pool(demand, budgets) + if pool is None: + state.gaps.append( + f"participant resource budget {demand.budget_id} ({demand.resource_kind}) has no exact configured " + "owner/unit/accounting/meter capacity" + ) + return + key = _pool_key(pool) + if key in policy_pool_keys: + state.gaps.append(f"participant policy {policy.address} aliases canonical resource pool {pool.pool_ref}") + return + policy_pool_keys.add(key) + state.pools_by_key[key] = pool + state.aggregate_limits[key] = state.aggregate_limits.get(key, 0) + demand.limit + if policy.resource_fairness.protected: + state.aggregate_protected_limits[key] = state.aggregate_protected_limits.get(key, 0) + demand.limit + state.gaps.extend(_pool_policy_gaps(demand, policy.resource_fairness, pool)) + + +def _assess_policy( + policy: ResourceGovernedPolicy, + budgets: ResourceBudgetCapabilities, + state: _AdmissionState, +) -> None: + fairness = policy.resource_fairness + if fairness.policy not in budgets.supported_fairness_policies: + state.gaps.append(f"unsupported participant resource fairness policy: {fairness.policy}") + state.gaps.extend(_parent_limit_gaps(policy)) + policy_pool_keys: set[tuple[str, ...]] = set() + for demand in policy.resource_demands: + _assess_demand(policy, demand, budgets, policy_pool_keys, state) + + +def _aggregate_pool_gaps(state: _AdmissionState) -> list[str]: + gaps: list[str] = [] + for key, required in state.aggregate_limits.items(): + pool = state.pools_by_key[key] if required > pool.capacity: gaps.append( f"participant resource pool {pool.pool_ref} aggregate policy limits require " f"{required}; configured capacity is {pool.capacity}" ) - protected = aggregate_protected_limits.get(pool_key, 0) + protected = state.aggregate_protected_limits.get(key, 0) if protected > pool.protected_capacity: gaps.append( f"participant resource pool {pool.pool_ref} protected policy limits require " @@ -197,4 +254,24 @@ def participant_resource_budget_gaps( return gaps +def participant_resource_budget_gaps( + manifest: BackendManifest, + capability: ParticipantRuntimeCapabilities, + policies: tuple[ResourceGovernedPolicy, ...], +) -> list[str]: + """Return atomic capacity, accounting, isolation, and fairness gaps.""" + + governed = tuple(policy for policy in policies if policy.profile == "participant-autonomous-execution/v3") + if not governed: + return [] + budgets = capability.resource_budgets + if budgets is None: + return ["participant-autonomous-execution/v3 requires resource-budget capabilities"] + state = _AdmissionState(gaps=_capability_gaps(manifest, budgets)) + for policy in governed: + _assess_policy(policy, budgets, state) + state.gaps.extend(_aggregate_pool_gaps(state)) + return state.gaps + + __all__ = ["participant_resource_budget_gaps"] diff --git a/implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py b/implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py index ef2465cbc..7886a44ac 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_resource_budgets.py @@ -32,6 +32,12 @@ from .participant_resource_types import ( require_quantity_semantics as _require_quantity_semantics, ) +from .participant_resource_validation import ( + pool_allocated_total, + validate_budget_capabilities, + validate_budget_policy, + validate_pool_allocations, +) class ParticipantResourceOwnerModel(ContractModel): @@ -104,77 +110,7 @@ class ParticipantResourceBudgetPolicyModel(ContractModel): @model_validator(mode="after") def _validate_policy(self) -> ParticipantResourceBudgetPolicyModel: - owner_ids = [owner.owner_id for owner in self.owners] - budget_ids = [demand.budget_id for demand in self.demands] - if len(owner_ids) != len(set(owner_ids)): - raise ValueError("resource-budget policy owner ids must be unique") - if len(budget_ids) != len(set(budget_ids)): - raise ValueError("resource-budget policy budget ids must be unique") - owners = {owner.owner_id: owner for owner in self.owners} - demands = {demand.budget_id: demand for demand in self.demands} - required_kinds = set(_RESOURCE_UNIT) - actual_kinds = {demand.quantity.resource_kind for demand in self.demands} - missing = sorted(required_kinds - actual_kinds) - if missing: - raise ValueError("resource-budget policy requires complete resource vector: " + ", ".join(missing)) - for demand in self.demands: - if demand.owner.owner_id not in owners or owners[demand.owner.owner_id] != demand.owner: - raise ValueError("resource-budget demand owner must resolve exactly in policy owners") - if demand.parent_budget_ref is not None and demand.parent_budget_ref not in demands: - raise ValueError("resource-budget demand parent must resolve in policy demands") - visiting: set[str] = set() - visited: set[str] = set() - - def visit(budget_id: str) -> None: - if budget_id in visiting: - raise ValueError("resource-budget policy parent graph must be acyclic") - if budget_id in visited: - return - visiting.add(budget_id) - demand = demands[budget_id] - if demand.parent_budget_ref is not None: - parent = demands[demand.parent_budget_ref] - if ( - demand.quantity.resource_kind, - demand.quantity.unit, - demand.quantity.accounting_mode, - demand.quantity.meter_profile_ref, - ) != ( - parent.quantity.resource_kind, - parent.quantity.unit, - parent.quantity.accounting_mode, - parent.quantity.meter_profile_ref, - ): - raise ValueError("resource-budget parent must use the same resource, unit, mode, and meter") - if demand.limit > parent.limit: - raise ValueError("resource-budget child limit cannot exceed its parent") - visit(demand.parent_budget_ref) - visiting.remove(budget_id) - visited.add(budget_id) - - for budget_id in demands: - visit(budget_id) - children_by_parent: dict[str, list[ParticipantResourceBudgetDemandModel]] = {} - for demand in self.demands: - if demand.parent_budget_ref is not None: - children_by_parent.setdefault(demand.parent_budget_ref, []).append(demand) - for parent_id, children in children_by_parent.items(): - if sum(child.limit for child in children) > demands[parent_id].limit: - raise ValueError("resource-budget sibling limits cannot exceed their parent limit") - pool_keys = [ - ( - demand.pool_ref, - demand.owner.kind, - demand.owner.owner_ref, - demand.quantity.resource_kind, - demand.quantity.unit, - demand.quantity.accounting_mode, - demand.quantity.meter_profile_ref, - ) - for demand in self.demands - ] - if len(pool_keys) != len(set(pool_keys)): - raise ValueError("resource-budget demands cannot alias the same canonical resource pool") + validate_budget_policy(self.owners, self.demands, set(_RESOURCE_UNIT)) return self @@ -229,42 +165,7 @@ class ParticipantResourceBudgetCapabilitiesModel(ContractModel): @model_validator(mode="after") def _validate_capabilities(self) -> ParticipantResourceBudgetCapabilitiesModel: - for field_name in ( - "supported_owner_kinds", - "supported_resource_kinds", - "supported_accounting_modes", - "supported_reset_modes", - "supported_fairness_policies", - "supported_isolation_strengths", - "realization_contract_ids", - "cross_range_pool_refs", - ): - values = getattr(self, field_name) - if len(values) != len(set(values)): - raise ValueError(f"{field_name} must be unique") - keys = [(pool.pool_ref, pool.resource_kind, pool.meter_profile_ref) for pool in self.configured_pools] - if len(keys) != len(set(keys)): - raise ValueError("configured pool resource entries must be unique") - pools_by_ref = { - pool_ref: tuple(pool for pool in self.configured_pools if pool.pool_ref == pool_ref) - for pool_ref in self.cross_range_pool_refs - } - for _pool_ref, pools in pools_by_ref.items(): - if not pools: - raise ValueError("cross-range pool ref must resolve") - if any(pool.tenant_isolation != "tenant_partitioned" for pool in pools): - raise ValueError("cross-range shared pools require tenant_partitioned isolation") - for pool in self.configured_pools: - if pool.owner_kind not in self.supported_owner_kinds: - raise ValueError("configured pool owner kind is not declared supported") - if pool.resource_kind not in self.supported_resource_kinds: - raise ValueError("configured pool resource kind is not declared supported") - if pool.accounting_mode not in self.supported_accounting_modes: - raise ValueError("configured pool accounting mode is not declared supported") - if pool.fairness_policy not in self.supported_fairness_policies: - raise ValueError("configured pool fairness policy is not declared supported") - if pool.tenant_isolation not in self.supported_isolation_strengths: - raise ValueError("configured pool isolation strength is not declared supported") + validate_budget_capabilities(self) return self @@ -407,27 +308,8 @@ def _validate_pool_state(self) -> ParticipantResourcePoolStateModel: raise ValueError("pool_state_ref must equal the canonical exact-pool identity") if self.protected_capacity > self.capacity: raise ValueError("protected capacity cannot exceed physical pool capacity") - for allocation_ref, allocation in self.allocations.items(): - if allocation_ref != allocation.budget_state_ref: - raise ValueError("pool allocation map key must equal budget_state_ref") - if allocation.priority_class not in self.priority_classes: - raise ValueError("pool allocation priority class must be configured") - if allocation.borrowing != self.borrowing or allocation.reclaim != self.reclaim: - raise ValueError("pool allocation borrowing and reclaim must match pool authority") - if ( - self.max_queue_ticks > allocation.max_queue_ticks - or self.starvation_bound_ticks > allocation.starvation_bound_ticks - ): - raise ValueError("pool allocation fairness bounds are weaker than required") - total = sum( - ( - allocation.current_use + allocation.reserved - if self.accounting_mode in {"reservable_gauge", "lease"} - else allocation.cumulative_use + allocation.reserved - ) - for allocation in self.allocations.values() - ) - if total > self.capacity: + validate_pool_allocations(self) + if pool_allocated_total(self) > self.capacity: raise ValueError("physical pool allocations cannot exceed capacity") return self diff --git a/implementations/python/packages/raes_contracts/contracts/participant_resource_validation.py b/implementations/python/packages/raes_contracts/contracts/participant_resource_validation.py new file mode 100644 index 000000000..215f1b0e4 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_resource_validation.py @@ -0,0 +1,230 @@ +"""Shared validation helpers for participant resource-budget contracts.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Protocol + + +class _Quantity(Protocol): + resource_kind: str + unit: str + accounting_mode: str + meter_profile_ref: str + + +class _Owner(Protocol): + owner_id: str + kind: str + owner_ref: str + + +class _Demand(Protocol): + budget_id: str + owner: _Owner + pool_ref: str + quantity: _Quantity + limit: int + parent_budget_ref: str | None + + +class _Pool(Protocol): + pool_ref: str + owner_kind: str + resource_kind: str + accounting_mode: str + meter_profile_ref: str + tenant_isolation: str + fairness_policy: str + + +class _Capabilities(Protocol): + supported_owner_kinds: Sequence[str] + supported_resource_kinds: Sequence[str] + supported_accounting_modes: Sequence[str] + supported_reset_modes: Sequence[str] + supported_fairness_policies: Sequence[str] + supported_isolation_strengths: Sequence[str] + realization_contract_ids: Sequence[str] + cross_range_pool_refs: Sequence[str] + configured_pools: Sequence[_Pool] + + +class _Allocation(Protocol): + budget_state_ref: str + priority_class: str + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + current_use: int + reserved: int + cumulative_use: int + + +class _PoolState(Protocol): + priority_classes: Sequence[str] + borrowing: str + reclaim: str + max_queue_ticks: int + starvation_bound_ticks: int + accounting_mode: str + capacity: int + allocations: Mapping[str, _Allocation] + + +def _quantity_identity(quantity: _Quantity) -> tuple[str, ...]: + return ( + quantity.resource_kind, + quantity.unit, + quantity.accounting_mode, + quantity.meter_profile_ref, + ) + + +def _visit_demand( + demands: Mapping[str, _Demand], + budget_id: str, + visiting: set[str], + visited: set[str], +) -> None: + if budget_id in visiting: + raise ValueError("resource-budget policy parent graph must be acyclic") + if budget_id in visited: + return + visiting.add(budget_id) + demand = demands[budget_id] + parent_ref = demand.parent_budget_ref + if parent_ref is not None: + parent = demands[parent_ref] + if _quantity_identity(demand.quantity) != _quantity_identity(parent.quantity): + raise ValueError("resource-budget parent must use the same resource, unit, mode, and meter") + if demand.limit > parent.limit: + raise ValueError("resource-budget child limit cannot exceed its parent") + _visit_demand(demands, parent_ref, visiting, visited) + visiting.remove(budget_id) + visited.add(budget_id) + + +def _validate_parent_limits(demands: Mapping[str, _Demand]) -> None: + visiting: set[str] = set() + visited: set[str] = set() + for budget_id in demands: + _visit_demand(demands, budget_id, visiting, visited) + children_by_parent: dict[str, list[_Demand]] = {} + for demand in demands.values(): + if demand.parent_budget_ref is not None: + children_by_parent.setdefault(demand.parent_budget_ref, []).append(demand) + for parent_id, children in children_by_parent.items(): + if sum(child.limit for child in children) > demands[parent_id].limit: + raise ValueError("resource-budget sibling limits cannot exceed their parent limit") + + +def validate_budget_policy( + owners: Sequence[_Owner], + demands: Sequence[_Demand], + required_resource_kinds: set[str], +) -> None: + owner_ids = [owner.owner_id for owner in owners] + budget_ids = [demand.budget_id for demand in demands] + if len(owner_ids) != len(set(owner_ids)): + raise ValueError("resource-budget policy owner ids must be unique") + if len(budget_ids) != len(set(budget_ids)): + raise ValueError("resource-budget policy budget ids must be unique") + owners_by_id = {owner.owner_id: owner for owner in owners} + demands_by_id = {demand.budget_id: demand for demand in demands} + missing = sorted(required_resource_kinds - {demand.quantity.resource_kind for demand in demands}) + if missing: + raise ValueError("resource-budget policy requires complete resource vector: " + ", ".join(missing)) + for demand in demands: + if demand.owner.owner_id not in owners_by_id or owners_by_id[demand.owner.owner_id] != demand.owner: + raise ValueError("resource-budget demand owner must resolve exactly in policy owners") + if demand.parent_budget_ref is not None and demand.parent_budget_ref not in demands_by_id: + raise ValueError("resource-budget demand parent must resolve in policy demands") + _validate_parent_limits(demands_by_id) + pool_keys = [ + ( + demand.pool_ref, + demand.owner.kind, + demand.owner.owner_ref, + *_quantity_identity(demand.quantity), + ) + for demand in demands + ] + if len(pool_keys) != len(set(pool_keys)): + raise ValueError("resource-budget demands cannot alias the same canonical resource pool") + + +def _require_unique_fields(capabilities: _Capabilities) -> None: + field_names = ( + "supported_owner_kinds", + "supported_resource_kinds", + "supported_accounting_modes", + "supported_reset_modes", + "supported_fairness_policies", + "supported_isolation_strengths", + "realization_contract_ids", + "cross_range_pool_refs", + ) + for field_name in field_names: + values = getattr(capabilities, field_name) + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must be unique") + + +def _validate_cross_range_pools(capabilities: _Capabilities) -> None: + for pool_ref in capabilities.cross_range_pool_refs: + pools = tuple(pool for pool in capabilities.configured_pools if pool.pool_ref == pool_ref) + if not pools: + raise ValueError("cross-range pool ref must resolve") + if any(pool.tenant_isolation != "tenant_partitioned" for pool in pools): + raise ValueError("cross-range shared pools require tenant_partitioned isolation") + + +def _validate_configured_pool(pool: _Pool, capabilities: _Capabilities) -> None: + supported_fields = ( + ("owner kind", pool.owner_kind, capabilities.supported_owner_kinds), + ("resource kind", pool.resource_kind, capabilities.supported_resource_kinds), + ("accounting mode", pool.accounting_mode, capabilities.supported_accounting_modes), + ("fairness policy", pool.fairness_policy, capabilities.supported_fairness_policies), + ("isolation strength", pool.tenant_isolation, capabilities.supported_isolation_strengths), + ) + for label, value, supported in supported_fields: + if value not in supported: + raise ValueError(f"configured pool {label} is not declared supported") + + +def validate_budget_capabilities(capabilities: _Capabilities) -> None: + _require_unique_fields(capabilities) + keys = [(pool.pool_ref, pool.resource_kind, pool.meter_profile_ref) for pool in capabilities.configured_pools] + if len(keys) != len(set(keys)): + raise ValueError("configured pool resource entries must be unique") + _validate_cross_range_pools(capabilities) + for pool in capabilities.configured_pools: + _validate_configured_pool(pool, capabilities) + + +def validate_pool_allocations(pool: _PoolState) -> None: + for allocation_ref, allocation in pool.allocations.items(): + if allocation_ref != allocation.budget_state_ref: + raise ValueError("pool allocation map key must equal budget_state_ref") + if allocation.priority_class not in pool.priority_classes: + raise ValueError("pool allocation priority class must be configured") + if allocation.borrowing != pool.borrowing or allocation.reclaim != pool.reclaim: + raise ValueError("pool allocation borrowing and reclaim must match pool authority") + if ( + pool.max_queue_ticks > allocation.max_queue_ticks + or pool.starvation_bound_ticks > allocation.starvation_bound_ticks + ): + raise ValueError("pool allocation fairness bounds are weaker than required") + + +def pool_allocated_total(pool: _PoolState) -> int: + return sum( + ( + allocation.current_use + allocation.reserved + if pool.accounting_mode in {"reservable_gauge", "lease"} + else allocation.cumulative_use + allocation.reserved + ) + for allocation in pool.allocations.values() + ) diff --git a/implementations/python/packages/raes_contracts/contracts/realization_plans.py b/implementations/python/packages/raes_contracts/contracts/realization_plans.py index 9321bd0cb..048f3f8a5 100644 --- a/implementations/python/packages/raes_contracts/contracts/realization_plans.py +++ b/implementations/python/packages/raes_contracts/contracts/realization_plans.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Annotated, Any, Literal from pydantic import Field, model_validator @@ -148,6 +149,48 @@ class RealizationProvenanceEntryModel(ContractModel): governing_scope: NonEmptyString | None = None +def _require_embedded_map_keys( + values: Mapping[str, object], + attribute: str, + message: str, +) -> None: + for map_key, value in values.items(): + if map_key != getattr(value, attribute): + raise ValueError(message) + + +def _validate_execution_service_budget_projection( + services: Mapping[str, ParticipantExecutionServiceStateModel], + budget_states: Mapping[str, ParticipantResourceBudgetStateModel], +) -> None: + budget_refs = set(budget_states) + for service in services.values(): + missing = sorted(set(service.resource_budget_state_refs) - budget_refs) + if missing: + raise ValueError( + "Participant execution service references missing resource-budget states: " + ", ".join(missing) + ) + concurrency = [ + budget_states[budget_ref] + for budget_ref in service.resource_budget_state_refs + if budget_states[budget_ref].resource_kind == "concurrent_actions" + ] + if not concurrency: + continue + if len(concurrency) != 1: + raise ValueError( + "Participant execution service must reference exactly one authoritative concurrency budget" + ) + authoritative = concurrency[0] + projection = (service.capacity, service.reserved, service.in_flight) + authority = (authoritative.limit, authoritative.reserved, authoritative.current_use) + if projection != authority: + raise ValueError( + "Participant execution service concurrency projection must " + "equal its authoritative resource-budget state" + ) + + class RuntimeSnapshotEnvelopeModel(ContractModel): """Published envelope for a live runtime snapshot. @@ -191,54 +234,45 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): @model_validator(mode="after") def _validate_entry_addresses(self) -> RuntimeSnapshotEnvelopeModel: - for map_key, entry in self.entries.items(): - if map_key != entry.address: - raise ValueError("Runtime snapshot entries map key must equal embedded address") + _require_embedded_map_keys( + self.entries, + "address", + "Runtime snapshot entries map key must equal embedded address", + ) for map_key, state in self.participant_autonomous_execution_states.items(): expected = f"{state.policy_address}.state.{state.participant_address}" if map_key != expected: raise ValueError( "Autonomous participant state map key must equal the embedded policy and participant address" ) - for map_key, state in self.participant_execution_services.items(): - if map_key != state.execution_scope_ref: - raise ValueError("Participant execution service map key must equal execution_scope_ref") - for map_key, state in self.participant_resource_budget_states.items(): - if map_key != state.state_ref: - raise ValueError("Participant resource-budget state map key must equal state_ref") - for map_key, state in self.participant_resource_pool_states.items(): - if map_key != state.pool_state_ref: - raise ValueError("Participant resource-pool state map key must equal pool_state_ref") - for map_key, event in self.participant_resource_budget_events.items(): - if map_key != event.event_id: - raise ValueError("Participant resource-budget event map key must equal event_id") - budget_refs = set(self.participant_resource_budget_states) - for service in self.participant_execution_services.values(): - missing = sorted(set(service.resource_budget_state_refs) - budget_refs) - if missing: - raise ValueError( - "Participant execution service references missing resource-budget states: " + ", ".join(missing) - ) - concurrency = [ - self.participant_resource_budget_states[budget_id] - for budget_id in service.resource_budget_state_refs - if self.participant_resource_budget_states[budget_id].resource_kind == "concurrent_actions" - ] - if concurrency: - if len(concurrency) != 1: - raise ValueError( - "Participant execution service must reference exactly one authoritative concurrency budget" - ) - authoritative = concurrency[0] - if ( - service.capacity != authoritative.limit - or service.reserved != authoritative.reserved - or service.in_flight != authoritative.current_use - ): - raise ValueError( - "Participant execution service concurrency projection must " - "equal its authoritative resource-budget state" - ) + key_checks = ( + ( + self.participant_execution_services, + "execution_scope_ref", + "Participant execution service map key must equal execution_scope_ref", + ), + ( + self.participant_resource_budget_states, + "state_ref", + "Participant resource-budget state map key must equal state_ref", + ), + ( + self.participant_resource_pool_states, + "pool_state_ref", + "Participant resource-pool state map key must equal pool_state_ref", + ), + ( + self.participant_resource_budget_events, + "event_id", + "Participant resource-budget event map key must equal event_id", + ), + ) + for values, attribute, message in key_checks: + _require_embedded_map_keys(values, attribute, message) + _validate_execution_service_budget_projection( + self.participant_execution_services, + self.participant_resource_budget_states, + ) return self diff --git a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py index 3bd5d7571..4a498ec6c 100644 --- a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py +++ b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py @@ -32,16 +32,18 @@ def _resource_owner_address( matching = tuple(address for address in participant_addresses if address.endswith(f".{ref}")) if len(matching) != 1: raise ValueError("participant resource owner must resolve to one policy participant") - return matching[0] - if kind == "deployment_tenant": + address = matching[0] + elif kind == "deployment_tenant": name = _section_ref_name(ref, "deployment_tenants", scenario.deployment_tenants) - return _address("deployment", "tenant", name) - if kind == "shared_service": + address = _address("deployment", "tenant", name) + elif kind == "shared_service": resolved = _resolve_node_service_ref(scenario, ref) if resolved is None: raise ValueError("shared-service resource owner must resolve to one node service") - return _address("provision", "node", resolved[0], "service", resolved[1]) - return ref + address = _address("provision", "node", resolved[0], "service", resolved[1]) + else: + address = ref + return address def _legacy_resource_demands( diff --git a/implementations/python/packages/raes_runtime/participant_resource_accounting.py b/implementations/python/packages/raes_runtime/participant_resource_accounting.py index 4f76b5097..00fe4f14b 100644 --- a/implementations/python/packages/raes_runtime/participant_resource_accounting.py +++ b/implementations/python/packages/raes_runtime/participant_resource_accounting.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import dataclass from raes_contracts.contracts.participant_resource_budgets import ( ParticipantResourceBudgetEventModel, @@ -23,6 +24,19 @@ release as release_pool_allocation, ) +_STALE_GENERATION_CODE = "runtime.participant-resource-stale-generation" + + +@dataclass +class _CommitMutation: + snapshot: RuntimeSnapshot + operation_id: str + execution_generation: int + evidence_refs: tuple[str, ...] + states: dict[str, dict[str, object]] + pool_states: dict[str, dict[str, object]] + events: dict[str, dict[str, object]] + def _diagnostic(code: str, policy_address: str, message: str) -> Diagnostic: return Diagnostic( @@ -76,40 +90,40 @@ def _operation_reservations( ) -def commit_participant_resource_reservation( +def _missing_reservation_result( snapshot: RuntimeSnapshot, - *, operation_id: str, - execution_generation: int, - measured_quantities: Mapping[str, int], - evidence_refs: tuple[str, ...], + events: Mapping[str, Mapping[str, object]], ) -> ApplyResult: - """Commit trusted, complete measurements for an operation exactly once.""" + committed = any( + payload.get("operation_id") == operation_id and payload.get("transition") == "commit" + for payload in events.values() + ) + diagnostics = ( + [] + if committed + else [ + _diagnostic( + "runtime.participant-resource-reservation-missing", + "unknown", + f"operation {operation_id} has no resource reservation", + ) + ] + ) + return ApplyResult( + success=committed, + snapshot=snapshot, + diagnostics=diagnostics, + ) - events = dict(snapshot.participant_resource_budget_events) - reservations = _operation_reservations(events, operation_id) - if not reservations: - committed = any( - payload.get("operation_id") == operation_id and payload.get("transition") == "commit" - for payload in events.values() - ) - return ApplyResult( - success=committed, - snapshot=snapshot, - diagnostics=( - [] - if committed - else [ - _diagnostic( - "runtime.participant-resource-reservation-missing", - "unknown", - f"operation {operation_id} has no resource reservation", - ) - ] - ), - ) + +def _valid_measurement_vector( + reservations: tuple[ParticipantResourceBudgetEventModel, ...], + measured_quantities: Mapping[str, int], + evidence_refs: tuple[str, ...], +) -> bool: expected_refs = {reservation.budget_state_ref for reservation in reservations} - valid_vector = ( + return ( set(measured_quantities) == expected_refs and bool(evidence_refs) and all( @@ -117,88 +131,139 @@ def commit_participant_resource_reservation( for reservation in reservations ) ) - if not valid_vector: + + +def _invalid_measurement_result( + snapshot: RuntimeSnapshot, + policy_address: str, +) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + _diagnostic( + "runtime.participant-resource-measurement-invalid", + policy_address, + "resource commit requires an exact, bounded measured vector and native evidence", + ) + ], + ) + + +def _commit_reservation( + mutation: _CommitMutation, + reservation: ParticipantResourceBudgetEventModel, + measured: int, +) -> ApplyResult | None: + commit_id = f"{mutation.operation_id}:{reservation.budget_state_ref}:commit" + if commit_id in mutation.events: + return None + current = _state(mutation.states[reservation.budget_state_ref]) + if current.generation != mutation.execution_generation: return ApplyResult( success=False, - snapshot=snapshot, + snapshot=mutation.snapshot, diagnostics=[ _diagnostic( - "runtime.participant-resource-measurement-invalid", - reservations[0].policy_address, - "resource commit requires an exact, bounded measured vector and native evidence", + _STALE_GENERATION_CODE, + reservation.policy_address, + f"operation {mutation.operation_id} cannot commit across a generation boundary", ) ], ) - states = dict(snapshot.participant_resource_budget_states) - pool_states = dict(snapshot.participant_resource_pool_states) - for reservation in reservations: - commit_id = f"{operation_id}:{reservation.budget_state_ref}:commit" - if commit_id in events: - continue - current = _state(states[reservation.budget_state_ref]) - if current.generation != execution_generation: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-stale-generation", - reservation.policy_address, - f"operation {operation_id} cannot commit across a generation boundary", - ) - ], - ) - measured = measured_quantities[reservation.budget_state_ref] - committed = current.model_copy( - update={ - "reserved": max(0, current.reserved - reservation.requested), - "cumulative_use": current.cumulative_use + measured, - "last_event_ref": commit_id, - "evidence_refs": tuple(dict.fromkeys((*current.evidence_refs, *evidence_refs))), - } - ) - event = ParticipantResourceBudgetEventModel( - event_id=commit_id, - operation_id=operation_id, - budget_state_ref=reservation.budget_state_ref, - budget_id=reservation.budget_id, - policy_address=reservation.policy_address, - owner_ref=current.owner_ref, - pool_ref=current.pool_ref, - execution_generation=execution_generation, - transition="commit", - disposition="committed", - requested=reservation.requested, + committed = current.model_copy( + update={ + "reserved": max(0, current.reserved - reservation.requested), + "cumulative_use": current.cumulative_use + measured, + "last_event_ref": commit_id, + "evidence_refs": tuple(dict.fromkeys((*current.evidence_refs, *mutation.evidence_refs))), + } + ) + event = ParticipantResourceBudgetEventModel( + event_id=commit_id, + operation_id=mutation.operation_id, + budget_state_ref=reservation.budget_state_ref, + budget_id=reservation.budget_id, + policy_address=reservation.policy_address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=mutation.execution_generation, + transition="commit", + disposition="committed", + requested=reservation.requested, + measured=measured, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=reservation.event_id, + evidence_refs=mutation.evidence_refs, + ) + mutation.states[reservation.budget_state_ref] = _payload(committed) + physical_pool_ref = _pool_ref_for_state(current) + physical_pool = _pool_state(mutation.pool_states[physical_pool_ref]) + mutation.pool_states[physical_pool_ref] = _payload( + commit_pool_allocation( + physical_pool, + current.state_ref, + reserved=reservation.requested, measured=measured, - resource_kind=current.resource_kind, - unit=current.unit, - meter_profile_ref=current.meter_profile_ref, - predecessor_event_ref=reservation.event_id, - evidence_refs=evidence_refs, - ) - states[reservation.budget_state_ref] = _payload(committed) - physical_pool_ref = _pool_ref_for_state(current) - physical_pool = _pool_state(pool_states[physical_pool_ref]) - pool_states[physical_pool_ref] = _payload( - commit_pool_allocation( - physical_pool, - current.state_ref, - reserved=reservation.requested, - measured=measured, - ) ) - events[commit_id] = _payload(event) + ) + mutation.events[commit_id] = _payload(event) + return None + + +def _commit_success_result(mutation: _CommitMutation) -> ApplyResult: return ApplyResult( success=True, - snapshot=snapshot.with_entries( - dict(snapshot.entries), - participant_resource_budget_states=states, - participant_resource_pool_states=pool_states, - participant_resource_budget_events=events, + snapshot=mutation.snapshot.with_entries( + dict(mutation.snapshot.entries), + participant_resource_budget_states=mutation.states, + participant_resource_pool_states=mutation.pool_states, + participant_resource_budget_events=mutation.events, ), ) +def commit_participant_resource_reservation( + snapshot: RuntimeSnapshot, + *, + operation_id: str, + execution_generation: int, + measured_quantities: Mapping[str, int], + evidence_refs: tuple[str, ...], +) -> ApplyResult: + """Commit trusted, complete measurements for an operation exactly once.""" + + events = dict(snapshot.participant_resource_budget_events) + reservations = _operation_reservations(events, operation_id) + if not reservations: + result = _missing_reservation_result(snapshot, operation_id, events) + elif not _valid_measurement_vector(reservations, measured_quantities, evidence_refs): + result = _invalid_measurement_result(snapshot, reservations[0].policy_address) + else: + mutation = _CommitMutation( + snapshot=snapshot, + operation_id=operation_id, + execution_generation=execution_generation, + evidence_refs=evidence_refs, + states=dict(snapshot.participant_resource_budget_states), + pool_states=dict(snapshot.participant_resource_pool_states), + events=events, + ) + failure = None + for reservation in reservations: + failure = _commit_reservation( + mutation, + reservation, + measured_quantities[reservation.budget_state_ref], + ) + if failure is not None: + break + result = failure or _commit_success_result(mutation) + return result + + def release_participant_resource_reservation( snapshot: RuntimeSnapshot, *, @@ -225,7 +290,7 @@ def release_participant_resource_reservation( snapshot=snapshot, diagnostics=[ _diagnostic( - "runtime.participant-resource-stale-generation", + _STALE_GENERATION_CODE, reservation.policy_address, f"operation {operation_id} cannot release across a generation boundary", ) @@ -302,7 +367,7 @@ def reconcile_participant_resource_budgets( snapshot=snapshot, diagnostics=[ _diagnostic( - "runtime.participant-resource-stale-generation", + _STALE_GENERATION_CODE, policy_address, f"resource budget {state_ref} cannot reconcile from generation {current_generation}", ) diff --git a/implementations/python/packages/raes_runtime/participant_resource_budgets.py b/implementations/python/packages/raes_runtime/participant_resource_budgets.py index 9a26b548a..3cfb6ef8c 100644 --- a/implementations/python/packages/raes_runtime/participant_resource_budgets.py +++ b/implementations/python/packages/raes_runtime/participant_resource_budgets.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence +from dataclasses import dataclass from typing import Protocol from raes_contracts.contracts.participant_resource_budgets import ( @@ -10,22 +11,15 @@ ParticipantResourceBudgetStateModel, ParticipantResourcePoolStateModel, participant_resource_budget_state_ref, - participant_resource_pool_state_ref, ) from raes_contracts.diagnostics import Diagnostic from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot -from .participant_resource_pool_ledger import ( - can_reserve as pool_can_reserve, -) from .participant_resource_pool_ledger import ( ensure_allocation, new_pool_state, pool_state_ref, ) -from .participant_resource_pool_ledger import ( - reserve as reserve_pool_allocation, -) class ResourceDemand(Protocol): @@ -80,6 +74,14 @@ class ResourceCapabilities(Protocol): configured_pools: tuple[ResourcePool, ...] +@dataclass +class _InitializationMutation: + snapshot: RuntimeSnapshot + execution_generation: int + states: dict[str, dict[str, object]] + pool_states: dict[str, dict[str, object]] + + def _diagnostic(code: str, policy_address: str, message: str) -> Diagnostic: return Diagnostic( code=code, @@ -122,87 +124,89 @@ def _matching_pool(demand: ResourceDemand, capabilities: ResourceCapabilities) - ) -def initialize_participant_resource_budgets( - snapshot: RuntimeSnapshot, - policies: Sequence[ResourcePolicy], - capabilities: ResourceCapabilities, - *, - execution_generation: int, +def _initialization_failure( + mutation: _InitializationMutation, + policy: ResourcePolicy, + code: str, + message: str, ) -> ApplyResult: - """Materialize policy budgets and authoritative physical-pool allocations.""" + return ApplyResult( + success=False, + snapshot=mutation.snapshot, + diagnostics=[_diagnostic(code, policy.address, message)], + ) - states = dict(snapshot.participant_resource_budget_states) - pool_states = dict(snapshot.participant_resource_pool_states) - for policy in policies: - for demand in policy.resource_demands: - pool = _matching_pool(demand, capabilities) - if pool is None: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-capacity-missing", - policy.address, - f"no exact configured capacity matches resource budget {demand.budget_id}", - ) - ], - ) - if policy.resource_fairness.protected and pool.protected_capacity < demand.reservation: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-protected-capacity-missing", - policy.address, - f"resource budget {demand.budget_id} lacks its protected reservation", - ) - ], + +def _new_budget_state( + policy: ResourcePolicy, + demand: ResourceDemand, + pool: ResourcePool, + execution_generation: int, +) -> ParticipantResourceBudgetStateModel: + state_ref = participant_resource_budget_state_ref(policy.address, demand.budget_id) + return ParticipantResourceBudgetStateModel( + state_ref=state_ref, + budget_id=demand.budget_id, + policy_address=policy.address, + owner_kind=demand.owner_kind, + owner_ref=demand.owner_address, + pool_ref=demand.pool_ref, + resource_kind=demand.resource_kind, + unit=demand.unit, + accounting_mode=demand.accounting_mode, + meter_profile_ref=demand.meter_profile_ref, + reset=demand.reset, + generation=execution_generation, + limit=demand.limit, + configured_capacity=pool.capacity, + reserved=0, + current_use=0, + cumulative_use=0, + throttled=0, + rejected=0, + reconciliation_status="reconciled", + last_event_ref=f"initial:{state_ref}", + ) + + +def _initialize_demand( + mutation: _InitializationMutation, + policy: ResourcePolicy, + demand: ResourceDemand, + capabilities: ResourceCapabilities, +) -> ApplyResult | None: + failure = None + pool = _matching_pool(demand, capabilities) + if pool is None: + failure = _initialization_failure( + mutation, + policy, + "runtime.participant-resource-capacity-missing", + f"no exact configured capacity matches resource budget {demand.budget_id}", + ) + elif policy.resource_fairness.protected and pool.protected_capacity < demand.reservation: + failure = _initialization_failure( + mutation, + policy, + "runtime.participant-resource-protected-capacity-missing", + f"resource budget {demand.budget_id} lacks its protected reservation", + ) + else: + state_ref = participant_resource_budget_state_ref(policy.address, demand.budget_id) + existing = mutation.states.get(state_ref) + if existing is not None: + current = _state(existing) + if current.generation != mutation.execution_generation: + failure = _initialization_failure( + mutation, + policy, + "runtime.participant-resource-state-conflict", + f"resource budget {demand.budget_id} already has incompatible state", ) - state_ref = participant_resource_budget_state_ref(policy.address, demand.budget_id) - existing = states.get(state_ref) - if existing is not None: - current = _state(existing) - if current.generation != execution_generation: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-state-conflict", - policy.address, - f"resource budget {demand.budget_id} already has incompatible state", - ) - ], - ) - continue - budget_state = ParticipantResourceBudgetStateModel( - state_ref=state_ref, - budget_id=demand.budget_id, - policy_address=policy.address, - owner_kind=demand.owner_kind, - owner_ref=demand.owner_address, - pool_ref=demand.pool_ref, - resource_kind=demand.resource_kind, - unit=demand.unit, - accounting_mode=demand.accounting_mode, - meter_profile_ref=demand.meter_profile_ref, - reset=demand.reset, - generation=execution_generation, - limit=demand.limit, - configured_capacity=pool.capacity, - reserved=0, - current_use=0, - cumulative_use=0, - throttled=0, - rejected=0, - reconciliation_status="reconciled", - last_event_ref=f"initial:{state_ref}", - ) - states[state_ref] = _payload(budget_state) + else: + budget_state = _new_budget_state(policy, demand, pool, mutation.execution_generation) exact_pool_ref = pool_state_ref(pool) - existing_pool = pool_states.get(exact_pool_ref) + existing_pool = mutation.pool_states.get(exact_pool_ref) physical_pool = new_pool_state(pool) if existing_pool is None else _pool_state(existing_pool) try: physical_pool = ensure_allocation( @@ -211,111 +215,53 @@ def initialize_participant_resource_budgets( fairness=policy.resource_fairness, ) except ValueError as exc: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-pool-conflict", - policy.address, - str(exc), - ) - ], + failure = _initialization_failure( + mutation, + policy, + "runtime.participant-resource-pool-conflict", + str(exc), ) - pool_states[exact_pool_ref] = _payload(physical_pool) - return ApplyResult( - success=True, - snapshot=snapshot.with_entries( - dict(snapshot.entries), - participant_resource_budget_states=states, - participant_resource_pool_states=pool_states, - ), - ) - - -def _reservation_event_id(operation_id: str, state_ref: str) -> str: - return f"{operation_id}:{state_ref}:reserve" - - -def _used_capacity(state: ParticipantResourceBudgetStateModel) -> int: - if state.accounting_mode in {"reservable_gauge", "lease"}: - return state.current_use + state.reserved - return state.cumulative_use + state.reserved - - -def _pool_ref_for_state(state: ParticipantResourceBudgetStateModel) -> str: - return participant_resource_pool_state_ref( - pool_ref=state.pool_ref, - owner_kind=state.owner_kind, - owner_ref=state.owner_ref, - resource_kind=state.resource_kind, - unit=state.unit, - accounting_mode=state.accounting_mode, - meter_profile_ref=state.meter_profile_ref, - ) + if failure is None: + mutation.states[state_ref] = _payload(budget_state) + mutation.pool_states[exact_pool_ref] = _payload(physical_pool) + return failure -def _throttled_result( +def initialize_participant_resource_budgets( snapshot: RuntimeSnapshot, - states: dict[str, dict[str, object]], - pool_states: dict[str, dict[str, object]], - events: dict[str, dict[str, object]], + policies: Sequence[ResourcePolicy], + capabilities: ResourceCapabilities, *, - policy: ResourcePolicy, - demand: ResourceDemand, - current: ParticipantResourceBudgetStateModel, - operation_id: str, execution_generation: int, - amount: int, - budget_available: bool, ) -> ApplyResult: - event_id = f"{operation_id}:{current.state_ref}:throttle" - states[current.state_ref] = _payload( - current.model_copy( - update={ - "throttled": current.throttled + 1, - "last_event_ref": event_id, - } - ) + """Materialize policy budgets and authoritative physical-pool allocations.""" + + mutation = _InitializationMutation( + snapshot=snapshot, + execution_generation=execution_generation, + states=dict(snapshot.participant_resource_budget_states), + pool_states=dict(snapshot.participant_resource_pool_states), ) - events[event_id] = _payload( - ParticipantResourceBudgetEventModel( - event_id=event_id, - operation_id=operation_id, - budget_state_ref=current.state_ref, - budget_id=demand.budget_id, - policy_address=policy.address, - owner_ref=current.owner_ref, - pool_ref=current.pool_ref, - execution_generation=execution_generation, - transition="throttle", - disposition="throttled", - requested=amount, - resource_kind=current.resource_kind, - unit=current.unit, - meter_profile_ref=current.meter_profile_ref, - predecessor_event_ref=current.last_event_ref, + failure = None + for policy in policies: + for demand in policy.resource_demands: + failure = _initialize_demand(mutation, policy, demand, capabilities) + if failure is not None: + break + if failure is not None: + break + if failure is not None: + result = failure + else: + result = ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_resource_budget_states=mutation.states, + participant_resource_pool_states=mutation.pool_states, + ), ) - ) - return ApplyResult( - success=False, - snapshot=snapshot.with_entries( - dict(snapshot.entries), - participant_resource_budget_states=states, - participant_resource_pool_states=pool_states, - participant_resource_budget_events=events, - ), - diagnostics=[ - _diagnostic( - "runtime.participant-resource-throttled", - policy.address, - ( - f"resource budget {demand.budget_id} has insufficient " - f"{'logical budget' if not budget_available else 'shared pool'} capacity" - ), - ) - ], - ) + return result def reserve_participant_resources( @@ -328,133 +274,14 @@ def reserve_participant_resources( ) -> ApplyResult: """Reserve a policy's complete resource vector or reserve none of it.""" - events = dict(snapshot.participant_resource_budget_events) - state_refs = [ - participant_resource_budget_state_ref(policy.address, demand.budget_id) for demand in policy.resource_demands - ] - event_ids = [_reservation_event_id(operation_id, state_ref) for state_ref in state_refs] - if event_ids and all(event_id in events for event_id in event_ids): - return ApplyResult(success=True, snapshot=snapshot) - states = dict(snapshot.participant_resource_budget_states) - pool_states = dict(snapshot.participant_resource_pool_states) - checked: list[ - tuple[ - ResourceDemand, - ParticipantResourceBudgetStateModel, - ParticipantResourcePoolStateModel, - int, - ] - ] = [] - for demand, state_ref in zip(policy.resource_demands, state_refs, strict=True): - raw = states.get(state_ref) - if raw is None: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-state-missing", - policy.address, - f"resource budget {demand.budget_id} was not initialized", - ) - ], - ) - current = _state(raw) - if current.generation != execution_generation: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-stale-generation", - policy.address, - ( - f"resource budget {demand.budget_id} is generation {current.generation}; " - f"request is generation {execution_generation}" - ), - ) - ], - ) - amount = ( - requested_quantities.get(demand.budget_id, demand.reservation) - if requested_quantities is not None - else demand.reservation - ) - if amount < 0: - raise ValueError("requested participant resource quantities must be non-negative") - exact_pool_ref = _pool_ref_for_state(current) - raw_pool = pool_states.get(exact_pool_ref) - if raw_pool is None: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - _diagnostic( - "runtime.participant-resource-pool-state-missing", - policy.address, - f"physical pool for resource budget {demand.budget_id} was not initialized", - ) - ], - ) - physical_pool = _pool_state(raw_pool) - budget_available = _used_capacity(current) + amount <= min( - current.limit, - current.configured_capacity, - ) - if not budget_available or not pool_can_reserve(physical_pool, state_ref, amount): - return _throttled_result( - snapshot, - states, - pool_states, - events, - policy=policy, - demand=demand, - current=current, - operation_id=operation_id, - execution_generation=execution_generation, - amount=amount, - budget_available=budget_available, - ) - checked.append((demand, current, physical_pool, amount)) - for demand, current, physical_pool, amount in checked: - event_id = _reservation_event_id(operation_id, current.state_ref) - event = ParticipantResourceBudgetEventModel( - event_id=event_id, - operation_id=operation_id, - budget_state_ref=current.state_ref, - budget_id=demand.budget_id, - policy_address=policy.address, - owner_ref=current.owner_ref, - pool_ref=current.pool_ref, - execution_generation=execution_generation, - transition="reserve", - disposition="reserved", - requested=amount, - resource_kind=current.resource_kind, - unit=current.unit, - meter_profile_ref=current.meter_profile_ref, - predecessor_event_ref=current.last_event_ref, - ) - states[current.state_ref] = _payload( - current.model_copy( - update={ - "reserved": current.reserved + amount, - "last_event_ref": event_id, - } - ) - ) - pool_states[physical_pool.pool_state_ref] = _payload( - reserve_pool_allocation(physical_pool, current.state_ref, amount) - ) - events[event_id] = _payload(event) - return ApplyResult( - success=True, - snapshot=snapshot.with_entries( - dict(snapshot.entries), - participant_resource_budget_states=states, - participant_resource_pool_states=pool_states, - participant_resource_budget_events=events, - ), + from .participant_resource_reservation import reserve_participant_resources as reserve + + return reserve( + snapshot, + policy, + operation_id=operation_id, + execution_generation=execution_generation, + requested_quantities=requested_quantities, ) diff --git a/implementations/python/packages/raes_runtime/participant_resource_reservation.py b/implementations/python/packages/raes_runtime/participant_resource_reservation.py new file mode 100644 index 000000000..363df19a0 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_resource_reservation.py @@ -0,0 +1,301 @@ +"""Atomic reservation against participant resource-budget and pool state.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass + +from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceBudgetEventModel, + ParticipantResourceBudgetStateModel, + ParticipantResourcePoolStateModel, + participant_resource_budget_state_ref, + participant_resource_pool_state_ref, +) +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot + +from .participant_resource_budgets import ( + ResourceDemand, + ResourcePolicy, + _diagnostic, + _payload, + _pool_state, + _state, +) +from .participant_resource_pool_ledger import ( + can_reserve as pool_can_reserve, +) +from .participant_resource_pool_ledger import ( + reserve as reserve_pool_allocation, +) + + +@dataclass +class _ReservationMutation: + snapshot: RuntimeSnapshot + policy: ResourcePolicy + operation_id: str + execution_generation: int + states: dict[str, dict[str, object]] + pool_states: dict[str, dict[str, object]] + events: dict[str, dict[str, object]] + + +@dataclass(frozen=True) +class _ReservationCheck: + demand: ResourceDemand + current: ParticipantResourceBudgetStateModel | None = None + physical_pool: ParticipantResourcePoolStateModel | None = None + amount: int = 0 + failure: ApplyResult | None = None + + +def _event_id(operation_id: str, state_ref: str) -> str: + return f"{operation_id}:{state_ref}:reserve" + + +def _used_capacity(state: ParticipantResourceBudgetStateModel) -> int: + if state.accounting_mode in {"reservable_gauge", "lease"}: + return state.current_use + state.reserved + return state.cumulative_use + state.reserved + + +def _pool_ref_for_state(state: ParticipantResourceBudgetStateModel) -> str: + return participant_resource_pool_state_ref( + pool_ref=state.pool_ref, + owner_kind=state.owner_kind, + owner_ref=state.owner_ref, + resource_kind=state.resource_kind, + unit=state.unit, + accounting_mode=state.accounting_mode, + meter_profile_ref=state.meter_profile_ref, + ) + + +def _failure( + mutation: _ReservationMutation, + code: str, + message: str, +) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=mutation.snapshot, + diagnostics=[_diagnostic(code, mutation.policy.address, message)], + ) + + +def _throttled_result( + mutation: _ReservationMutation, + demand: ResourceDemand, + current: ParticipantResourceBudgetStateModel, + amount: int, + budget_available: bool, +) -> ApplyResult: + event_id = f"{mutation.operation_id}:{current.state_ref}:throttle" + mutation.states[current.state_ref] = _payload( + current.model_copy( + update={ + "throttled": current.throttled + 1, + "last_event_ref": event_id, + } + ) + ) + mutation.events[event_id] = _payload( + ParticipantResourceBudgetEventModel( + event_id=event_id, + operation_id=mutation.operation_id, + budget_state_ref=current.state_ref, + budget_id=demand.budget_id, + policy_address=mutation.policy.address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=mutation.execution_generation, + transition="throttle", + disposition="throttled", + requested=amount, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=current.last_event_ref, + ) + ) + capacity_kind = "logical budget" if not budget_available else "shared pool" + return ApplyResult( + success=False, + snapshot=mutation.snapshot.with_entries( + dict(mutation.snapshot.entries), + participant_resource_budget_states=mutation.states, + participant_resource_pool_states=mutation.pool_states, + participant_resource_budget_events=mutation.events, + ), + diagnostics=[ + _diagnostic( + "runtime.participant-resource-throttled", + mutation.policy.address, + f"resource budget {demand.budget_id} has insufficient {capacity_kind} capacity", + ) + ], + ) + + +def _requested_amount( + demand: ResourceDemand, + requested_quantities: Mapping[str, int] | None, +) -> int: + amount = ( + requested_quantities.get(demand.budget_id, demand.reservation) + if requested_quantities is not None + else demand.reservation + ) + if amount < 0: + raise ValueError("requested participant resource quantities must be non-negative") + return amount + + +def _check_reservation( + mutation: _ReservationMutation, + demand: ResourceDemand, + state_ref: str, + requested_quantities: Mapping[str, int] | None, +) -> _ReservationCheck: + current: ParticipantResourceBudgetStateModel | None = None + physical_pool: ParticipantResourcePoolStateModel | None = None + amount = 0 + failure = None + raw = mutation.states.get(state_ref) + if raw is None: + failure = _failure( + mutation, + "runtime.participant-resource-state-missing", + f"resource budget {demand.budget_id} was not initialized", + ) + else: + current = _state(raw) + if current is not None and current.generation != mutation.execution_generation: + failure = _failure( + mutation, + "runtime.participant-resource-stale-generation", + ( + f"resource budget {demand.budget_id} is generation {current.generation}; " + f"request is generation {mutation.execution_generation}" + ), + ) + if current is not None and failure is None: + amount = _requested_amount(demand, requested_quantities) + raw_pool = mutation.pool_states.get(_pool_ref_for_state(current)) + if raw_pool is None: + failure = _failure( + mutation, + "runtime.participant-resource-pool-state-missing", + f"physical pool for resource budget {demand.budget_id} was not initialized", + ) + else: + physical_pool = _pool_state(raw_pool) + if current is not None and physical_pool is not None and failure is None: + budget_available = _used_capacity(current) + amount <= min(current.limit, current.configured_capacity) + if not budget_available or not pool_can_reserve(physical_pool, state_ref, amount): + failure = _throttled_result(mutation, demand, current, amount, budget_available) + return _ReservationCheck( + demand=demand, + current=current, + physical_pool=physical_pool, + amount=amount, + failure=failure, + ) + + +def _apply_reservation( + mutation: _ReservationMutation, + checked: _ReservationCheck, +) -> None: + if checked.current is None or checked.physical_pool is None: + raise AssertionError("validated reservation must include budget and pool state") + current = checked.current + event_id = _event_id(mutation.operation_id, current.state_ref) + event = ParticipantResourceBudgetEventModel( + event_id=event_id, + operation_id=mutation.operation_id, + budget_state_ref=current.state_ref, + budget_id=checked.demand.budget_id, + policy_address=mutation.policy.address, + owner_ref=current.owner_ref, + pool_ref=current.pool_ref, + execution_generation=mutation.execution_generation, + transition="reserve", + disposition="reserved", + requested=checked.amount, + resource_kind=current.resource_kind, + unit=current.unit, + meter_profile_ref=current.meter_profile_ref, + predecessor_event_ref=current.last_event_ref, + ) + mutation.states[current.state_ref] = _payload( + current.model_copy( + update={ + "reserved": current.reserved + checked.amount, + "last_event_ref": event_id, + } + ) + ) + mutation.pool_states[checked.physical_pool.pool_state_ref] = _payload( + reserve_pool_allocation(checked.physical_pool, current.state_ref, checked.amount) + ) + mutation.events[event_id] = _payload(event) + + +def _success_result(mutation: _ReservationMutation) -> ApplyResult: + return ApplyResult( + success=True, + snapshot=mutation.snapshot.with_entries( + dict(mutation.snapshot.entries), + participant_resource_budget_states=mutation.states, + participant_resource_pool_states=mutation.pool_states, + participant_resource_budget_events=mutation.events, + ), + ) + + +def reserve_participant_resources( + snapshot: RuntimeSnapshot, + policy: ResourcePolicy, + *, + operation_id: str, + execution_generation: int, + requested_quantities: Mapping[str, int] | None = None, +) -> ApplyResult: + """Reserve a policy's complete resource vector or reserve none of it.""" + + events = dict(snapshot.participant_resource_budget_events) + state_refs = [ + participant_resource_budget_state_ref(policy.address, demand.budget_id) for demand in policy.resource_demands + ] + event_ids = [_event_id(operation_id, state_ref) for state_ref in state_refs] + if event_ids and all(event_id in events for event_id in event_ids): + return ApplyResult(success=True, snapshot=snapshot) + mutation = _ReservationMutation( + snapshot=snapshot, + policy=policy, + operation_id=operation_id, + execution_generation=execution_generation, + states=dict(snapshot.participant_resource_budget_states), + pool_states=dict(snapshot.participant_resource_pool_states), + events=events, + ) + checked: list[_ReservationCheck] = [] + failure = None + for demand, state_ref in zip(policy.resource_demands, state_refs, strict=True): + check = _check_reservation(mutation, demand, state_ref, requested_quantities) + if check.failure is not None: + failure = check.failure + break + checked.append(check) + if failure is not None: + result = failure + else: + for check in checked: + _apply_reservation(mutation, check) + result = _success_result(mutation) + return result + + +__all__ = ("reserve_participant_resources",) diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index ba490d11e..096c8cd62 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -23,23 +23,21 @@ ) from .participant_execution_scheduler_state import ( execution_service_state, - reset_execution_service, set_execution_clock_lifecycle, ) -from .participant_resource_accounting import ( - reconcile_participant_resource_budgets, -) from .participant_resource_budgets import initialize_participant_resource_budgets +from .participant_scheduler_lifecycle import reset_policy_at_clock from .participant_scheduler_operations import ( SchedulerRunState, run_participant_due, run_policy_due_concurrently, ) from .participant_scheduler_policy import _policy_digest -from .participant_scheduler_reset import clock_reset_context, reset_scheduler_participant from .participant_scheduler_time import cadence as _cadence from .participant_scheduler_time import clock_coordinate +_RESOURCE_GOVERNED_PROFILE = "participant-autonomous-execution/v3" + def _state_key(policy_address: str, participant_address: str) -> str: return f"{policy_address}.state.{participant_address}" @@ -99,7 +97,7 @@ def _initialize_participant( policy.profile in { "participant-autonomous-execution/v2", - "participant-autonomous-execution/v3", + _RESOURCE_GOVERNED_PROFILE, } and activity_control is None ): @@ -258,6 +256,109 @@ def _run_due_policy( _run_serial_due(policy, time_model, participant_runtime, current_tick, cadence_ticks, activity_controls, run) +def _initialize_governed_resources( + policies: tuple[ParticipantAutonomousExecutionRuntime, ...], + snapshot: RuntimeSnapshot, + resource_capabilities: object | None, +) -> ApplyResult: + governed = tuple(policy for policy in policies if policy.profile == _RESOURCE_GOVERNED_PROFILE) + if not governed: + return ApplyResult(success=True, snapshot=snapshot) + if resource_capabilities is None: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code="runtime.participant-resource-capabilities-missing", + domain="participant", + address=governed[0].address, + message="Participant execution v3 requires admitted resource-budget capabilities.", + ) + ], + ) + return initialize_participant_resource_budgets( + snapshot, + governed, + resource_capabilities, + execution_generation=0, + ) + + +def _initialize_policy( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + snapshot: RuntimeSnapshot, + activity_controls: dict[str, ParticipantActivityRandomControl], +) -> ApplyResult: + working = snapshot + changed: list[str] = [] + failure = None + for participant_address in policy.participant_addresses: + participant_result = _initialize_participant( + policy, + time_model, + participant_runtime, + working, + participant_address, + activity_controls, + ) + if not participant_result.success: + failure = participant_result + break + working = participant_result.snapshot + changed.extend(participant_result.changed_addresses) + if failure is None: + services = dict(working.participant_execution_services) + expected_service = execution_service_state( + policy, + time_model, + policy_digest=_policy_digest(policy, time_model), + ) + existing_payload = services.get(policy.address) + if existing_payload is not None: + existing = ParticipantExecutionServiceStateModel.model_validate(existing_payload) + identity = ( + existing.policy_digest, + existing.binding_digest, + existing.time_declaration_digest, + ) + expected_identity = ( + expected_service.policy_digest, + expected_service.binding_digest, + expected_service.time_declaration_digest, + ) + if identity != expected_identity: + failure = ApplyResult( + success=False, + snapshot=working, + diagnostics=[ + Diagnostic( + code="runtime.participant-execution-state-conflict", + domain="participant", + address=policy.address, + message=( + "Existing participant execution service state does not match " + "the admitted policy, bindings, or shared-time declaration." + ), + ) + ], + ) + else: + services[policy.address] = expected_service.model_dump(mode="json") + working = working.with_entries( + dict(working.entries), + participant_execution_services=services, + ) + changed.append(policy.address) + return failure or ApplyResult( + success=True, + snapshot=working, + changed_addresses=list(dict.fromkeys(changed)), + ) + + class ParticipantScheduler: """Deterministically selects due actions and delegates native execution.""" @@ -274,83 +375,22 @@ def initialize( resolved_activity_controls = activity_controls or {} changed: list[str] = [] normalized_policies = tuple(policies) - governed_policies = tuple( - policy for policy in normalized_policies if policy.profile == "participant-autonomous-execution/v3" - ) - if governed_policies: - if resource_capabilities is None: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - Diagnostic( - code="runtime.participant-resource-capabilities-missing", - domain="participant", - address=governed_policies[0].address, - message="Participant execution v3 requires admitted resource-budget capabilities.", - ) - ], - ) - initialized = initialize_participant_resource_budgets( - working, - governed_policies, - resource_capabilities, - execution_generation=0, - ) - if not initialized.success: - return initialized - working = initialized.snapshot + initialized = _initialize_governed_resources(normalized_policies, working, resource_capabilities) + if not initialized.success: + return initialized + working = initialized.snapshot for policy in normalized_policies: - for participant_address in policy.participant_addresses: - result = _initialize_participant( - policy, - time_model, - participant_runtime, - working, - participant_address, - resolved_activity_controls, - ) - if not result.success: - return result - working = result.snapshot - changed.extend(result.changed_addresses) - services = dict(working.participant_execution_services) - expected_service = execution_service_state( + policy_result = _initialize_policy( policy, time_model, - policy_digest=_policy_digest(policy, time_model), + participant_runtime, + working, + resolved_activity_controls, ) - existing_service = services.get(policy.address) - if existing_service is not None: - existing = ParticipantExecutionServiceStateModel.model_validate(existing_service) - if ( - existing.policy_digest != expected_service.policy_digest - or existing.binding_digest != expected_service.binding_digest - or existing.time_declaration_digest != expected_service.time_declaration_digest - ): - return ApplyResult( - success=False, - snapshot=working, - diagnostics=[ - Diagnostic( - code="runtime.participant-execution-state-conflict", - domain="participant", - address=policy.address, - message=( - "Existing participant execution service state " - "does not match the admitted policy, bindings, " - "or shared-time declaration." - ), - ) - ], - ) - else: - services[policy.address] = expected_service.model_dump(mode="json") - working = working.with_entries( - dict(working.entries), - participant_execution_services=services, - ) - changed.append(policy.address) + if not policy_result.success: + return policy_result + working = policy_result.snapshot + changed.extend(policy_result.changed_addresses) return ApplyResult( success=True, snapshot=working, @@ -393,49 +433,19 @@ def reset_clock( for policy in policies: if policy.clock_address != clock_address: continue - current_tick = _clock_tick(snapshot, clock_address) - activity_control = activity_control_for(policy, resolved_activity_controls) - context = clock_reset_context( + policy_result = reset_policy_at_clock( policy, time_model, participant_runtime, + working, segment, - current_tick, reset_participants, - activity_control, - ) - for participant_address in policy.participant_addresses: - result = reset_scheduler_participant(context, working, participant_address) - if not result.success: - return result - working = result.snapshot - changed.extend(result.changed_addresses) - if policy.profile == "participant-autonomous-execution/v3": - service_payload = working.participant_execution_services.get(policy.address) - if service_payload is None: - return _missing_execution_service_result( - policy, - SchedulerRunState(working=working, diagnostics=[], changed=changed), - ) - service = ParticipantExecutionServiceStateModel.model_validate(service_payload) - generation = service.generation + 1 - budget_reset = reconcile_participant_resource_budgets( - working, - policy_address=policy.address, - current_generation=service.generation, - next_generation=generation, - boundary="time_segment", - evidence_refs=(f"evidence:{policy.address}:shared-time-reset:generation-{generation}",), - ) - if not budget_reset.success: - return budget_reset - working = budget_reset.snapshot - working, service_changed = reset_execution_service( - working, - policy.address, + resolved_activity_controls, ) - if service_changed: - changed.append(policy.address) + if not policy_result.success: + return policy_result + working = policy_result.snapshot + changed.extend(policy_result.changed_addresses) return ApplyResult( success=True, snapshot=working, diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py b/implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py new file mode 100644 index 000000000..12637663c --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py @@ -0,0 +1,105 @@ +"""Lifecycle transitions for autonomous participant scheduler policies.""" + +from __future__ import annotations + +from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + +from .participant_activity import ParticipantActivityRandomControl, activity_control_for +from .participant_execution_scheduler_state import reset_execution_service +from .participant_resource_accounting import reconcile_participant_resource_budgets +from .participant_scheduler_reset import clock_reset_context, reset_scheduler_participant + +_RESOURCE_GOVERNED_PROFILE = "participant-autonomous-execution/v3" + + +def _clock_tick(snapshot: RuntimeSnapshot, clock_address: str) -> int: + if snapshot.time_model_state is None: + raise ValueError("autonomous participant execution requires typed shared-time state") + clock = snapshot.time_model_state.clocks.get(clock_address) + if clock is None: + raise ValueError(f"autonomous participant clock {clock_address!r} has no runtime state") + return clock.coordinate.tick + + +def _missing_execution_service_result( + policy: ParticipantAutonomousExecutionRuntime, + snapshot: RuntimeSnapshot, +) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code="runtime.participant-execution-state-missing", + domain="participant", + address=policy.address, + message="Autonomous participant execution requires typed execution-service state.", + ) + ], + ) + + +def reset_policy_at_clock( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + snapshot: RuntimeSnapshot, + segment: str, + reset_participants: bool, + activity_controls: dict[str, ParticipantActivityRandomControl], +) -> ApplyResult: + """Reset one policy's participants, resource generation, and service state.""" + + working = snapshot + changed: list[str] = [] + failure = None + context = clock_reset_context( + policy, + time_model, + participant_runtime, + segment, + _clock_tick(snapshot, policy.clock_address), + reset_participants, + activity_control_for(policy, activity_controls), + ) + for participant_address in policy.participant_addresses: + participant_result = reset_scheduler_participant(context, working, participant_address) + if not participant_result.success: + failure = participant_result + break + working = participant_result.snapshot + changed.extend(participant_result.changed_addresses) + if failure is None and policy.profile == _RESOURCE_GOVERNED_PROFILE: + service_payload = working.participant_execution_services.get(policy.address) + if service_payload is None: + failure = _missing_execution_service_result(policy, working) + else: + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + generation = service.generation + 1 + budget_reset = reconcile_participant_resource_budgets( + working, + policy_address=policy.address, + current_generation=service.generation, + next_generation=generation, + boundary="time_segment", + evidence_refs=(f"evidence:{policy.address}:shared-time-reset:generation-{generation}",), + ) + if budget_reset.success: + working = budget_reset.snapshot + else: + failure = budget_reset + if failure is None: + working, service_changed = reset_execution_service(working, policy.address) + if service_changed: + changed.append(policy.address) + return failure or ApplyResult( + success=True, + snapshot=working, + changed_addresses=list(dict.fromkeys(changed)), + ) + + +__all__ = ("reset_policy_at_clock",) diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_resources.py b/implementations/python/packages/raes_runtime/participant_scheduler_resources.py index 2626af4f0..6b0a5f051 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_resources.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_resources.py @@ -3,6 +3,7 @@ from typing import Protocol from raes_contracts.contracts.participant_resource_budgets import ( + ParticipantResourceMeasurementModel, ParticipantResourceMeasurementRequirementModel, participant_resource_budget_state_ref, ) @@ -19,6 +20,8 @@ ) from .participant_scheduler_types import SchedulerRunState, _DueActionContext +_RESOURCE_GOVERNED_PROFILE = "participant-autonomous-execution/v3" + class _MeasurementDemand(Protocol): budget_id: str @@ -37,7 +40,7 @@ class _MeasurementPolicy(Protocol): def measurement_requirements( policy: _MeasurementPolicy, ) -> tuple[ParticipantResourceMeasurementRequirementModel, ...]: - if policy.profile != "participant-autonomous-execution/v3": + if policy.profile != _RESOURCE_GOVERNED_PROFILE: return () return tuple( ParticipantResourceMeasurementRequirementModel( @@ -67,7 +70,7 @@ def reserve_activity_resources( ) -> bool: """Reserve the complete v3 resource vector before native execution.""" - if context.policy.profile != "participant-autonomous-execution/v3": + if context.policy.profile != _RESOURCE_GOVERNED_PROFILE: return True reservation = reserve_participant_resources( run.working, @@ -82,18 +85,11 @@ def reserve_activity_resources( return reservation.success -def commit_activity_resources( - context: _DueActionContext, +def _trusted_measurements( request: ParticipantActionAdmissionRequest, result: ParticipantActionApplyResult, - *, protocol_failure: bool, - run: SchedulerRunState, -) -> bool: - """Commit only a complete, trusted native measurement vector.""" - - if context.policy.profile != "participant-autonomous-execution/v3": - return True +) -> dict[str, ParticipantResourceMeasurementModel] | None: action_result = result.action_result requirements = {item.budget_state_ref: item for item in request.resource_measurement_requirements} measurements = { @@ -112,26 +108,41 @@ def commit_activity_resources( for state_ref, measurement in measurements.items() ) ) - if not trusted: - released = release_participant_resource_reservation( - run.working, - operation_id=request.action_instance_id, - execution_generation=request.execution_generation or 0, - evidence_refs=(f"evidence:{request.action_instance_id}:resource-release",), - ) - run.working = released.snapshot - run.diagnostics.extend(released.diagnostics) - if not protocol_failure: - run.diagnostics.append( - Diagnostic( - code="runtime.participant-resource-measurement-untrusted", - domain="participant-runtime", - address=context.policy.address, - message="native action did not return the exact trusted resource measurement vector", - ) + return measurements if trusted else None + + +def _release_untrusted_measurements( + context: _DueActionContext, + request: ParticipantActionAdmissionRequest, + protocol_failure: bool, + run: SchedulerRunState, +) -> bool: + released = release_participant_resource_reservation( + run.working, + operation_id=request.action_instance_id, + execution_generation=request.execution_generation or 0, + evidence_refs=(f"evidence:{request.action_instance_id}:resource-release",), + ) + run.working = released.snapshot + run.diagnostics.extend(released.diagnostics) + if not protocol_failure: + run.diagnostics.append( + Diagnostic( + code="runtime.participant-resource-measurement-untrusted", + domain="participant-runtime", + address=context.policy.address, + message="native action did not return the exact trusted resource measurement vector", ) - _record_resource_failure(run) - return False + ) + _record_resource_failure(run) + return False + + +def _commit_trusted_measurements( + request: ParticipantActionAdmissionRequest, + measurements: dict[str, ParticipantResourceMeasurementModel], + run: SchedulerRunState, +) -> bool: evidence_refs = tuple( dict.fromkeys( evidence_ref for measurement in measurements.values() for evidence_ref in measurement.evidence_refs @@ -151,6 +162,24 @@ def commit_activity_resources( return committed.success +def commit_activity_resources( + context: _DueActionContext, + request: ParticipantActionAdmissionRequest, + result: ParticipantActionApplyResult, + *, + protocol_failure: bool, + run: SchedulerRunState, +) -> bool: + """Commit only a complete, trusted native measurement vector.""" + + if context.policy.profile != _RESOURCE_GOVERNED_PROFILE: + return True + measurements = _trusted_measurements(request, result, protocol_failure) + if measurements is None: + return _release_untrusted_measurements(context, request, protocol_failure, run) + return _commit_trusted_measurements(request, measurements, run) + + __all__ = [ "commit_activity_resources", "measurement_requirements", diff --git a/implementations/python/tests/test_issue_899_participant_resource_budgets.py b/implementations/python/tests/test_issue_899_participant_resource_budgets.py index 17a02332d..9d604877e 100644 --- a/implementations/python/tests/test_issue_899_participant_resource_budgets.py +++ b/implementations/python/tests/test_issue_899_participant_resource_budgets.py @@ -421,12 +421,13 @@ def test_v3_resource_budget_compiles_complete_typed_vector() -> None: def test_resource_owners_must_bind_to_authorized_execution_topology() -> None: payload = yaml.safe_load(_budget_policy_yaml()) del payload["relationships"]["range-a-inference-service"] + serialized = yaml.safe_dump(payload, sort_keys=False) with pytest.raises( SDLValidationError, match="lacks an authorized tenant uses_shared_service edge", ): - parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + parse_sdl(serialized) def test_composition_rewrites_kind_specific_resource_owner_refs() -> None: @@ -871,6 +872,7 @@ def test_cross_range_shared_pool_requires_partitioned_isolation() -> None: replace(pool, tenant_isolation="none") if pool.pool_ref == "inference-pool" else pool for pool in capabilities.configured_pools ) + cross_range_pool_refs = frozenset({"inference-pool"}) with pytest.raises(ValueError, match="cross-range.*tenant_partitioned"): ParticipantResourceBudgetCapabilities( @@ -883,7 +885,7 @@ def test_cross_range_shared_pool_requires_partitioned_isolation() -> None: supported_isolation_strengths=capabilities.supported_isolation_strengths, configured_pools=shared, realization_contract_ids=capabilities.realization_contract_ids, - cross_range_pool_refs=frozenset({"inference-pool"}), + cross_range_pool_refs=cross_range_pool_refs, ) From e3aa2005934f8a1e41d8b0cb84b9b6493f5069c6 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 06:16:55 +0200 Subject: [PATCH 45/55] refactor: simplify artifact satisfaction validation --- .../python/packages/raes/_source.py | 50 +- .../raes_contracts/artifact_requirements.py | 60 ++- .../compiler/realization_requirements.py | 79 +++- .../semantics/artifact_realization.py | 436 ++++++++++++------ .../packages/raes_runtime/backend_calls.py | 23 +- .../python/packages/raes_runtime/manager.py | 12 +- .../test_artifact_requirement_satisfaction.py | 17 +- 7 files changed, 448 insertions(+), 229 deletions(-) diff --git a/implementations/python/packages/raes/_source.py b/implementations/python/packages/raes/_source.py index 1b7ce091d..04cbda67d 100644 --- a/implementations/python/packages/raes/_source.py +++ b/implementations/python/packages/raes/_source.py @@ -218,31 +218,39 @@ class ArtifactRequirement(SDLModel): def validate_authority(self) -> "ArtifactRequirement": self._validate_unique_identities() if self.explicitness is ExplicitnessClass.EXACT: - if self.exact_artifact is None: - raise ValueError("exact artifact requirements require one immutable exact_artifact identity") - if self.constraints or self.candidates or self.locked_inputs or self.materialization_specifications: - raise ValueError( - "exact artifact requirements must not declare alternative constraints, " - "candidates, locked inputs, or materialization specifications" - ) - if any(route.mechanism.mechanism != "exact-artifact" for route in self.permitted_routes): - raise ValueError("exact artifact requirements permit only the exact-artifact mechanism") + self._validate_exact_authority() elif self.explicitness is ExplicitnessClass.CONSTRAINED: - if self.exact_artifact is not None: - raise ValueError("constrained artifact requirements must not declare exact_artifact") - if not (self.constraints or self.candidates or self.locked_inputs or self.materialization_specifications): - raise ValueError("constrained artifact requirements require a non-empty constraint domain") + self._validate_constrained_authority() else: - if self.exact_artifact is not None: - raise ValueError("open artifact requirements must not declare exact_artifact") - if self.constraints or self.candidates or self.materialization_specifications: - raise ValueError( - "open artifact requirements must not declare constraints, " - "candidates, or materialization specifications" - ) + self._validate_open_authority() self._validate_materialization_input_joins() return self + def _validate_exact_authority(self) -> None: + if self.exact_artifact is None: + raise ValueError("exact artifact requirements require one immutable exact_artifact identity") + if self.constraints or self.candidates or self.locked_inputs or self.materialization_specifications: + raise ValueError( + "exact artifact requirements must not declare alternative constraints, " + "candidates, locked inputs, or materialization specifications" + ) + if any(route.mechanism.mechanism != "exact-artifact" for route in self.permitted_routes): + raise ValueError("exact artifact requirements permit only the exact-artifact mechanism") + + def _validate_constrained_authority(self) -> None: + if self.exact_artifact is not None: + raise ValueError("constrained artifact requirements must not declare exact_artifact") + if not (self.constraints or self.candidates or self.locked_inputs or self.materialization_specifications): + raise ValueError("constrained artifact requirements require a non-empty constraint domain") + + def _validate_open_authority(self) -> None: + if self.exact_artifact is not None: + raise ValueError("open artifact requirements must not declare exact_artifact") + if self.constraints or self.candidates or self.materialization_specifications: + raise ValueError( + "open artifact requirements must not declare constraints, candidates, or materialization specifications" + ) + @classmethod def __get_pydantic_json_schema__( cls, @@ -367,7 +375,7 @@ class Source(SDLModel): artifact_requirement: ArtifactRequirement | None = None @model_validator(mode="after") - def validate_exact_requirement_selector(self) -> "Source": + def validate_exact_requirement_selector(self) -> "Source": # NOSONAR - Pydantic requires returning self. requirement = self.artifact_requirement if requirement is None or requirement.explicitness is not ExplicitnessClass.EXACT: return self diff --git a/implementations/python/packages/raes_contracts/artifact_requirements.py b/implementations/python/packages/raes_contracts/artifact_requirements.py index 5edef14dd..7aab32b5b 100644 --- a/implementations/python/packages/raes_contracts/artifact_requirements.py +++ b/implementations/python/packages/raes_contracts/artifact_requirements.py @@ -20,6 +20,7 @@ from .versions import ARTIFACT_REQUIREMENT_SCHEMA_VERSION Sha256DigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] +_INVARIANT_VALIDATOR = "raes_contracts.artifact_requirements.validate_artifact_requirement_invariants" def _require_unique(values: list[object], *, field_name: str) -> None: @@ -31,26 +32,51 @@ def _require_unique(values: list[object], *, field_name: str) -> None: def artifact_requirement_invariant_violations(payload: object) -> tuple[str, ...]: """Evaluate the cross-object invariants published in ``x-raes-invariants``.""" + source, requirement, structural_violation = _artifact_requirement_mappings(payload) + if structural_violation is not None: + return (structural_violation,) + assert source is not None + assert requirement is not None + violations = [ + *_exact_identity_violations(source, requirement), + *_materialization_violations(requirement), + ] + return tuple(dict.fromkeys(violations)) + + +def _artifact_requirement_mappings( + payload: object, +) -> tuple[Mapping[object, object] | None, Mapping[object, object] | None, str | None]: if not isinstance(payload, Mapping): - return ("artifact-requirement-document-object",) + return None, None, "artifact-requirement-document-object" source = payload.get("source") if not isinstance(source, Mapping): - return ("artifact-requirement-source-object",) + return None, None, "artifact-requirement-source-object" requirement = source.get("artifact_requirement") if not isinstance(requirement, Mapping): - return ("artifact-requirement-present",) - + return None, None, "artifact-requirement-present" + return source, requirement, None + + +def _exact_identity_violations( + source: Mapping[object, object], + requirement: Mapping[object, object], +) -> list[str]: + if requirement.get("explicitness") != "exact": + return [] + identity = requirement.get("exact_artifact") + if not isinstance(identity, Mapping): + return ["exact-artifact-present"] violations: list[str] = [] - if requirement.get("explicitness") == "exact": - identity = requirement.get("exact_artifact") - if not isinstance(identity, Mapping): - violations.append("exact-artifact-present") - else: - if source.get("name") != identity.get("artifact_id"): - violations.append("exact-source-artifact-id-match") - if source.get("version", "*") != identity.get("version"): - violations.append("exact-source-version-match") + if source.get("name") != identity.get("artifact_id"): + violations.append("exact-source-artifact-id-match") + if source.get("version", "*") != identity.get("version"): + violations.append("exact-source-version-match") + return violations + +def _materialization_violations(requirement: Mapping[object, object]) -> list[str]: + violations: list[str] = [] locked_inputs = requirement.get("locked_inputs", []) declared_input_ids = { item.get("input_id") @@ -70,7 +96,7 @@ def artifact_requirement_invariant_violations(payload: object) -> tuple[str, ... referenced = specification.get("locked_input_ids", []) if isinstance(referenced, list) and not set(referenced).issubset(declared_input_ids): violations.append("materialization-locked-input-join") - return tuple(dict.fromkeys(violations)) + return violations def validate_artifact_requirement_invariants( @@ -110,7 +136,7 @@ def __get_pydantic_json_schema__( "An exact requirement's immutable artifact id must equal the enclosing Source selector name." ), "level": "error", - "validator": ("raes_contracts.artifact_requirements.validate_artifact_requirement_invariants"), + "validator": _INVARIANT_VALIDATOR, "inputs": [ { "contract_id": "artifact-requirement-v1", @@ -125,7 +151,7 @@ def __get_pydantic_json_schema__( "equal the enclosing Source selector version." ), "level": "error", - "validator": ("raes_contracts.artifact_requirements.validate_artifact_requirement_invariants"), + "validator": _INVARIANT_VALIDATOR, "inputs": [ { "contract_id": "artifact-requirement-v1", @@ -140,7 +166,7 @@ def __get_pydantic_json_schema__( "locked input declared by the same artifact requirement." ), "level": "error", - "validator": ("raes_contracts.artifact_requirements.validate_artifact_requirement_invariants"), + "validator": _INVARIANT_VALIDATOR, "inputs": [ { "contract_id": "artifact-requirement-v1", diff --git a/implementations/python/packages/raes_processor/compiler/realization_requirements.py b/implementations/python/packages/raes_processor/compiler/realization_requirements.py index 0155db67d..b8dcfb82c 100644 --- a/implementations/python/packages/raes_processor/compiler/realization_requirements.py +++ b/implementations/python/packages/raes_processor/compiler/realization_requirements.py @@ -60,6 +60,15 @@ def _append_source_artifact_requirements( ) -> None: """Lower every realized ``Source`` carrier into the existing demand graph.""" + _append_resource_source_artifact_requirements(requirements, scenario) + _append_bound_source_artifact_requirements(requirements, scenario) + _append_action_source_artifact_requirements(requirements, scenario) + + +def _append_resource_source_artifact_requirements( + requirements: list[CompiledRealizationRequirement], + scenario: InstantiatedScenario, +) -> None: for name, node in scenario.nodes.items(): if node.source is not None: _append_source_artifact_requirement( @@ -78,27 +87,57 @@ def _append_source_artifact_requirements( address=_content_address(name), governing_scope=f"#/content/{name}/source/artifact_requirement", ) + + +def _append_bound_source_artifact_requirements( + requirements: list[CompiledRealizationRequirement], + scenario: InstantiatedScenario, +) -> None: for node_name, node in scenario.nodes.items(): - for feature_name in node.features: - feature = scenario.features.get(feature_name) - if feature is not None and feature.source is not None: - _append_source_artifact_requirement( - requirements, - source=feature.source, - field_path=f"features.{feature_name}.source.artifact_requirement", - address=_feature_binding_address(node_name, feature_name), - governing_scope=f"#/features/{feature_name}/source/artifact_requirement", - ) - for condition_name in node.conditions: - condition = scenario.conditions.get(condition_name) - if condition is not None and condition.source is not None: - _append_source_artifact_requirement( - requirements, - source=condition.source, - field_path=f"conditions.{condition_name}.source.artifact_requirement", - address=_condition_binding_address(node_name, condition_name), - governing_scope=f"#/conditions/{condition_name}/source/artifact_requirement", - ) + _append_feature_source_artifact_requirements(requirements, scenario, node_name, node.features) + _append_condition_source_artifact_requirements(requirements, scenario, node_name, node.conditions) + + +def _append_feature_source_artifact_requirements( + requirements: list[CompiledRealizationRequirement], + scenario: InstantiatedScenario, + node_name: str, + feature_names: list[str], +) -> None: + for feature_name in feature_names: + feature = scenario.features.get(feature_name) + if feature is not None and feature.source is not None: + _append_source_artifact_requirement( + requirements, + source=feature.source, + field_path=f"features.{feature_name}.source.artifact_requirement", + address=_feature_binding_address(node_name, feature_name), + governing_scope=f"#/features/{feature_name}/source/artifact_requirement", + ) + + +def _append_condition_source_artifact_requirements( + requirements: list[CompiledRealizationRequirement], + scenario: InstantiatedScenario, + node_name: str, + condition_names: list[str], +) -> None: + for condition_name in condition_names: + condition = scenario.conditions.get(condition_name) + if condition is not None and condition.source is not None: + _append_source_artifact_requirement( + requirements, + source=condition.source, + field_path=f"conditions.{condition_name}.source.artifact_requirement", + address=_condition_binding_address(node_name, condition_name), + governing_scope=f"#/conditions/{condition_name}/source/artifact_requirement", + ) + + +def _append_action_source_artifact_requirements( + requirements: list[CompiledRealizationRequirement], + scenario: InstantiatedScenario, +) -> None: for section, declarations, address_factory in ( ("injects", scenario.injects, _inject_address), ("events", scenario.events, _event_address), diff --git a/implementations/python/packages/raes_processor/semantics/artifact_realization.py b/implementations/python/packages/raes_processor/semantics/artifact_realization.py index e5da3d447..07817a341 100644 --- a/implementations/python/packages/raes_processor/semantics/artifact_realization.py +++ b/implementations/python/packages/raes_processor/semantics/artifact_realization.py @@ -7,6 +7,7 @@ from raes.artifact_requirements import ArtifactMechanismProfile, ArtifactRequirement from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance from raes_backend_protocols.capabilities import BackendManifest +from raes_contracts.apparatus import RealizationSupportDeclaration from raes_contracts.artifact_requirements import ( ArtifactAvailabilityContext, ArtifactRequirementAvailability, @@ -36,112 +37,186 @@ def artifact_requirement_diagnostics( for compiled in requirements: requirement = compiled.artifact_requirement - if requirement is None: - continue - scoped_facts = facts.for_address(compiled.address) - available_digests = set(scoped_facts.available_artifact_digests) - available_candidates = set(scoped_facts.available_candidate_ids) - verified_locked_inputs = set(scoped_facts.verified_locked_input_ids) - satisfied_constraints = set(scoped_facts.satisfied_constraint_ids) - declarations = [ - declaration for declaration in manifest.realization_support if declaration.domain == compiled.domain - ] - - if requirement.explicitness is ExplicitnessClass.OPEN and not any( - declaration.support_mode is RealizationSupportMode.OPEN_REALIZATION for declaration in declarations - ): - diagnostics.append( - _artifact_diagnostic( + if requirement is not None: + diagnostics.extend( + _compiled_artifact_diagnostics( compiled, - "artifact.unsupported-open-realization", - "Backend declares no open artifact realization support.", + requirement, + manifest, + facts.for_address(compiled.address), ) ) + return diagnostics - if ( - requirement.explicitness is ExplicitnessClass.EXACT - and requirement.exact_artifact is not None - and requirement.exact_artifact.digest not in available_digests - ): - diagnostics.append( - _artifact_diagnostic( - compiled, - "artifact.unavailable-exact-artifact", - "The required immutable artifact is not available.", - ) - ) - for constraint in requirement.constraints: - if constraint.constraint_id not in satisfied_constraints: - diagnostics.append( - _artifact_diagnostic( - compiled, - "artifact.unsatisfied-constraint", - f"Artifact constraint '{constraint.constraint_id}' is not satisfied.", - ) - ) +def _compiled_artifact_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + manifest: BackendManifest, + facts: ArtifactRequirementAvailability, +) -> list[Diagnostic]: + declarations = [ + declaration for declaration in manifest.realization_support if declaration.domain == compiled.domain + ] + return [ + *_open_support_diagnostics(compiled, requirement, declarations), + *_artifact_availability_diagnostics(compiled, requirement, facts), + *_artifact_route_diagnostics(compiled, requirement, declarations), + ] + + +def _open_support_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + declarations: list[RealizationSupportDeclaration], +) -> list[Diagnostic]: + supported = any(declaration.support_mode is RealizationSupportMode.OPEN_REALIZATION for declaration in declarations) + if requirement.explicitness is ExplicitnessClass.OPEN and not supported: + return [ + _artifact_diagnostic( + compiled, + "artifact.unsupported-open-realization", + "Backend declares no open artifact realization support.", + ) + ] + return [] - for locked_input in requirement.locked_inputs: - if locked_input.input_id not in verified_locked_inputs: - diagnostics.append( - _artifact_diagnostic( - compiled, - "artifact.missing-locked-input", - f"Locked artifact input '{locked_input.input_id}' is not verified.", - ) - ) - if requirement.candidates and not any( - candidate.candidate_id in available_candidates for candidate in requirement.candidates - ): - diagnostics.append( - _artifact_diagnostic( - compiled, - "artifact.unavailable-candidate", - "No declared artifact candidate is available.", - ) +def _artifact_availability_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + facts: ArtifactRequirementAvailability, +) -> list[Diagnostic]: + return [ + *_exact_artifact_availability_diagnostics(compiled, requirement, facts), + *_constraint_availability_diagnostics(compiled, requirement, facts), + *_locked_input_availability_diagnostics(compiled, requirement, facts), + *_candidate_availability_diagnostics(compiled, requirement, facts), + *_materialization_availability_diagnostics(compiled, requirement, facts), + ] + + +def _exact_artifact_availability_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + facts: ArtifactRequirementAvailability, +) -> list[Diagnostic]: + exact = requirement.exact_artifact + if exact is not None and exact.digest not in facts.available_artifact_digests: + return [ + _artifact_diagnostic( + compiled, + "artifact.unavailable-exact-artifact", + "The required immutable artifact is not available.", ) + ] + return [] - if requirement.materialization_specifications and not any( - specification.digest in scoped_facts.available_materialization_specification_digests - for specification in requirement.materialization_specifications - ): - diagnostics.append( - _artifact_diagnostic( - compiled, - "artifact.unavailable-materialization-specification", - "No declared digest-bound materialization specification is available.", - ) - ) - supported_routes = { - ( - _artifact_mechanism_key(capability.mechanism), - route.acquisition, - route.timing, - ) - for declaration in declarations - for capability in declaration.artifact_mechanisms - if compiled.requirement_kind in capability.supported_requirement_kinds - for route in capability.supported_routes - } - if not any( - ( - _artifact_mechanism_key(route.mechanism), - route.acquisition, - route.timing, +def _constraint_availability_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + facts: ArtifactRequirementAvailability, +) -> list[Diagnostic]: + satisfied = set(facts.satisfied_constraint_ids) + return [ + _artifact_diagnostic( + compiled, + "artifact.unsatisfied-constraint", + f"Artifact constraint '{constraint.constraint_id}' is not satisfied.", + ) + for constraint in requirement.constraints + if constraint.constraint_id not in satisfied + ] + + +def _locked_input_availability_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + facts: ArtifactRequirementAvailability, +) -> list[Diagnostic]: + verified = set(facts.verified_locked_input_ids) + return [ + _artifact_diagnostic( + compiled, + "artifact.missing-locked-input", + f"Locked artifact input '{locked_input.input_id}' is not verified.", + ) + for locked_input in requirement.locked_inputs + if locked_input.input_id not in verified + ] + + +def _candidate_availability_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + facts: ArtifactRequirementAvailability, +) -> list[Diagnostic]: + available = set(facts.available_candidate_ids) + if requirement.candidates and not any(candidate.candidate_id in available for candidate in requirement.candidates): + return [ + _artifact_diagnostic( + compiled, + "artifact.unavailable-candidate", + "No declared artifact candidate is available.", ) - in supported_routes - for route in requirement.permitted_routes - ): - diagnostics.append( - _artifact_diagnostic( - compiled, - "artifact.unsupported-backend-mechanism", - "Backend declares no supported artifact mechanism and acquisition/timing route.", - ) + ] + return [] + + +def _materialization_availability_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + facts: ArtifactRequirementAvailability, +) -> list[Diagnostic]: + available = set(facts.available_materialization_specification_digests) + if requirement.materialization_specifications and not any( + specification.digest in available for specification in requirement.materialization_specifications + ): + return [ + _artifact_diagnostic( + compiled, + "artifact.unavailable-materialization-specification", + "No declared digest-bound materialization specification is available.", ) - return diagnostics + ] + return [] + + +def _artifact_route_diagnostics( + compiled: CompiledRealizationRequirement, + requirement: ArtifactRequirement, + declarations: list[RealizationSupportDeclaration], +) -> list[Diagnostic]: + supported_routes = { + ( + _artifact_mechanism_key(capability.mechanism), + route.acquisition, + route.timing, + ) + for declaration in declarations + for capability in declaration.artifact_mechanisms + if compiled.requirement_kind in capability.supported_requirement_kinds + for route in capability.supported_routes + } + permitted = any( + ( + _artifact_mechanism_key(route.mechanism), + route.acquisition, + route.timing, + ) + in supported_routes + for route in requirement.permitted_routes + ) + if permitted: + return [] + return [ + _artifact_diagnostic( + compiled, + "artifact.unsupported-backend-mechanism", + "Backend declares no supported artifact mechanism and acquisition/timing route.", + ) + ] def evaluate_artifact_realization( @@ -166,6 +241,44 @@ def evaluate_artifact_realization( return _silent_approximation_diagnostic(requirement), None facts = (availability or ArtifactAvailabilityContext()).for_address(requirement.address) + invalid = _artifact_satisfaction_invalid(requirement, contract, satisfaction, facts, manifest) + diagnostic = _silent_approximation_diagnostic(requirement) if invalid else None + provenance = None if invalid else _artifact_realization_provenance(requirement, contract, satisfaction) + return diagnostic, provenance + + +def _artifact_satisfaction_invalid( + requirement: CompiledRealizationRequirement, + contract: ArtifactRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, + facts: ArtifactRequirementAvailability, + manifest: BackendManifest | None, +) -> bool: + return any( + ( + _identity_or_route_invalid(requirement, contract, satisfaction, facts, manifest), + _candidate_selection_invalid(contract, satisfaction, facts), + _materialization_selection_invalid(contract, satisfaction, facts), + _locked_input_or_constraint_invalid(contract, satisfaction, facts), + not _trust_claims_verified(contract, satisfaction, facts), + ) + ) + + +def _identity_or_route_invalid( + requirement: CompiledRealizationRequirement, + contract: ArtifactRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, + facts: ArtifactRequirementAvailability, + manifest: BackendManifest | None, +) -> bool: + if manifest is None: + return True + selected_route = ( + _artifact_mechanism_key(satisfaction.mechanism), + satisfaction.acquisition, + satisfaction.timing, + ) permitted_routes = { ( _artifact_mechanism_key(route.mechanism), @@ -174,85 +287,104 @@ def evaluate_artifact_realization( ) for route in contract.permitted_routes } - candidate_ids = {candidate.candidate_id for candidate in contract.candidates} + exact = contract.exact_artifact + return any( + ( + satisfaction.backend != manifest.identity, + not _backend_route_supported(requirement, satisfaction, manifest), + satisfaction.requirement_id != contract.requirement_id, + selected_route not in permitted_routes, + exact is not None and satisfaction.artifact != exact, + exact is not None and exact.digest not in facts.available_artifact_digests, + ) + ) + + +def _candidate_selection_invalid( + contract: ArtifactRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, + facts: ArtifactRequirementAvailability, +) -> bool: candidates = {candidate.candidate_id: candidate for candidate in contract.candidates} + selected = candidates.get(satisfaction.candidate_id) if satisfaction.candidate_id is not None else None + selected_invalid = selected is not None and ( + satisfaction.artifact != selected.artifact or selected.candidate_id not in facts.available_candidate_ids + ) + return any( + ( + satisfaction.candidate_id is not None and selected is None, + bool(candidates) and satisfaction.candidate_id is None, + not candidates and satisfaction.candidate_id is not None, + selected_invalid, + ) + ) + + +def _materialization_selection_invalid( + contract: ArtifactRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, + facts: ArtifactRequirementAvailability, +) -> bool: specifications = { specification.specification_id: specification for specification in contract.materialization_specifications } - selected_candidate = candidates.get(satisfaction.candidate_id) if satisfaction.candidate_id is not None else None - selected_specification = ( + selected = ( specifications.get(satisfaction.materialization_specification_id) if satisfaction.materialization_specification_id is not None else None ) selected_mechanism = _artifact_mechanism_key(satisfaction.mechanism) - matching_specifications = { - specification.specification_id + requires_selection = any( + _artifact_mechanism_key(specification.profile) == selected_mechanism for specification in contract.materialization_specifications - if _artifact_mechanism_key(specification.profile) == selected_mechanism + ) + return any( + ( + satisfaction.materialization_specification_id is not None and selected is None, + selected is not None and _artifact_mechanism_key(selected.profile) != selected_mechanism, + selected is None and satisfaction.materialization_specification_digest is not None, + selected is not None and satisfaction.materialization_specification_digest != selected.digest, + selected is not None and selected.digest not in facts.available_materialization_specification_digests, + requires_selection and satisfaction.materialization_specification_id is None, + ) + ) + + +def _locked_input_or_constraint_invalid( + contract: ArtifactRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, + facts: ArtifactRequirementAvailability, +) -> bool: + specifications = { + specification.specification_id: specification for specification in contract.materialization_specifications } + selected = specifications.get(satisfaction.materialization_specification_id) locked_input_ids = {locked_input.input_id for locked_input in contract.locked_inputs} - required_locked_input_ids = ( - set(selected_specification.locked_input_ids) if selected_specification is not None else locked_input_ids - ) + required_locked_input_ids = set(selected.locked_input_ids) if selected is not None else locked_input_ids constraint_ids = {constraint.constraint_id for constraint in contract.constraints} - invalid = ( - manifest is None - or satisfaction.backend != manifest.identity - or not _backend_route_supported(requirement, satisfaction, manifest) - or satisfaction.requirement_id != contract.requirement_id - or ( - selected_mechanism, - satisfaction.acquisition, - satisfaction.timing, - ) - not in permitted_routes - or (contract.exact_artifact is not None and satisfaction.artifact != contract.exact_artifact) - or ( - contract.exact_artifact is not None - and contract.exact_artifact.digest not in facts.available_artifact_digests - ) - or (satisfaction.candidate_id is not None and selected_candidate is None) - or (bool(candidate_ids) and satisfaction.candidate_id is None) - or (not candidate_ids and satisfaction.candidate_id is not None) - or ( - selected_candidate is not None - and ( - satisfaction.artifact != selected_candidate.artifact - or selected_candidate.candidate_id not in facts.available_candidate_ids - ) - ) - or (satisfaction.materialization_specification_id is not None and selected_specification is None) - or ( - selected_specification is not None - and _artifact_mechanism_key(selected_specification.profile) != selected_mechanism - ) - or (selected_specification is None and satisfaction.materialization_specification_digest is not None) - or ( - selected_specification is not None - and satisfaction.materialization_specification_digest != selected_specification.digest - ) - or ( - selected_specification is not None - and selected_specification.digest not in facts.available_materialization_specification_digests + return any( + ( + set(satisfaction.locked_input_ids) != required_locked_input_ids, + not required_locked_input_ids.issubset(facts.verified_locked_input_ids), + set(satisfaction.satisfied_constraint_ids) != constraint_ids, + not constraint_ids.issubset(facts.satisfied_constraint_ids), ) - or (bool(matching_specifications) and satisfaction.materialization_specification_id is None) - or set(satisfaction.locked_input_ids) != required_locked_input_ids - or not required_locked_input_ids.issubset(facts.verified_locked_input_ids) - or set(satisfaction.satisfied_constraint_ids) != constraint_ids - or not constraint_ids.issubset(facts.satisfied_constraint_ids) - or not _trust_claims_verified(contract, satisfaction, facts) ) - if invalid: - return _silent_approximation_diagnostic(requirement), None + + +def _artifact_realization_provenance( + requirement: CompiledRealizationRequirement, + contract: ArtifactRequirement, + satisfaction: ArtifactSatisfactionDisclosureModel, +) -> RealizationProvenanceEntry: honoured = contract.explicitness is ExplicitnessClass.EXACT and contract.exact_artifact == satisfaction.artifact - return None, RealizationProvenanceEntry( + return RealizationProvenanceEntry( address=requirement.address, field_path=requirement.field_path, domain=requirement.domain, requirement_kind=requirement.requirement_kind, explicitness=requirement.explicitness, - provenance=(requirement.provenance if honoured else ExplicitnessProvenance.BACKEND_REALIZED), + provenance=requirement.provenance if honoured else ExplicitnessProvenance.BACKEND_REALIZED, governing_scope=requirement.governing_scope, artifact_satisfaction=satisfaction, ) diff --git a/implementations/python/packages/raes_runtime/backend_calls.py b/implementations/python/packages/raes_runtime/backend_calls.py index 001fc9a24..ea0eb982a 100644 --- a/implementations/python/packages/raes_runtime/backend_calls.py +++ b/implementations/python/packages/raes_runtime/backend_calls.py @@ -4,6 +4,7 @@ from collections.abc import Callable, Iterable from copy import deepcopy +from dataclasses import dataclass from raes_backend_protocols.capabilities import BackendManifest from raes_contracts.addressing import require_compiled_address @@ -27,6 +28,14 @@ _BACKEND_CONTRACT_INVALID = "runtime.backend-contract-invalid" +@dataclass(frozen=True) +class _RealizationApplyContext: + requirements: tuple[CompiledRealizationRequirement, ...] = () + plan: ProvisioningPlan | None = None + manifest: BackendManifest | None = None + artifact_availability: ArtifactAvailabilityContext | None = None + + def _call_backend_diagnostics( method: Callable[..., object], *args: object, @@ -53,11 +62,9 @@ def _call_backend_apply( *args: object, address: str, snapshot: RuntimeSnapshot, - realization_requirements: tuple[CompiledRealizationRequirement, ...] = (), - realization_plan: ProvisioningPlan | None = None, - backend_manifest: BackendManifest | None = None, - artifact_availability: ArtifactAvailabilityContext | None = None, + realization: _RealizationApplyContext | None = None, ) -> ApplyResult: + realization_context = realization or _RealizationApplyContext() baseline_snapshot = deepcopy(snapshot) backend_snapshot = deepcopy(snapshot) backend_args = tuple(backend_snapshot if arg is snapshot else arg for arg in args) @@ -74,10 +81,10 @@ def _call_backend_apply( result, address=address, baseline_snapshot=baseline_snapshot, - realization_requirements=realization_requirements, - realization_plan=realization_plan, - backend_manifest=backend_manifest, - artifact_availability=artifact_availability, + realization_requirements=realization_context.requirements, + realization_plan=realization_context.plan, + backend_manifest=realization_context.manifest, + artifact_availability=realization_context.artifact_availability, ) diff --git a/implementations/python/packages/raes_runtime/manager.py b/implementations/python/packages/raes_runtime/manager.py index 63502cd52..25361730f 100644 --- a/implementations/python/packages/raes_runtime/manager.py +++ b/implementations/python/packages/raes_runtime/manager.py @@ -14,7 +14,7 @@ from raes_processor.planner import plan, snapshot_delete_order from .apply_failure import maybe_synthesize_failure, rollback_services -from .backend_calls import _call_backend_apply, _call_backend_diagnostics +from .backend_calls import _call_backend_apply, _call_backend_diagnostics, _RealizationApplyContext from .diagnostics import _failure_diagnostic, _has_error_diagnostic from .participant_activity import resolve_participant_activity_controls from .participant_execution_control import RuntimeParticipantExecutionMixin @@ -195,10 +195,12 @@ def _apply_provisioning_phase( state.working_snapshot, address="runtime.apply.provisioning", snapshot=state.working_snapshot, - realization_requirements=execution_plan.model.realization_requirements, - realization_plan=execution_plan.provisioning, - backend_manifest=execution_plan.manifest, - artifact_availability=execution_plan.artifact_availability, + realization=_RealizationApplyContext( + requirements=execution_plan.model.realization_requirements, + plan=execution_plan.provisioning, + manifest=execution_plan.manifest, + artifact_availability=execution_plan.artifact_availability, + ), ) self._record_phase_result(state, provision_result) if not provision_result.success: diff --git a/implementations/python/tests/test_artifact_requirement_satisfaction.py b/implementations/python/tests/test_artifact_requirement_satisfaction.py index f45c01b78..7f4e3cea9 100644 --- a/implementations/python/tests/test_artifact_requirement_satisfaction.py +++ b/implementations/python/tests/test_artifact_requirement_satisfaction.py @@ -194,25 +194,29 @@ def test_exact_requirement_requires_one_immutable_identity_and_no_alternatives() assert source.artifact_requirement is not None assert source.artifact_requirement.explicitness is ExplicitnessClass.EXACT + exact_routes = [_route()] with pytest.raises(ValidationError, match="immutable|exact"): ArtifactRequirement( requirement_id="bad-exact", explicitness=ExplicitnessClass.EXACT, - permitted_routes=[_route()], + permitted_routes=exact_routes, ) + exact_identity = _identity() + fallback_candidate = ArtifactCandidate(candidate_id="fallback", artifact=_identity(digest=_DIGEST_B)) with pytest.raises(ValidationError, match="alternative|candidate|exact"): ArtifactRequirement( requirement_id="bad-fallback", explicitness=ExplicitnessClass.EXACT, - exact_artifact=_identity(), - candidates=[ArtifactCandidate(candidate_id="fallback", artifact=_identity(digest=_DIGEST_B))], - permitted_routes=[_route()], + exact_artifact=exact_identity, + candidates=[fallback_candidate], + permitted_routes=exact_routes, ) + exact_requirement = _exact_requirement() with pytest.raises(ValidationError, match="selector|identity|match"): Source( name="different-image", version="24.04.1", - artifact_requirement=_exact_requirement(), + artifact_requirement=exact_requirement, ) @@ -285,12 +289,13 @@ def test_materialization_and_locked_inputs_are_explicit_constrained_authority() ) assert requirement.materialization_specifications[0].locked_input_ids == ["rootfs"] + materialization_routes = [_route(mechanism=_mechanism("materialization-specification"))] with pytest.raises(ValidationError, match="locked input"): ArtifactRequirement( requirement_id="missing-input-ref", explicitness=ExplicitnessClass.CONSTRAINED, materialization_specifications=[specification], - permitted_routes=[_route(mechanism=_mechanism("materialization-specification"))], + permitted_routes=materialization_routes, ) From 6390f4e4e994224be3f5e81947071e0f5fa188b9 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 06:18:36 +0200 Subject: [PATCH 46/55] docs: fix ADR-097 specification reference --- .../adrs/adr-097-portable-artifact-requirement-satisfaction.md | 3 ++- docs/decisions/adrs/adr-index.yaml | 2 +- tools/policy/historical_identity_records.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md b/docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md index 0fb75e8f6..478885d54 100644 --- a/docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md +++ b/docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md @@ -140,4 +140,5 @@ with authorized backend and trust-policy owners. - [ADR-070](adr-070-realization-envelope-semantics.md) - [ADR-071](adr-071-reusable-asset-trust-and-integrity-policy.md) - [ADR-077](adr-077-associated-artifact-manifest-boundary.md) -- [Portable Artifact Requirement Satisfaction](../../../specs/supply-chain/artifact-requirement-satisfaction.md) +- Portable Artifact Requirement Satisfaction: + `specs/supply-chain/artifact-requirement-satisfaction.md` diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 9644f3e75..27b21f946 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -451,4 +451,4 @@ adrs: summary: "Retained the existing SonarCloud project key as an exact, content-bound external-service designation rather than a current RAES product identity." - id: ADR-097 path: docs/decisions/adrs/adr-097-portable-artifact-requirement-satisfaction.md - pin: 39a8c1b3865355cbd4612e1367b51f8b80a0977a7f5748b68a5165b1228dd417 + pin: 4a69740d5600298ae1b8be29c1f974a7029a8cbabeb08caaca9d4544a9d7cba5 diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index 98696ad54..a61db4a9f 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -499,7 +499,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surf\u0061ces.", "occurrences": 4, - "content_sha256": "534fbb9db337f6a9abe186f7ca5048988537a00b3f0e9c619213bb7991b5946f" + "content_sha256": "597c105d8f011cbb4b1ee1f42f2fb38304d0dece69f54edf3c8fa418c3311271" }, { "path": "docs/decisions/cage-2-replication-design.md", From e151b13eaaaeae0a04a0147814a1e5863ad161a0 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 06:30:13 +0200 Subject: [PATCH 47/55] Finish participant resource complexity cleanup --- .../_participant_resource_budget_owners.py | 51 +++++++---- .../participant_resource_budgets.py | 83 +++++++++++------- .../participant_resource_reservation.py | 84 +++++++++++++------ .../participant_scheduler_lifecycle.py | 40 +++++---- 4 files changed, 169 insertions(+), 89 deletions(-) diff --git a/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py b/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py index a3de24c3e..1c12eb6ab 100644 --- a/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py +++ b/implementations/python/packages/raes/validator/_participant_resource_budget_owners.py @@ -63,22 +63,41 @@ def _owner_error( ) -> str | None: kind = getattr(getattr(owner, "kind", ""), "value", getattr(owner, "kind", "")) ref = str(getattr(owner, "ref", "")) - if kind == "participant" and ref.removeprefix("agents.") not in scope.participant_refs: - return f"{label} participant ref '{ref}' is outside the policy participant scope" - if kind == "deployment_tenant": - tenant_ref = ref.removeprefix(_TENANT_PREFIX) - if tenant_ref not in scope.deployment_tenants: - return f"{label} deployment tenant ref '{ref}' is undefined" - if tenant_ref not in scope.target_tenants: - return f"{label} deployment tenant ref '{ref}' does not own an authorized action target" - if kind == "shared_service": - if split_node_service_ref(ref) is None: - return f"{label} shared service ref '{ref}' is undefined" - if ref not in scope.action_targets: - return f"{label} shared service ref '{ref}' is not an exact execution target" - if not any((tenant, ref) in scope.shared_permissions for tenant in scope.declared_tenants): - return f"{label} shared service ref '{ref}' lacks an authorized tenant uses_shared_service edge" - return None + error = None + if kind == "participant": + if ref.removeprefix("agents.") not in scope.participant_refs: + error = f"{label} participant ref '{ref}' is outside the policy participant scope" + elif kind == "deployment_tenant": + error = _tenant_owner_error(label, ref, scope) + elif kind == "shared_service": + error = _shared_service_owner_error(label, ref, scope, split_node_service_ref) + return error + + +def _tenant_owner_error(label: str, ref: str, scope: _OwnerScope) -> str | None: + tenant_ref = ref.removeprefix(_TENANT_PREFIX) + error = None + if tenant_ref not in scope.deployment_tenants: + error = f"{label} deployment tenant ref '{ref}' is undefined" + elif tenant_ref not in scope.target_tenants: + error = f"{label} deployment tenant ref '{ref}' does not own an authorized action target" + return error + + +def _shared_service_owner_error( + label: str, + ref: str, + scope: _OwnerScope, + split_node_service_ref: Callable[[str], object | None], +) -> str | None: + error = None + if split_node_service_ref(ref) is None: + error = f"{label} shared service ref '{ref}' is undefined" + elif ref not in scope.action_targets: + error = f"{label} shared service ref '{ref}' is not an exact execution target" + elif not any((tenant, ref) in scope.shared_permissions for tenant in scope.declared_tenants): + error = f"{label} shared service ref '{ref}' lacks an authorized tenant uses_shared_service edge" + return error def participant_resource_budget_owner_errors( diff --git a/implementations/python/packages/raes_runtime/participant_resource_budgets.py b/implementations/python/packages/raes_runtime/participant_resource_budgets.py index 3cfb6ef8c..a78819c21 100644 --- a/implementations/python/packages/raes_runtime/participant_resource_budgets.py +++ b/implementations/python/packages/raes_runtime/participant_resource_budgets.py @@ -169,6 +169,55 @@ def _new_budget_state( ) +def _existing_budget_failure( + mutation: _InitializationMutation, + policy: ResourcePolicy, + demand: ResourceDemand, + state_ref: str, +) -> tuple[bool, ApplyResult | None]: + existing = mutation.states.get(state_ref) + failure = None + if existing is not None and _state(existing).generation != mutation.execution_generation: + failure = _initialization_failure( + mutation, + policy, + "runtime.participant-resource-state-conflict", + f"resource budget {demand.budget_id} already has incompatible state", + ) + return existing is not None, failure + + +def _initialize_new_budget( + mutation: _InitializationMutation, + policy: ResourcePolicy, + demand: ResourceDemand, + pool: ResourcePool, + state_ref: str, +) -> ApplyResult | None: + budget_state = _new_budget_state(policy, demand, pool, mutation.execution_generation) + exact_pool_ref = pool_state_ref(pool) + existing_pool = mutation.pool_states.get(exact_pool_ref) + physical_pool = new_pool_state(pool) if existing_pool is None else _pool_state(existing_pool) + failure = None + try: + physical_pool = ensure_allocation( + physical_pool, + budget_state, + fairness=policy.resource_fairness, + ) + except ValueError as exc: + failure = _initialization_failure( + mutation, + policy, + "runtime.participant-resource-pool-conflict", + str(exc), + ) + if failure is None: + mutation.states[state_ref] = _payload(budget_state) + mutation.pool_states[exact_pool_ref] = _payload(physical_pool) + return failure + + def _initialize_demand( mutation: _InitializationMutation, policy: ResourcePolicy, @@ -193,37 +242,9 @@ def _initialize_demand( ) else: state_ref = participant_resource_budget_state_ref(policy.address, demand.budget_id) - existing = mutation.states.get(state_ref) - if existing is not None: - current = _state(existing) - if current.generation != mutation.execution_generation: - failure = _initialization_failure( - mutation, - policy, - "runtime.participant-resource-state-conflict", - f"resource budget {demand.budget_id} already has incompatible state", - ) - else: - budget_state = _new_budget_state(policy, demand, pool, mutation.execution_generation) - exact_pool_ref = pool_state_ref(pool) - existing_pool = mutation.pool_states.get(exact_pool_ref) - physical_pool = new_pool_state(pool) if existing_pool is None else _pool_state(existing_pool) - try: - physical_pool = ensure_allocation( - physical_pool, - budget_state, - fairness=policy.resource_fairness, - ) - except ValueError as exc: - failure = _initialization_failure( - mutation, - policy, - "runtime.participant-resource-pool-conflict", - str(exc), - ) - if failure is None: - mutation.states[state_ref] = _payload(budget_state) - mutation.pool_states[exact_pool_ref] = _payload(physical_pool) + exists, failure = _existing_budget_failure(mutation, policy, demand, state_ref) + if not exists: + failure = _initialize_new_budget(mutation, policy, demand, pool, state_ref) return failure diff --git a/implementations/python/packages/raes_runtime/participant_resource_reservation.py b/implementations/python/packages/raes_runtime/participant_resource_reservation.py index 363df19a0..33ce8f4b2 100644 --- a/implementations/python/packages/raes_runtime/participant_resource_reservation.py +++ b/implementations/python/packages/raes_runtime/participant_resource_reservation.py @@ -152,15 +152,12 @@ def _requested_amount( return amount -def _check_reservation( +def _budget_state_check( mutation: _ReservationMutation, demand: ResourceDemand, state_ref: str, - requested_quantities: Mapping[str, int] | None, -) -> _ReservationCheck: - current: ParticipantResourceBudgetStateModel | None = None - physical_pool: ParticipantResourcePoolStateModel | None = None - amount = 0 +) -> tuple[ParticipantResourceBudgetStateModel | None, ApplyResult | None]: + current = None failure = None raw = mutation.states.get(state_ref) if raw is None: @@ -171,30 +168,67 @@ def _check_reservation( ) else: current = _state(raw) - if current is not None and current.generation != mutation.execution_generation: + if current.generation != mutation.execution_generation: + failure = _failure( + mutation, + "runtime.participant-resource-stale-generation", + ( + f"resource budget {demand.budget_id} is generation {current.generation}; " + f"request is generation {mutation.execution_generation}" + ), + ) + return current, failure + + +def _pool_state_check( + mutation: _ReservationMutation, + demand: ResourceDemand, + current: ParticipantResourceBudgetStateModel, + requested_quantities: Mapping[str, int] | None, +) -> tuple[ParticipantResourcePoolStateModel | None, int, ApplyResult | None]: + amount = _requested_amount(demand, requested_quantities) + raw_pool = mutation.pool_states.get(_pool_ref_for_state(current)) + physical_pool = None + failure = None + if raw_pool is None: failure = _failure( mutation, - "runtime.participant-resource-stale-generation", - ( - f"resource budget {demand.budget_id} is generation {current.generation}; " - f"request is generation {mutation.execution_generation}" - ), + "runtime.participant-resource-pool-state-missing", + f"physical pool for resource budget {demand.budget_id} was not initialized", ) + else: + physical_pool = _pool_state(raw_pool) + return physical_pool, amount, failure + + +def _capacity_failure( + mutation: _ReservationMutation, + demand: ResourceDemand, + current: ParticipantResourceBudgetStateModel, + physical_pool: ParticipantResourcePoolStateModel, + state_ref: str, + amount: int, +) -> ApplyResult | None: + budget_available = _used_capacity(current) + amount <= min(current.limit, current.configured_capacity) + failure = None + if not budget_available or not pool_can_reserve(physical_pool, state_ref, amount): + failure = _throttled_result(mutation, demand, current, amount, budget_available) + return failure + + +def _check_reservation( + mutation: _ReservationMutation, + demand: ResourceDemand, + state_ref: str, + requested_quantities: Mapping[str, int] | None, +) -> _ReservationCheck: + current, failure = _budget_state_check(mutation, demand, state_ref) + physical_pool: ParticipantResourcePoolStateModel | None = None + amount = 0 if current is not None and failure is None: - amount = _requested_amount(demand, requested_quantities) - raw_pool = mutation.pool_states.get(_pool_ref_for_state(current)) - if raw_pool is None: - failure = _failure( - mutation, - "runtime.participant-resource-pool-state-missing", - f"physical pool for resource budget {demand.budget_id} was not initialized", - ) - else: - physical_pool = _pool_state(raw_pool) + physical_pool, amount, failure = _pool_state_check(mutation, demand, current, requested_quantities) if current is not None and physical_pool is not None and failure is None: - budget_available = _used_capacity(current) + amount <= min(current.limit, current.configured_capacity) - if not budget_available or not pool_can_reserve(physical_pool, state_ref, amount): - failure = _throttled_result(mutation, demand, current, amount, budget_available) + failure = _capacity_failure(mutation, demand, current, physical_pool, state_ref, amount) return _ReservationCheck( demand=demand, current=current, diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py b/implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py index 12637663c..e4f073444 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_lifecycle.py @@ -42,6 +42,25 @@ def _missing_execution_service_result( ) +def _reset_resource_generation( + policy: ParticipantAutonomousExecutionRuntime, + snapshot: RuntimeSnapshot, +) -> ApplyResult: + service_payload = snapshot.participant_execution_services.get(policy.address) + if service_payload is None: + return _missing_execution_service_result(policy, snapshot) + service = ParticipantExecutionServiceStateModel.model_validate(service_payload) + generation = service.generation + 1 + return reconcile_participant_resource_budgets( + snapshot, + policy_address=policy.address, + current_generation=service.generation, + next_generation=generation, + boundary="time_segment", + evidence_refs=(f"evidence:{policy.address}:shared-time-reset:generation-{generation}",), + ) + + def reset_policy_at_clock( policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel, @@ -73,24 +92,11 @@ def reset_policy_at_clock( working = participant_result.snapshot changed.extend(participant_result.changed_addresses) if failure is None and policy.profile == _RESOURCE_GOVERNED_PROFILE: - service_payload = working.participant_execution_services.get(policy.address) - if service_payload is None: - failure = _missing_execution_service_result(policy, working) + budget_reset = _reset_resource_generation(policy, working) + if budget_reset.success: + working = budget_reset.snapshot else: - service = ParticipantExecutionServiceStateModel.model_validate(service_payload) - generation = service.generation + 1 - budget_reset = reconcile_participant_resource_budgets( - working, - policy_address=policy.address, - current_generation=service.generation, - next_generation=generation, - boundary="time_segment", - evidence_refs=(f"evidence:{policy.address}:shared-time-reset:generation-{generation}",), - ) - if budget_reset.success: - working = budget_reset.snapshot - else: - failure = budget_reset + failure = budget_reset if failure is None: working, service_changed = reset_execution_service(working, policy.address) if service_changed: From c3feb3023441f2a1d4590fde5a5b92da7ce2a338 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 06:36:16 +0200 Subject: [PATCH 48/55] refactor: simplify artifact invariant parsing --- .../raes_contracts/artifact_requirements.py | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/implementations/python/packages/raes_contracts/artifact_requirements.py b/implementations/python/packages/raes_contracts/artifact_requirements.py index 7aab32b5b..e6e72ed39 100644 --- a/implementations/python/packages/raes_contracts/artifact_requirements.py +++ b/implementations/python/packages/raes_contracts/artifact_requirements.py @@ -47,15 +47,23 @@ def artifact_requirement_invariant_violations(payload: object) -> tuple[str, ... def _artifact_requirement_mappings( payload: object, ) -> tuple[Mapping[object, object] | None, Mapping[object, object] | None, str | None]: + source: Mapping[object, object] | None = None + requirement: Mapping[object, object] | None = None + violation: str | None = None if not isinstance(payload, Mapping): - return None, None, "artifact-requirement-document-object" - source = payload.get("source") - if not isinstance(source, Mapping): - return None, None, "artifact-requirement-source-object" - requirement = source.get("artifact_requirement") - if not isinstance(requirement, Mapping): - return None, None, "artifact-requirement-present" - return source, requirement, None + violation = "artifact-requirement-document-object" + else: + source_payload = payload.get("source") + if not isinstance(source_payload, Mapping): + violation = "artifact-requirement-source-object" + else: + source = source_payload + requirement_payload = source.get("artifact_requirement") + if not isinstance(requirement_payload, Mapping): + violation = "artifact-requirement-present" + else: + requirement = requirement_payload + return source, requirement, violation def _exact_identity_violations( @@ -76,7 +84,6 @@ def _exact_identity_violations( def _materialization_violations(requirement: Mapping[object, object]) -> list[str]: - violations: list[str] = [] locked_inputs = requirement.get("locked_inputs", []) declared_input_ids = { item.get("input_id") @@ -84,18 +91,27 @@ def _materialization_violations(requirement: Mapping[object, object]) -> list[st if isinstance(item, Mapping) and isinstance(item.get("input_id"), str) } specifications = requirement.get("materialization_specifications", []) + violations: list[str] = [] for specification in specifications: - if not isinstance(specification, Mapping): - continue - profile = specification.get("profile") - mechanism = profile.get("mechanism") if isinstance(profile, Mapping) else None - if mechanism != "materialization-specification" and not ( - isinstance(mechanism, str) and mechanism.startswith("x-") - ): - violations.append("materialization-profile-mechanism") - referenced = specification.get("locked_input_ids", []) - if isinstance(referenced, list) and not set(referenced).issubset(declared_input_ids): - violations.append("materialization-locked-input-join") + violations.extend(_materialization_specification_violations(specification, declared_input_ids)) + return violations + + +def _materialization_specification_violations( + specification: object, + declared_input_ids: set[object], +) -> list[str]: + if not isinstance(specification, Mapping): + return [] + violations: list[str] = [] + profile = specification.get("profile") + mechanism = profile.get("mechanism") if isinstance(profile, Mapping) else None + governed_extension = isinstance(mechanism, str) and mechanism.startswith("x-") + if mechanism != "materialization-specification" and not governed_extension: + violations.append("materialization-profile-mechanism") + referenced = specification.get("locked_input_ids", []) + if isinstance(referenced, list) and not set(referenced).issubset(declared_input_ids): + violations.append("materialization-locked-input-join") return violations From 66a86f24181976ceb5b1d6b7d9d02a1cd0fca5ae Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 07:33:50 +0200 Subject: [PATCH 49/55] fix: record canonical schema publication hashes --- contracts/schema-publication/entries/backend-manifest-v2.json | 4 ++-- contracts/schema-publication/entries/backend-profile-v1.json | 4 ++-- .../entries/instantiated-scenario-snapshot-v1.json | 4 ++-- .../schema-publication/entries/instantiated-scenario-v1.json | 4 ++-- contracts/schema-publication/entries/runtime-snapshot-v1.json | 4 ++-- .../entries/scenario-satisfiability-evidence-v1.json | 4 ++-- .../schema-publication/entries/sdl-authoring-input-v1.json | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index 26e594e15..daeaca400 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": "9cf0d750399b97c7d5eb9a988f6525898e10992567b724806e4ae9fbf9297154", + "content_hash": "8f140797bd697fb49969fcf61a50c6b658f9b7e002d46f3a63cd566109436e83", "last_change": { "summary": "Added portable artifact satisfaction capabilities and participant resource-budget support for issues #920 and #899.", - "content_hash": "9cf0d750399b97c7d5eb9a988f6525898e10992567b724806e4ae9fbf9297154" + "content_hash": "8f140797bd697fb49969fcf61a50c6b658f9b7e002d46f3a63cd566109436e83" } } diff --git a/contracts/schema-publication/entries/backend-profile-v1.json b/contracts/schema-publication/entries/backend-profile-v1.json index 70bfb0ba0..73008ac5f 100644 --- a/contracts/schema-publication/entries/backend-profile-v1.json +++ b/contracts/schema-publication/entries/backend-profile-v1.json @@ -2,9 +2,9 @@ "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "982fe0c4737cc4921a711b9acecb65a326567bac44f19d4d356d5069b239b60b", + "content_hash": "96ee9545d16749f1a3f2c80150950517cbe738ea21d7420befc62cffda73c157", "last_change": { "summary": "Advertised portable artifact requirements and participant resource-budget contracts for issues #920 and #899.", - "content_hash": "982fe0c4737cc4921a711b9acecb65a326567bac44f19d4d356d5069b239b60b" + "content_hash": "96ee9545d16749f1a3f2c80150950517cbe738ea21d7420befc62cffda73c157" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json index 662bcb447..1ec99433e 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": "5e0fd9ed60af4e83b9e6cadfba630da9ebea8caff439e8c75291627b704e362b", + "content_hash": "871d7c3f4cbb86a50caf28ec04c957e2624f893c9956ab1ee80880a8e41e2608", "last_change": { "summary": "Published portable artifact requirements and scoped participant resource budgets in immutable scenario snapshots for issues #920 and #899.", - "content_hash": "5e0fd9ed60af4e83b9e6cadfba630da9ebea8caff439e8c75291627b704e362b" + "content_hash": "871d7c3f4cbb86a50caf28ec04c957e2624f893c9956ab1ee80880a8e41e2608" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-v1.json b/contracts/schema-publication/entries/instantiated-scenario-v1.json index a56b0f497..983a88cc5 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": "b35d3a2b6f6f5be51ba7425a6d84c5bfb4f8643e100d93f8ffedf11c033d6964", + "content_hash": "91fc70f7f34b378304d125aed4729a6d20048a2539ff00abc3729af2be7cfbfe", "last_change": { "summary": "Published portable artifact requirements and scoped participant resource budgets in instantiated scenarios for issues #920 and #899.", - "content_hash": "b35d3a2b6f6f5be51ba7425a6d84c5bfb4f8643e100d93f8ffedf11c033d6964" + "content_hash": "91fc70f7f34b378304d125aed4729a6d20048a2539ff00abc3729af2be7cfbfe" } } diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index def1a52f3..7b7e432ad 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": "feff711696fe15419039cb14fce07ecfcf1066815786b79611688b01a0e3e34e", + "content_hash": "73b2b3eba4400777c380b1d532614d29c2eb83ec82da5fef127918768aa2f8d8", "last_change": { "summary": "Added artifact satisfaction evidence and participant resource-budget state and allocation evidence for issues #920 and #899.", - "content_hash": "feff711696fe15419039cb14fce07ecfcf1066815786b79611688b01a0e3e34e" + "content_hash": "73b2b3eba4400777c380b1d532614d29c2eb83ec82da5fef127918768aa2f8d8" } } diff --git a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json index 63cf5fd77..abc441202 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": "a6b8b78a64918a0a10b4e411b473f2dec3ccb518605231a3c69242a1e3754121", + "content_hash": "3ebaa6f78e03ebbdf06052cfc85abc4f1f40746ce0ea7a8eccc96637e1fcdace", "last_change": { "summary": "Carried portable artifact requirements and scoped participant resource budgets through satisfiability evidence for issues #920 and #899.", - "content_hash": "a6b8b78a64918a0a10b4e411b473f2dec3ccb518605231a3c69242a1e3754121" + "content_hash": "3ebaa6f78e03ebbdf06052cfc85abc4f1f40746ce0ea7a8eccc96637e1fcdace" } } diff --git a/contracts/schema-publication/entries/sdl-authoring-input-v1.json b/contracts/schema-publication/entries/sdl-authoring-input-v1.json index 079de9c8c..2bd64f743 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": "c558d108dd38eea13ebd8d6c85dd091fa98b8728846d44098c6acfeb22c17fe8", + "content_hash": "d8e490195ae4fb192e559686d8089c8b13ac69d19fb1b8682192275f75f5623e", "last_change": { "summary": "Published portable Source artifact authoring and scoped participant resource-budget policy for issues #920 and #899.", - "content_hash": "c558d108dd38eea13ebd8d6c85dd091fa98b8728846d44098c6acfeb22c17fe8" + "content_hash": "d8e490195ae4fb192e559686d8089c8b13ac69d19fb1b8682192275f75f5623e" } } From 298937bd2817cfa402c4de269ea11f5e976f9baf Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 08:04:21 +0200 Subject: [PATCH 50/55] fix: resolve Sonar quality gate findings --- .../raes/semantics/participant_behavior.py | 194 +++++---- .../capability_admission.py | 10 +- .../participant_capabilities.py | 121 +++--- .../raes_contracts/contracts/bundle.py | 11 +- .../raes_contracts/contracts/manifests.py | 69 +-- .../contracts/participant_runtime.py | 8 +- .../packages/raes_contracts/runtime_state.py | 226 +++++----- .../participant_autonomous_execution.py | 202 +++++---- .../raes_reference_backend/manifest.py | 112 ++--- .../raes_runtime/participant_activity.py | 102 +++-- .../participant_activity_support.py | 8 +- .../raes_runtime/participant_scheduler.py | 220 ++++++---- .../participant_scheduler_concurrency.py | 16 +- .../participant_scheduler_operations.py | 401 ++++++++++++------ .../participant_scheduler_reset.py | 19 +- ...st_dsl_437_benign_participant_execution.py | 12 +- .../tests/test_random_stream_profile.py | 5 +- 17 files changed, 1070 insertions(+), 666 deletions(-) diff --git a/implementations/python/packages/raes/semantics/participant_behavior.py b/implementations/python/packages/raes/semantics/participant_behavior.py index 76ae04795..7a7d19531 100644 --- a/implementations/python/packages/raes/semantics/participant_behavior.py +++ b/implementations/python/packages/raes/semantics/participant_behavior.py @@ -559,66 +559,96 @@ def _autonomous_clock_binding_issues( 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 - profile = getattr(context.policy, "profile", "participant-autonomous-execution/v1") - activity_policy = profile in { + activity_policy = getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { "participant-autonomous-execution/v2", "participant-autonomous-execution/v3", } - constraint_refs = ( - [*context.policy.work_window_refs, *context.policy.pause_window_refs] - if activity_policy - else list(context.policy.temporal_constraint_refs) - ) - for constraint_ref in constraint_refs: - if context.is_unresolved(constraint_ref): - continue - constraint_name = _resolve_section_ref( + for constraint_ref in _autonomous_constraint_refs(context, activity_policy=activity_policy): + reference_issues, constraint = _autonomous_constraint_reference_issues( + context, constraint_ref, - "temporal_constraints", - context.references.temporal_constraints, - ) - constraint = ( - context.references.temporal_constraints.get(constraint_name) if constraint_name is not None else None + activity_policy=activity_policy, ) + issues.extend(reference_issues) if constraint is None: - issues.append(_autonomous_issue(context, "participant.autonomous-constraint-unbound", constraint_ref)) continue kind = getattr(getattr(constraint, "constraint_kind", None), "value", "") cadence_count += int(kind == "cadence") if kind == "cadence": cadence = constraint - 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": - subjects = {str(ref) for ref in getattr(constraint, "subject_refs", ())} - if context.spec_name not in subjects and not context.participants.issubset(subjects): - issues.append( - _autonomous_issue( - context, - "participant.autonomous-activity-window-subject-mismatch", - constraint_ref, - ) - ) - if getattr(constraint, "clock_ref", None) != context.policy.clock_ref: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-constraint-clock-mismatch", - constraint_ref, - ) - ) 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 @@ -673,50 +703,58 @@ def _autonomous_progression_issues( return issues -def _autonomous_stepped_cadence_issues( +def _autonomous_stepped_issue_code( context: _AutonomousExecutionReferenceContext, bindings: _AutonomousTimeBindings, - progression_mode: str, -) -> list[ParticipantBehaviorIssue]: - if progression_mode != "stepped": - return [] +) -> str | None: + issue_code = None step_ticks = getattr(bindings.progression, "step_ticks", None) - if getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { + activity_policy = getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { "participant-autonomous-execution/v2", "participant-autonomous-execution/v3", - }: + } + if activity_policy: minimum_ticks = context.policy.timing.minimum_ticks maximum_ticks = context.policy.timing.maximum_ticks - if isinstance(step_ticks, int) and not minimum_ticks % step_ticks and not maximum_ticks % step_ticks: - return [] - return [ - _autonomous_issue( - context, - "participant.autonomous-activity-timing-unreachable", - context.policy.progression_policy_ref, - ) - ] - if bindings.cadence_count != 1 or bindings.cadence is None: - return [] - 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 - reachable = ( - 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 - ) - if reachable: - return [] - return [ - _autonomous_issue( + reachable = isinstance(step_ticks, int) and not minimum_ticks % step_ticks and not maximum_ticks % step_ticks + if not reachable: + issue_code = "participant.autonomous-activity-timing-unreachable" + elif bindings.cadence_count == 1 and bindings.cadence is not None: + 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 + reachable = ( + 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 + ) + if not reachable: + issue_code = "participant.autonomous-cadence-unreachable" + return issue_code + + +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, - "participant.autonomous-cadence-unreachable", - context.policy.progression_policy_ref, + 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( diff --git a/implementations/python/packages/raes_backend_protocols/capability_admission.py b/implementations/python/packages/raes_backend_protocols/capability_admission.py index ce1f99b61..a482ba5af 100644 --- a/implementations/python/packages/raes_backend_protocols/capability_admission.py +++ b/implementations/python/packages/raes_backend_protocols/capability_admission.py @@ -12,9 +12,9 @@ PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, PARTICIPANT_RUNTIME_ROLE_SCOPE, ) -from .participant_feature_admission import participant_feature_support_gaps as participant_feature_support_gaps from .participant_feature_admission import ( - resolve_participant_feature_support as resolve_participant_feature_support, + participant_feature_support_gaps, + resolve_participant_feature_support, ) from .participant_resource_admission import ( ResourceGovernedPolicy, @@ -430,3 +430,9 @@ def require_cleanup_plan_capability(manifest: BackendManifest, plan: TrialCleanu required_cleanup = any(obligation.requirement == "required" for obligation in plan.cleanup_obligations.values()) if required_cleanup and not cleanup.supports_residual_state_disclosure: raise ValueError("required cleanup needs backend residual-state disclosure") + + +__all__ = [ + "participant_feature_support_gaps", + "resolve_participant_feature_support", +] diff --git a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py index 79aa177c8..253b62631 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -41,6 +41,46 @@ def _validate_participant_feature_support_term(feature: str) -> None: ) +def _participant_feature_support_level( + value: ParticipantFeatureSupportLevel | str, +) -> ParticipantFeatureSupportLevel: + try: + if isinstance(value, ParticipantFeatureSupportLevel): + return value + return ParticipantFeatureSupportLevel(str(value)) + except ValueError as exc: + raise ValueError("ParticipantFeatureSupport.support_level must be a valid support level") from exc + + +def _participant_feature_refs(field_name: str, values: tuple[str, ...]) -> tuple[str, ...]: + normalized = tuple(values) + _validate_unique_non_empty_strings(f"ParticipantFeatureSupport.{field_name}", normalized) + return normalized + + +def _validate_participant_feature_evidence( + *, + feature: str, + support_level: ParticipantFeatureSupportLevel, + constraint_refs: tuple[str, ...], + limitation_refs: tuple[str, ...], + disclosure_refs: tuple[str, ...], + evidence_refs: tuple[str, ...], +) -> None: + if support_level != ParticipantFeatureSupportLevel.EXACT and not disclosure_refs: + raise ValueError( + "ParticipantFeatureSupport disclosure_refs must be non-empty when support_level is below exact" + ) + if feature not in PARTICIPANT_RUNTIME_POLICY_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") + if support_level == ParticipantFeatureSupportLevel.BOUNDED and not constraint_refs: + raise ValueError("ParticipantFeatureSupport constraint_refs must be non-empty for bounded policy support") + if support_level != ParticipantFeatureSupportLevel.UNSUPPORTED and not evidence_refs: + raise ValueError("ParticipantFeatureSupport evidence_refs must be non-empty for positive policy support") + + @dataclass(frozen=True) class ParticipantFeatureSupport: """API-407 per-feature participant runtime support declaration.""" @@ -56,39 +96,19 @@ def __post_init__(self) -> None: if not self.feature.strip(): raise ValueError("ParticipantFeatureSupport.feature must be non-empty") _validate_participant_feature_support_term(self.feature) - try: - support_level = ( - self.support_level - if isinstance(self.support_level, ParticipantFeatureSupportLevel) - else ParticipantFeatureSupportLevel(str(self.support_level)) - ) - except ValueError as exc: - raise ValueError("ParticipantFeatureSupport.support_level must be a valid support level") from exc - constraint_refs = tuple(self.constraint_refs) - limitation_refs = tuple(self.limitation_refs) - disclosure_refs = tuple(self.disclosure_refs) - evidence_refs = tuple(self.evidence_refs) - _validate_unique_non_empty_strings("ParticipantFeatureSupport.constraint_refs", constraint_refs) - _validate_unique_non_empty_strings("ParticipantFeatureSupport.limitation_refs", limitation_refs) - _validate_unique_non_empty_strings("ParticipantFeatureSupport.disclosure_refs", disclosure_refs) - _validate_unique_non_empty_strings("ParticipantFeatureSupport.evidence_refs", evidence_refs) - if support_level != ParticipantFeatureSupportLevel.EXACT and not disclosure_refs: - raise ValueError( - "ParticipantFeatureSupport disclosure_refs must be non-empty when support_level is below exact" - ) - if self.feature in PARTICIPANT_RUNTIME_POLICY_FEATURES: - if support_level != ParticipantFeatureSupportLevel.EXACT and not limitation_refs: - raise ValueError( - "ParticipantFeatureSupport limitation_refs must be non-empty for below-exact policy support" - ) - if support_level == ParticipantFeatureSupportLevel.BOUNDED and not constraint_refs: - raise ValueError( - "ParticipantFeatureSupport constraint_refs must be non-empty for bounded policy support" - ) - if support_level != ParticipantFeatureSupportLevel.UNSUPPORTED and not evidence_refs: - raise ValueError( - "ParticipantFeatureSupport evidence_refs must be non-empty for positive policy support" - ) + support_level = _participant_feature_support_level(self.support_level) + constraint_refs = _participant_feature_refs("constraint_refs", self.constraint_refs) + limitation_refs = _participant_feature_refs("limitation_refs", self.limitation_refs) + disclosure_refs = _participant_feature_refs("disclosure_refs", self.disclosure_refs) + evidence_refs = _participant_feature_refs("evidence_refs", self.evidence_refs) + _validate_participant_feature_evidence( + feature=self.feature, + support_level=support_level, + constraint_refs=constraint_refs, + limitation_refs=limitation_refs, + disclosure_refs=disclosure_refs, + evidence_refs=evidence_refs, + ) object.__setattr__(self, "support_level", support_level) object.__setattr__(self, "constraint_refs", constraint_refs) object.__setattr__(self, "limitation_refs", limitation_refs) @@ -355,22 +375,25 @@ def _autonomous_limits(self) -> tuple[tuple[str, int | None], ...]: ) def _has_autonomous_configuration(self) -> bool: - return bool( - self.supported_autonomous_selection_strategies - or self.supported_autonomous_action_contracts - or self.supported_autonomous_observation_boundaries - or self.supported_autonomous_target_addresses - or self.supported_autonomous_policy_profiles - or self.supported_autonomous_activity_features - or self.supported_autonomous_random_stream_profiles - or self.execution_bindings - or self.supports_execution_control - or self.supported_execution_control_actions - or self.supports_bounded_concurrency - or self.max_execution_services is not None - or self.max_concurrent_actions is not None - or self.resource_budgets is not None - or any(value is not None for _, value in self._autonomous_limits()) + limits_configured = any(value is not None for _, value in self._autonomous_limits()) + return any( + ( + self.supported_autonomous_selection_strategies, + self.supported_autonomous_action_contracts, + self.supported_autonomous_observation_boundaries, + self.supported_autonomous_target_addresses, + self.supported_autonomous_policy_profiles, + self.supported_autonomous_activity_features, + self.supported_autonomous_random_stream_profiles, + self.execution_bindings, + self.supports_execution_control, + self.supported_execution_control_actions, + self.supports_bounded_concurrency, + self.max_execution_services is not None, + self.max_concurrent_actions is not None, + self.resource_budgets is not None, + limits_configured, + ) ) diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index 2e6d1a2fa..41912f71d 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -115,7 +115,7 @@ ) -def _raw_schema_bundle() -> dict[str, dict[str, Any]]: +def _core_schema_bundle() -> dict[str, dict[str, Any]]: from raes_contracts.realization_envelope import BackendRealizationEnvelopeModel from ..behavioral_relations import BehavioralRelationCatalogModel @@ -185,6 +185,11 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: "scientific-completeness-assessment-v1": ScientificCompletenessAssessmentModel.model_json_schema(), "validation-profile-catalog-v1": ValidationProfileCatalogModel.model_json_schema(), "validation-basis-disclosure-v1": ValidationBasisDisclosureDocumentModel.model_json_schema(), + } + + +def _runtime_schema_bundle() -> dict[str, dict[str, Any]]: + return { "evaluation-history-event-stream-v1": _event_stream_schema( "EvaluationHistoryEventStream", EvaluationHistoryEventModel.model_json_schema(), @@ -227,6 +232,10 @@ def _raw_schema_bundle() -> dict[str, dict[str, Any]]: } +def _raw_schema_bundle() -> dict[str, dict[str, Any]]: + return {**_core_schema_bundle(), **_runtime_schema_bundle()} + + @cache def _schema_bundle_template() -> dict[str, dict[str, Any]]: """Build the immutable-in-practice template used by :func:`schema_bundle`.""" diff --git a/implementations/python/packages/raes_contracts/contracts/manifests.py b/implementations/python/packages/raes_contracts/contracts/manifests.py index a5f53a886..b44e99d4d 100644 --- a/implementations/python/packages/raes_contracts/contracts/manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/manifests.py @@ -217,32 +217,36 @@ def _validate_autonomous_configuration(self) -> None: declares_autonomous = "autonomous_execution" in self.supported_behavior_features if declares_autonomous != self.supports_autonomous_execution: raise ValueError("autonomous_execution feature and support flag must agree") - if self.supports_autonomous_execution and not self._has_complete_autonomous_configuration(): + if self.supports_autonomous_execution: + self._validate_enabled_autonomous_configuration() + elif self._has_any_autonomous_configuration(): + raise ValueError("autonomous execution limits require autonomous execution support") + + def _validate_enabled_autonomous_configuration(self) -> None: + if not self._has_complete_autonomous_configuration(): raise ValueError( "autonomous execution requires selection strategies, exact action, observation, and policy-profile " "support, and finite limits" ) - if ( - self.supports_autonomous_execution - and { - "participant-autonomous-execution/v2", - "participant-autonomous-execution/v3", - }.intersection(self.supported_autonomous_policy_profiles) - and ( - not self.supported_autonomous_activity_features or not self.supported_autonomous_random_stream_profiles - ) - ): + self._validate_activity_profile_configuration() + + def _validate_activity_profile_configuration(self) -> None: + activity_profiles = { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + }.intersection(self.supported_autonomous_policy_profiles) + missing_activity_support = ( + not self.supported_autonomous_activity_features or not self.supported_autonomous_random_stream_profiles + ) + if activity_profiles and missing_activity_support: raise ValueError( "autonomous execution v2 requires exact activity-feature and random-stream-profile support" ) if ( - self.supports_autonomous_execution - and "participant-autonomous-execution/v3" in self.supported_autonomous_policy_profiles + "participant-autonomous-execution/v3" in self.supported_autonomous_policy_profiles and self.resource_budgets is None ): raise ValueError("autonomous execution v3 requires participant resource-budget capabilities") - if not self.supports_autonomous_execution and self._has_any_autonomous_configuration(): - raise ValueError("autonomous execution limits require autonomous execution support") def _has_complete_autonomous_configuration(self) -> bool: return bool( @@ -259,22 +263,25 @@ def _has_complete_autonomous_configuration(self) -> bool: ) def _has_any_autonomous_configuration(self) -> bool: - return bool( - self.supported_autonomous_selection_strategies - or self.supported_autonomous_action_contracts - or self.supported_autonomous_observation_boundaries - or self.supported_autonomous_target_addresses - or self.supported_autonomous_policy_profiles - or self.supported_autonomous_activity_features - or self.supported_autonomous_random_stream_profiles - or any(value is not None for value in self._autonomous_limits()) - or self.execution_bindings - or self.supports_execution_control - or self.supported_execution_control_actions - or self.supports_bounded_concurrency - or self.max_execution_services is not None - or self.max_concurrent_actions is not None - or self.resource_budgets is not None + limits_configured = any(value is not None for value in self._autonomous_limits()) + return any( + ( + self.supported_autonomous_selection_strategies, + self.supported_autonomous_action_contracts, + self.supported_autonomous_observation_boundaries, + self.supported_autonomous_target_addresses, + self.supported_autonomous_policy_profiles, + self.supported_autonomous_activity_features, + self.supported_autonomous_random_stream_profiles, + limits_configured, + self.execution_bindings, + self.supports_execution_control, + self.supported_execution_control_actions, + self.supports_bounded_concurrency, + self.max_execution_services is not None, + self.max_concurrent_actions is not None, + self.resource_budgets is not None, + ) ) def _validate_execution_control(self) -> None: diff --git a/implementations/python/packages/raes_contracts/contracts/participant_runtime.py b/implementations/python/packages/raes_contracts/contracts/participant_runtime.py index 5fdf334bc..244c0f9c8 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_runtime.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_runtime.py @@ -39,6 +39,8 @@ from .participant_resource_budgets import ParticipantResourceMeasurementModel from .random_stream import ParticipantStreamAddressModel +_AUTONOMOUS_EXECUTION_V1 = "participant-autonomous-execution/v1" + class ParticipantEpisodeStateModel(ContractModel): state_schema_version: Literal[PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION] = PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION @@ -287,7 +289,7 @@ class ParticipantAutonomousExecutionStateModel(ContractModel): "participant-autonomous-execution/v1", "participant-autonomous-execution/v2", "participant-autonomous-execution/v3", - ] = "participant-autonomous-execution/v1" + ] = _AUTONOMOUS_EXECUTION_V1 occurrence_ordinal: StrictInt = Field(default=0, ge=0) current_retry: StrictInt = Field(default=0, ge=0) burst_position: StrictInt = Field(default=0, ge=0) @@ -306,7 +308,7 @@ def _serialize_profile_state( handler: SerializerFunctionWrapHandler, ) -> dict[str, Any]: payload = handler(self) - if self.profile == "participant-autonomous-execution/v1": + if self.profile == _AUTONOMOUS_EXECUTION_V1: for field_name in ( "profile", "occurrence_ordinal", @@ -328,7 +330,7 @@ def _serialize_profile_state( def _validate_counters(self) -> ParticipantAutonomousExecutionStateModel: if self.succeeded_actions + self.failed_actions > self.attempted_actions: raise ValueError("terminal autonomous action counts cannot exceed attempted actions") - if self.profile == "participant-autonomous-execution/v1": + if self.profile == _AUTONOMOUS_EXECUTION_V1: if any((self.random_control_id, self.random_profile_id, self.random_namespace)): raise ValueError("v1 autonomous execution state cannot carry participant random-control identity") elif not all((self.random_control_id, self.random_profile_id, self.random_namespace)): diff --git a/implementations/python/packages/raes_contracts/runtime_state.py b/implementations/python/packages/raes_contracts/runtime_state.py index 93d635c02..bb67c3855 100644 --- a/implementations/python/packages/raes_contracts/runtime_state.py +++ b/implementations/python/packages/raes_contracts/runtime_state.py @@ -125,107 +125,131 @@ def with_entries( **updates: object, ) -> RuntimeSnapshot: _validate_snapshot_update_keys(updates) - return RuntimeSnapshot( - entries=entries, - orchestration_results=_mapping_update( - updates, - "orchestration_results", - self.orchestration_results, - ), - orchestration_history=_history_update( - updates, - "orchestration_history", - self.orchestration_history, - ), - evaluation_results=_mapping_update(updates, "evaluation_results", self.evaluation_results), - evaluation_history=_history_update(updates, "evaluation_history", self.evaluation_history), - proposition_truth_results=_mapping_update( - updates, - "proposition_truth_results", - self.proposition_truth_results, - ), - participant_episode_results=_mapping_update( - updates, - "participant_episode_results", - self.participant_episode_results, - ), - participant_episode_history=_history_update( - updates, - "participant_episode_history", - self.participant_episode_history, - ), - participant_behavior_history=_history_update( - updates, - "participant_behavior_history", - self.participant_behavior_history, - ), - participant_control_history=_history_update( - updates, - "participant_control_history", - self.participant_control_history, - ), - participant_autonomous_execution_states=_mapping_update( - updates, - "participant_autonomous_execution_states", - self.participant_autonomous_execution_states, - ), - participant_execution_services=_mapping_update( - updates, - "participant_execution_services", - self.participant_execution_services, - ), - participant_resource_budget_states=_mapping_update( - updates, - "participant_resource_budget_states", - self.participant_resource_budget_states, - ), - participant_resource_pool_states=_mapping_update( - updates, - "participant_resource_pool_states", - self.participant_resource_pool_states, - ), - participant_resource_budget_events=_mapping_update( - updates, - "participant_resource_budget_events", - self.participant_resource_budget_events, - ), - shared_state_records=_mapping_update( - updates, - "shared_state_records", - self.shared_state_records, - ), - shared_state_history=_history_update( - updates, - "shared_state_history", - self.shared_state_history, - ), - joint_action_records=_mapping_update( - updates, - "joint_action_records", - self.joint_action_records, - ), - time_management_contexts=_mapping_update( - updates, - "time_management_contexts", - self.time_management_contexts, - ), - time_model_state=_time_model_state_update( - updates, - "time_model_state", - self.time_model_state, - ), - realization_provenance=_provenance_update( - updates, - "realization_provenance", - self.realization_provenance, - ), - realization_envelope=_identity_update( - updates, - "realization_envelope", - self.realization_envelope, - ), - metadata=_mapping_update(updates, "metadata", self.metadata), - ) + return RuntimeSnapshot(entries=entries, **_snapshot_updates(self, updates)) + + +def _snapshot_result_updates( + snapshot: RuntimeSnapshot, + updates: Mapping[str, object], +) -> dict[str, Any]: + return { + "orchestration_results": _mapping_update( + updates, + "orchestration_results", + snapshot.orchestration_results, + ), + "orchestration_history": _history_update( + updates, + "orchestration_history", + snapshot.orchestration_history, + ), + "evaluation_results": _mapping_update(updates, "evaluation_results", snapshot.evaluation_results), + "evaluation_history": _history_update(updates, "evaluation_history", snapshot.evaluation_history), + "proposition_truth_results": _mapping_update( + updates, + "proposition_truth_results", + snapshot.proposition_truth_results, + ), + "participant_episode_results": _mapping_update( + updates, + "participant_episode_results", + snapshot.participant_episode_results, + ), + "participant_episode_history": _history_update( + updates, + "participant_episode_history", + snapshot.participant_episode_history, + ), + "participant_behavior_history": _history_update( + updates, + "participant_behavior_history", + snapshot.participant_behavior_history, + ), + "participant_control_history": _history_update( + updates, + "participant_control_history", + snapshot.participant_control_history, + ), + } + + +def _snapshot_participant_updates( + snapshot: RuntimeSnapshot, + updates: Mapping[str, object], +) -> dict[str, Any]: + return { + "participant_autonomous_execution_states": _mapping_update( + updates, + "participant_autonomous_execution_states", + snapshot.participant_autonomous_execution_states, + ), + "participant_execution_services": _mapping_update( + updates, + "participant_execution_services", + snapshot.participant_execution_services, + ), + "participant_resource_budget_states": _mapping_update( + updates, + "participant_resource_budget_states", + snapshot.participant_resource_budget_states, + ), + "participant_resource_pool_states": _mapping_update( + updates, + "participant_resource_pool_states", + snapshot.participant_resource_pool_states, + ), + "participant_resource_budget_events": _mapping_update( + updates, + "participant_resource_budget_events", + snapshot.participant_resource_budget_events, + ), + "shared_state_records": _mapping_update( + updates, + "shared_state_records", + snapshot.shared_state_records, + ), + "shared_state_history": _history_update( + updates, + "shared_state_history", + snapshot.shared_state_history, + ), + "joint_action_records": _mapping_update( + updates, + "joint_action_records", + snapshot.joint_action_records, + ), + "time_management_contexts": _mapping_update( + updates, + "time_management_contexts", + snapshot.time_management_contexts, + ), + } + + +def _snapshot_updates( + snapshot: RuntimeSnapshot, + updates: Mapping[str, object], +) -> dict[str, Any]: + return { + **_snapshot_result_updates(snapshot, updates), + **_snapshot_participant_updates(snapshot, updates), + "time_model_state": _time_model_state_update( + updates, + "time_model_state", + snapshot.time_model_state, + ), + "realization_provenance": _provenance_update( + updates, + "realization_provenance", + snapshot.realization_provenance, + ), + "realization_envelope": _identity_update( + updates, + "realization_envelope", + snapshot.realization_envelope, + ), + "metadata": _mapping_update(updates, "metadata", snapshot.metadata), + } _SNAPSHOT_UPDATE_KEYS = { diff --git a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py index 4a498ec6c..11a7991f7 100644 --- a/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py +++ b/implementations/python/packages/raes_processor/compiler/participant_autonomous_execution.py @@ -1,5 +1,7 @@ """Compilation of autonomous participant execution policies.""" +from typing import Any + from raes.scenario import InstantiatedScenario from ..models import ( @@ -159,35 +161,13 @@ def _compiled_resource_budget( ) -def _compile_autonomous_execution( - *, +def _compiled_execution_bindings( scenario: InstantiatedScenario, - spec_name: str, - participant_addresses: tuple[str, ...], - behavior_spec: object, -) -> ParticipantAutonomousExecutionRuntime | None: - policy = behavior_spec.autonomous_execution - if policy is None: - return None - address = _address("participant", "autonomous-execution", spec_name) - authority = policy.evaluation_authority - profile = getattr(policy, "profile", "participant-autonomous-execution/v1") - activity_candidates = getattr(policy, "action_candidates", None) - ordered_candidates = sorted(activity_candidates.items()) if activity_candidates is not None else [] - action_refs = ( - [candidate.action_ref for _, candidate in ordered_candidates] - if ordered_candidates - else list(policy.action_order) - ) - work_window_refs = list(getattr(policy, "work_window_refs", ())) - pause_window_refs = list(getattr(policy, "pause_window_refs", ())) - temporal_constraint_refs = ( - [*work_window_refs, *pause_window_refs] - if profile in {"participant-autonomous-execution/v2", "participant-autonomous-execution/v3"} - else list(policy.temporal_constraint_refs) - ) + policy: object, + action_refs: list[str], +) -> tuple[tuple[ParticipantExecutionBindingRuntime, ...], tuple[str, ...]]: addressable_ref_index = _runtime_addressable_ref_index(scenario) - execution_bindings_by_key: dict[tuple[str, tuple[str, ...]], ParticipantExecutionBindingRuntime] = {} + bindings_by_key: dict[tuple[str, tuple[str, ...]], ParticipantExecutionBindingRuntime] = {} for action_ref in action_refs: action_name = _section_ref_name( action_ref, @@ -204,7 +184,7 @@ def _compile_autonomous_execution( list(dict.fromkeys(target_refs)), addressable_ref_index=addressable_ref_index, ) - execution_bindings_by_key.setdefault( + bindings_by_key.setdefault( (action_contract_address, target_addresses), ParticipantExecutionBindingRuntime( action_contract_address=action_contract_address, @@ -214,10 +194,104 @@ def _compile_autonomous_execution( max_in_flight=policy.max_in_flight, ), ) - execution_bindings = tuple(execution_bindings_by_key.values()) - target_addresses = tuple( - dict.fromkeys(target for binding in execution_bindings for target in binding.target_addresses) + bindings = tuple(bindings_by_key.values()) + targets = tuple(dict.fromkeys(target for binding in bindings for target in binding.target_addresses)) + return bindings, targets + + +def _temporal_constraint_addresses( + scenario: InstantiatedScenario, + refs: list[str], +) -> tuple[str, ...]: + return tuple( + _address( + "time", + "constraint", + _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), + ) + for ref in refs ) + + +def _activity_runtime_fields( + scenario: InstantiatedScenario, + policy: object, + *, + profile: str, + work_window_refs: list[str], + pause_window_refs: list[str], + ordered_candidates: list[tuple[str, Any]], +) -> dict[str, object]: + return { + "profile": profile, + "work_window_addresses": _temporal_constraint_addresses(scenario, work_window_refs), + "pause_window_addresses": _temporal_constraint_addresses(scenario, pause_window_refs), + "stochastic_control_ref": str(getattr(policy, "stochastic_control_ref", "")), + "timing_minimum_ticks": int(getattr(getattr(policy, "timing", None), "minimum_ticks", 0)), + "timing_maximum_ticks": int(getattr(getattr(policy, "timing", None), "maximum_ticks", 0)), + "outside_window_disposition": str(getattr(policy, "outside_window_disposition", "")), + "empty_eligible_disposition": str(getattr(policy, "empty_eligible_disposition", "")), + "action_candidate_ids": tuple(str(candidate_id) for candidate_id, _ in ordered_candidates), + "action_candidate_weights": tuple(candidate.weight for _, candidate in ordered_candidates), + "action_candidate_dependencies": tuple( + tuple(str(ref) for ref in candidate.depends_on) for _, candidate in ordered_candidates + ), + "action_candidate_retry_failure_classes": tuple( + tuple(value.value for value in candidate.retryable_failure_classes) for _, candidate in ordered_candidates + ), + "action_candidate_max_retries": tuple(candidate.max_retries for _, candidate in ordered_candidates), + "action_candidate_cooldown_ticks": tuple(candidate.cooldown_ticks for _, candidate in ordered_candidates), + "max_occurrences": int(getattr(policy, "max_occurrences", 0)), + "max_burst_size": int(getattr(policy, "max_burst_size", 1)), + } + + +def _runtime_refresh_dependencies( + scenario: InstantiatedScenario, + participant_addresses: tuple[str, ...], + action_refs: list[str], + objective_refs: tuple[str, ...], + target_addresses: tuple[str, ...], +) -> tuple[str, ...]: + return ( + *participant_addresses, + *tuple( + _action_contract_address(_section_ref_name(ref, "action_contracts", scenario.action_contracts)) + for ref in action_refs + ), + *tuple(_objective_address(_section_ref_name(ref, "objectives", scenario.objectives)) for ref in objective_refs), + *target_addresses, + ) + + +def _compile_autonomous_execution( + *, + scenario: InstantiatedScenario, + spec_name: str, + participant_addresses: tuple[str, ...], + behavior_spec: object, +) -> ParticipantAutonomousExecutionRuntime | None: + policy = behavior_spec.autonomous_execution + if policy is None: + return None + address = _address("participant", "autonomous-execution", spec_name) + authority = policy.evaluation_authority + profile = getattr(policy, "profile", "participant-autonomous-execution/v1") + activity_candidates = getattr(policy, "action_candidates", None) + ordered_candidates = sorted(activity_candidates.items()) if activity_candidates is not None else [] + action_refs = ( + [candidate.action_ref for _, candidate in ordered_candidates] + if ordered_candidates + else list(policy.action_order) + ) + work_window_refs = list(getattr(policy, "work_window_refs", ())) + pause_window_refs = list(getattr(policy, "pause_window_refs", ())) + temporal_constraint_refs = ( + [*work_window_refs, *pause_window_refs] + if profile in {"participant-autonomous-execution/v2", "participant-autonomous-execution/v3"} + else list(policy.temporal_constraint_refs) + ) + execution_bindings, target_addresses = _compiled_execution_bindings(scenario, policy, action_refs) resource_owners, resource_demands, resource_fairness = _compiled_resource_budget( scenario, policy, @@ -239,14 +313,7 @@ def _compile_autonomous_execution( scenario.time_progression_policies, ), ), - temporal_constraint_addresses=tuple( - _address( - "time", - "constraint", - _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), - ) - for ref in temporal_constraint_refs - ), + temporal_constraint_addresses=_temporal_constraint_addresses(scenario, temporal_constraint_refs), action_contract_addresses=tuple( _action_contract_address(_section_ref_name(ref, "action_contracts", scenario.action_contracts)) for ref in action_refs @@ -272,54 +339,23 @@ def _compile_autonomous_execution( proof_producer_refs=tuple(authority.proof_producer_refs), score_authority_refs=tuple(authority.score_authority_refs), receipt_authority_refs=tuple(authority.receipt_authority_refs), - profile=profile, - work_window_addresses=tuple( - _address( - "time", - "constraint", - _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), - ) - for ref in work_window_refs + **_activity_runtime_fields( + scenario, + policy, + profile=profile, + work_window_refs=work_window_refs, + pause_window_refs=pause_window_refs, + ordered_candidates=ordered_candidates, ), - pause_window_addresses=tuple( - _address( - "time", - "constraint", - _section_ref_name(ref, "temporal_constraints", scenario.temporal_constraints), - ) - for ref in pause_window_refs - ), - stochastic_control_ref=str(getattr(policy, "stochastic_control_ref", "")), - timing_minimum_ticks=int(getattr(getattr(policy, "timing", None), "minimum_ticks", 0)), - timing_maximum_ticks=int(getattr(getattr(policy, "timing", None), "maximum_ticks", 0)), - outside_window_disposition=str(getattr(policy, "outside_window_disposition", "")), - empty_eligible_disposition=str(getattr(policy, "empty_eligible_disposition", "")), - action_candidate_ids=tuple(str(candidate_id) for candidate_id, _ in ordered_candidates), - action_candidate_weights=tuple(candidate.weight for _, candidate in ordered_candidates), - action_candidate_dependencies=tuple( - tuple(str(ref) for ref in candidate.depends_on) for _, candidate in ordered_candidates - ), - action_candidate_retry_failure_classes=tuple( - tuple(value.value for value in candidate.retryable_failure_classes) for _, candidate in ordered_candidates - ), - action_candidate_max_retries=tuple(candidate.max_retries for _, candidate in ordered_candidates), - action_candidate_cooldown_ticks=tuple(candidate.cooldown_ticks for _, candidate in ordered_candidates), - max_occurrences=int(getattr(policy, "max_occurrences", 0)), - max_burst_size=int(getattr(policy, "max_burst_size", 1)), resource_owners=resource_owners, resource_demands=resource_demands, resource_fairness=resource_fairness, - refresh_dependencies=( - *participant_addresses, - *tuple( - _action_contract_address(_section_ref_name(ref, "action_contracts", scenario.action_contracts)) - for ref in action_refs - ), - *tuple( - _objective_address(_section_ref_name(ref, "objectives", scenario.objectives)) - for ref in authority.objective_refs - ), - *target_addresses, + refresh_dependencies=_runtime_refresh_dependencies( + scenario, + participant_addresses, + action_refs, + tuple(authority.objective_refs), + target_addresses, ), spec=_dump(policy), ) diff --git a/implementations/python/packages/raes_reference_backend/manifest.py b/implementations/python/packages/raes_reference_backend/manifest.py index 635d818d6..9c83a3617 100644 --- a/implementations/python/packages/raes_reference_backend/manifest.py +++ b/implementations/python/packages/raes_reference_backend/manifest.py @@ -164,6 +164,65 @@ def _time_capabilities(*, enabled: bool) -> TimeCapabilities | None: ) +def _participant_runtime_capabilities() -> ParticipantRuntimeCapabilities: + return ParticipantRuntimeCapabilities( + name="reference-emulation-participant-runtime", + supported_participant_roles=_PARTICIPANT_ROLES, + supported_behavior_features=_PARTICIPANT_BEHAVIOR_FEATURES, + supported_interaction_features=_PARTICIPANT_INTERACTION_FEATURES, + feature_support=tuple( + 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) + ), + ) + + +def _observation_capabilities() -> ObservationCapabilities: + return ObservationCapabilities( + name="reference-emulation-observation", + supported_capture_kinds=frozenset({"artifact", "log", "observation", "telemetry", "trace"}), + supported_channel_kinds=frozenset( + { + "backend-log", + "evaluation-history", + "file-artifact", + "participant-observation", + "runtime-snapshot", + "workflow-history", + } + ), + supported_evidence_contracts=frozenset( + { + "experiment-capture-spec-v1", + "experiment-evidence-record-v1", + "experiment-derived-measure-v1", + "experiment-run-v1", + } + ), + supported_media_types=frozenset({"application/json", "text/plain"}), + supported_sealing_modes=frozenset({"digest", "immutable-store"}), + supports_redaction=True, + supports_loss_disclosure=True, + supports_chain_of_custody=False, + ) + + +def _cleanup_capabilities() -> CleanupCapabilities: + return CleanupCapabilities( + name="reference-emulation-cleanup", + supported_contract_versions=CLEANUP_CAPABILITY_REQUIRED_CONTRACTS, + supported_action_kinds=frozenset({"destroy", "reset", "restore", "compensate", "verify"}), + supported_verification_methods=frozenset({"probe", "receipt"}), + supports_reusable_state=True, + supports_residual_state_disclosure=True, + ) + + def _capabilities(*, with_time: bool) -> BackendCapabilitySet: return BackendCapabilitySet( provisioner=ProvisionerCapabilities( @@ -215,56 +274,9 @@ def _capabilities(*, with_time: bool) -> BackendCapabilitySet: supported_time_domains=frozenset({"scenario_time"}), preserves_binding_provenance=True, ), - participant_runtime=ParticipantRuntimeCapabilities( - name="reference-emulation-participant-runtime", - supported_participant_roles=_PARTICIPANT_ROLES, - supported_behavior_features=_PARTICIPANT_BEHAVIOR_FEATURES, - supported_interaction_features=_PARTICIPANT_INTERACTION_FEATURES, - feature_support=tuple( - 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) - ), - ), - observation=ObservationCapabilities( - name="reference-emulation-observation", - supported_capture_kinds=frozenset({"artifact", "log", "observation", "telemetry", "trace"}), - supported_channel_kinds=frozenset( - { - "backend-log", - "evaluation-history", - "file-artifact", - "participant-observation", - "runtime-snapshot", - "workflow-history", - } - ), - supported_evidence_contracts=frozenset( - { - "experiment-capture-spec-v1", - "experiment-evidence-record-v1", - "experiment-derived-measure-v1", - "experiment-run-v1", - } - ), - supported_media_types=frozenset({"application/json", "text/plain"}), - supported_sealing_modes=frozenset({"digest", "immutable-store"}), - supports_redaction=True, - supports_loss_disclosure=True, - supports_chain_of_custody=False, - ), - cleanup=CleanupCapabilities( - name="reference-emulation-cleanup", - supported_contract_versions=CLEANUP_CAPABILITY_REQUIRED_CONTRACTS, - supported_action_kinds=frozenset({"destroy", "reset", "restore", "compensate", "verify"}), - supported_verification_methods=frozenset({"probe", "receipt"}), - supports_reusable_state=True, - supports_residual_state_disclosure=True, - ), + participant_runtime=_participant_runtime_capabilities(), + observation=_observation_capabilities(), + cleanup=_cleanup_capabilities(), time=_time_capabilities(enabled=with_time), ) diff --git a/implementations/python/packages/raes_runtime/participant_activity.py b/implementations/python/packages/raes_runtime/participant_activity.py index c34c46ec4..aeef0ab49 100644 --- a/implementations/python/packages/raes_runtime/participant_activity.py +++ b/implementations/python/packages/raes_runtime/participant_activity.py @@ -39,6 +39,17 @@ class ParticipantActivityTimingSelection: disposition: str +@dataclass(frozen=True) +class ParticipantActivityDrawContext: + """Stable address inputs shared by occurrence-local random draws.""" + + policy: ParticipantAutonomousExecutionRuntime + participant_address: str + time_segment: int + occurrence_ordinal: int + control: ParticipantActivityRandomControl + + def resolve_participant_activity_controls( controls: Iterable[ExperimentStochasticControlModel], ) -> dict[str, ParticipantActivityRandomControl]: @@ -103,12 +114,8 @@ def activity_draw_address( def draw_activity_integer( + context: ParticipantActivityDrawContext, *, - policy: ParticipantAutonomousExecutionRuntime, - participant_address: str, - time_segment: int, - occurrence_ordinal: int, - control: ParticipantActivityRandomControl, local_coordinate: int, minimum: int, maximum: int, @@ -116,14 +123,14 @@ def draw_activity_integer( """Draw one bounded value from a stable occurrence-local coordinate.""" draw = draw_bounded_integer( - profile_id=control.profile_id, - stream_key=control.stream_key, + profile_id=context.control.profile_id, + stream_key=context.control.stream_key, address=activity_draw_address( - policy=policy, - participant_address=participant_address, - time_segment=time_segment, - occurrence_ordinal=occurrence_ordinal, - control=control, + policy=context.policy, + participant_address=context.participant_address, + time_segment=context.time_segment, + occurrence_ordinal=context.occurrence_ordinal, + control=context.control, local_coordinate=local_coordinate, ), minimum=minimum, @@ -187,14 +194,16 @@ def next_activity_timing( item for item in time_model.progression_policies if item.address == policy.progression_policy_address ) step_ticks = progression.step_ticks if progression.advancement_mode == "stepped" else None - minimum = policy.timing_minimum_ticks // step_ticks if step_ticks is not None else policy.timing_minimum_ticks - maximum = policy.timing_maximum_ticks // step_ticks if step_ticks is not None else policy.timing_maximum_ticks + minimum = _timing_units(policy.timing_minimum_ticks, step_ticks) + maximum = _timing_units(policy.timing_maximum_ticks, step_ticks) interval_units = draw_activity_integer( - policy=policy, - participant_address=participant_address, - time_segment=time_segment, - occurrence_ordinal=occurrence_ordinal, - control=control, + ParticipantActivityDrawContext( + policy=policy, + participant_address=participant_address, + time_segment=time_segment, + occurrence_ordinal=occurrence_ordinal, + control=control, + ), local_coordinate=0, minimum=minimum, maximum=maximum, @@ -202,18 +211,40 @@ def next_activity_timing( interval = interval_units * step_ticks if step_ticks is not None else interval_units candidate = current_tick + interval if activity_tick_is_eligible(policy, time_model, candidate): - return ParticipantActivityTimingSelection(tick=candidate, disposition="drawn") - if policy.outside_window_disposition == "skip": - return ParticipantActivityTimingSelection(tick=None, disposition="drawn") + selection = ParticipantActivityTimingSelection(tick=candidate, disposition="drawn") + elif policy.outside_window_disposition == "skip": + selection = ParticipantActivityTimingSelection(tick=None, disposition="drawn") + else: + selection = ParticipantActivityTimingSelection( + tick=_next_activity_opening(policy, time_model, candidate, step_ticks), + disposition="next_opening", + ) + return selection + + +def _timing_units(ticks: int, step_ticks: int | None) -> int: + return ticks // step_ticks if step_ticks is not None else ticks + + +def _aligned_activity_tick(tick: int, step_ticks: int | None) -> int: + if step_ticks is not None and tick % step_ticks: + return tick + step_ticks - tick % step_ticks + return tick + + +def _next_activity_opening( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + candidate: int, + step_ticks: int | None, +) -> int | None: work = _window_ranges(policy.work_window_addresses, time_model) for start, end in work: first_tick = start[0] + int(start[1] > 0) - normalized = max(candidate, first_tick) - if step_ticks is not None and normalized % step_ticks: - normalized += step_ticks - normalized % step_ticks + normalized = _aligned_activity_tick(max(candidate, first_tick), step_ticks) while (normalized, 0) < end: if activity_tick_is_eligible(policy, time_model, normalized): - return ParticipantActivityTimingSelection(tick=normalized, disposition="next_opening") + return normalized pause_end = max( ( pause_end[0] + int(pause_end[1] > 0) @@ -222,10 +253,8 @@ def next_activity_timing( ), default=normalized + 1, ) - normalized = pause_end - if step_ticks is not None and normalized % step_ticks: - normalized += step_ticks - normalized % step_ticks - return ParticipantActivityTimingSelection(tick=None, disposition="next_opening") + normalized = _aligned_activity_tick(pause_end, step_ticks) + return None def next_activity_tick( @@ -266,11 +295,13 @@ def select_activity_candidate( return None total = sum(policy.action_candidate_weights[index] for index in eligible_indices) selected = draw_activity_integer( - policy=policy, - participant_address=participant_address, - time_segment=time_segment, - occurrence_ordinal=occurrence_ordinal, - control=control, + ParticipantActivityDrawContext( + policy=policy, + participant_address=participant_address, + time_segment=time_segment, + occurrence_ordinal=occurrence_ordinal, + control=control, + ), local_coordinate=1, minimum=0, maximum=total - 1, @@ -284,6 +315,7 @@ def select_activity_candidate( __all__ = [ + "ParticipantActivityDrawContext", "ParticipantActivityRandomControl", "activity_draw_address", "activity_control_for", diff --git a/implementations/python/packages/raes_runtime/participant_activity_support.py b/implementations/python/packages/raes_runtime/participant_activity_support.py index 66be116da..b7355b89c 100644 --- a/implementations/python/packages/raes_runtime/participant_activity_support.py +++ b/implementations/python/packages/raes_runtime/participant_activity_support.py @@ -95,9 +95,11 @@ def _activity_provenance( occurrence_ordinal=state.occurrence_ordinal, retry_ordinal=state.current_retry - 1, ) - disposition = ( - "retry" if state.current_retry else ("burst" if state.burst_position else state.next_timing_disposition) - ) + disposition = state.next_timing_disposition + if state.burst_position: + disposition = "burst" + if state.current_retry: + disposition = "retry" return ParticipantActivityOccurrenceProvenanceModel( policy_address=context.policy.address, policy_profile=context.policy.profile, diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index 096c8cd62..eab1b58d2 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -16,6 +16,7 @@ from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime from .participant_activity import ( + ParticipantActivityDrawContext, ParticipantActivityRandomControl, activity_control_for, draw_activity_integer, @@ -29,6 +30,7 @@ from .participant_scheduler_lifecycle import reset_policy_at_clock from .participant_scheduler_operations import ( SchedulerRunState, + participant_due_context, run_participant_due, run_policy_due_concurrently, ) @@ -68,91 +70,105 @@ def _state_identity(state: ParticipantAutonomousExecutionStateModel) -> tuple[ob ) -def _initialize_participant( - policy: ParticipantAutonomousExecutionRuntime, - time_model: CompiledTimeModel, +def _ensure_participant_episode( participant_runtime: object, snapshot: RuntimeSnapshot, participant_address: str, - activity_controls: dict[str, ParticipantActivityRandomControl], ) -> ApplyResult: - working = snapshot - changed: list[str] = [] - if participant_address not in working.participant_episode_results: - result = participant_runtime.initialize( - ParticipantEpisodeInitializeRequest( - participant_address=participant_address, - episode_id=f"{participant_address}-autonomous-0", - ), - working, - ) - if not result.success: - return result - working = result.snapshot - changed.extend(result.changed_addresses) - key = _state_key(policy.address, participant_address) - segment, _ = clock_coordinate(working, policy.clock_address) - activity_control = activity_control_for(policy, activity_controls) - if ( - policy.profile - in { - "participant-autonomous-execution/v2", - _RESOURCE_GOVERNED_PROFILE, - } - and activity_control is None - ): - return ApplyResult( - success=False, - snapshot=working, - diagnostics=[ - Diagnostic( - code="runtime.participant-activity-control-unbound", - domain="participant", - address=policy.address, - message=( - f"Participant activity policy requires admitted stochastic control " - f"{policy.stochastic_control_ref!r}." - ), - ) - ], - ) + if participant_address in snapshot.participant_episode_results: + return ApplyResult(success=True, snapshot=snapshot) + return participant_runtime.initialize( + ParticipantEpisodeInitializeRequest( + participant_address=participant_address, + episode_id=f"{participant_address}-autonomous-0", + ), + snapshot, + ) + + +def _activity_control_unbound_result( + policy: ParticipantAutonomousExecutionRuntime, + snapshot: RuntimeSnapshot, +) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code="runtime.participant-activity-control-unbound", + domain="participant", + address=policy.address, + message=( + f"Participant activity policy requires admitted stochastic control " + f"{policy.stochastic_control_ref!r}." + ), + ) + ], + ) + + +def _initial_activity_schedule( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + snapshot: RuntimeSnapshot, + participant_address: str, + segment: int, + activity_control: ParticipantActivityRandomControl | None, +) -> tuple[int | None, int, str]: if activity_control is None: first_tick, _ = _cadence(policy, time_model) - burst_size = 1 - timing_disposition = "cadence" - else: - current_tick = _clock_tick(working, policy.clock_address) - burst_size = draw_activity_integer( + return first_tick, 1, "cadence" + current_tick = _clock_tick(snapshot, policy.clock_address) + burst_size = draw_activity_integer( + ParticipantActivityDrawContext( policy=policy, participant_address=participant_address, time_segment=segment, occurrence_ordinal=0, control=activity_control, - local_coordinate=2, - minimum=1, - maximum=policy.max_burst_size, - ) - timing = next_activity_timing( - policy=policy, - time_model=time_model, - participant_address=participant_address, - time_segment=segment, - occurrence_ordinal=0, - current_tick=current_tick, - control=activity_control, - ) - first_tick = timing.tick - timing_disposition = timing.disposition - expected = ParticipantAutonomousExecutionStateModel( + ), + local_coordinate=2, + minimum=1, + maximum=policy.max_burst_size, + ) + timing = next_activity_timing( + policy=policy, + time_model=time_model, + participant_address=participant_address, + time_segment=segment, + occurrence_ordinal=0, + current_tick=current_tick, + control=activity_control, + ) + return timing.tick, burst_size, timing.disposition + + +def _initial_participant_state( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + snapshot: RuntimeSnapshot, + participant_address: str, + segment: int, + activity_control: ParticipantActivityRandomControl | None, +) -> ParticipantAutonomousExecutionStateModel: + first_tick, burst_size, timing_disposition = _initial_activity_schedule( + policy, + time_model, + snapshot, + participant_address, + segment, + activity_control, + ) + return ParticipantAutonomousExecutionStateModel( policy_address=policy.address, policy_digest=_policy_digest(policy, time_model), participant_address=participant_address, - episode_id=working.participant_episode_results[participant_address]["episode_id"], + episode_id=snapshot.participant_episode_results[participant_address]["episode_id"], participant_implementation_ref=policy.participant_implementation_ref, clock_address=policy.clock_address, time_segment=segment, lifecycle_state="running" if first_tick is not None else "completed", - next_tick=first_tick if first_tick is not None else _clock_tick(working, policy.clock_address), + next_tick=first_tick if first_tick is not None else _clock_tick(snapshot, policy.clock_address), next_action_index=0, attempted_actions=0, succeeded_actions=0, @@ -164,13 +180,23 @@ def _initialize_participant( burst_size=burst_size, next_timing_disposition=timing_disposition, ) - states = dict(working.participant_autonomous_execution_states) + + +def _persist_initial_participant_state( + policy: ParticipantAutonomousExecutionRuntime, + snapshot: RuntimeSnapshot, + participant_address: str, + expected: ParticipantAutonomousExecutionStateModel, + changed: list[str], +) -> ApplyResult: + key = _state_key(policy.address, participant_address) + states = dict(snapshot.participant_autonomous_execution_states) if key in states and _state_identity(ParticipantAutonomousExecutionStateModel.model_validate(states[key])) != ( _state_identity(expected) ): return ApplyResult( success=False, - snapshot=working, + snapshot=snapshot, diagnostics=[ Diagnostic( code="runtime.participant-autonomous-state-conflict", @@ -180,16 +206,52 @@ def _initialize_participant( ) ], ) + working = snapshot if key not in states: states[key] = expected.model_dump(mode="json") - working = working.with_entries( - dict(working.entries), + working = snapshot.with_entries( + dict(snapshot.entries), participant_autonomous_execution_states=states, ) changed.append(key) return ApplyResult(success=True, snapshot=working, changed_addresses=changed) +def _initialize_participant( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + snapshot: RuntimeSnapshot, + participant_address: str, + activity_controls: dict[str, ParticipantActivityRandomControl], +) -> ApplyResult: + episode_result = _ensure_participant_episode(participant_runtime, snapshot, participant_address) + if not episode_result.success: + return episode_result + working = episode_result.snapshot + changed = list(episode_result.changed_addresses) + segment, _ = clock_coordinate(working, policy.clock_address) + activity_control = activity_control_for(policy, activity_controls) + if ( + policy.profile + in { + "participant-autonomous-execution/v2", + _RESOURCE_GOVERNED_PROFILE, + } + and activity_control is None + ): + return _activity_control_unbound_result(policy, working) + expected = _initial_participant_state( + policy, + time_model, + working, + participant_address, + segment, + activity_control, + ) + return _persist_initial_participant_state(policy, working, participant_address, expected, changed) + + def _missing_execution_service_result( policy: ParticipantAutonomousExecutionRuntime, run: SchedulerRunState, @@ -223,14 +285,16 @@ def _run_serial_due( ) -> None: for participant_address in policy.participant_addresses: run_participant_due( - policy, - time_model, - participant_runtime, - participant_address, - current_tick, - cadence_ticks, + participant_due_context( + policy, + time_model, + participant_runtime, + participant_address, + current_tick, + cadence_ticks, + activity_controls, + ), run, - activity_controls, ) if run.failure is not None: break diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py index 85c371626..901cac398 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_concurrency.py @@ -301,7 +301,7 @@ def _finish_due_policy( cadence_ticks: int, run: SchedulerRunState, ) -> None: - from .participant_scheduler_operations import run_participant_due + from .participant_scheduler_operations import participant_due_context, run_participant_due if run.failure is not None: return @@ -309,12 +309,14 @@ def _finish_due_policy( return for participant_address in policy.participant_addresses: run_participant_due( - policy, - time_model, - participant_runtime, - participant_address, - current_tick, - cadence_ticks, + participant_due_context( + policy, + time_model, + participant_runtime, + participant_address, + current_tick, + cadence_ticks, + ), run, ) if run.failure is not None: diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py index 801fa2f21..f6eaee236 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import replace +from dataclasses import dataclass, replace from typing import cast from raes_contracts.contracts import ( @@ -17,6 +17,7 @@ from .participant_action_validation import autonomous_action_result_violation from .participant_activity import ( + ParticipantActivityDrawContext, ParticipantActivityRandomControl, activity_control_for, draw_activity_integer, @@ -222,42 +223,45 @@ def _run_one_due_action( return next_state -def _next_activity_occurrence_state( +def _activity_attempt_is_retryable( context: _DueActionContext, state: ParticipantAutonomousExecutionStateModel, - request: ParticipantActionAdmissionRequest, *, action_succeeded: bool, failure_class: str | None, protocol_failure: bool, -) -> ParticipantAutonomousExecutionStateModel: - policy = context.policy - control = context.activity_control - if control is None: - raise ValueError("participant activity execution requires a random control") + attempted: int, +) -> bool: index = state.next_action_index - candidate_id = policy.action_candidate_ids[index] - attempted = state.attempted_actions + 1 - failed = state.failed_actions + (0 if action_succeeded else 1) - retryable = ( + return ( not protocol_failure and not action_succeeded - and failure_class in policy.action_candidate_retry_failure_classes[index] - and state.current_retry < policy.action_candidate_max_retries[index] - and attempted < policy.max_action_attempts + and failure_class in context.policy.action_candidate_retry_failure_classes[index] + and state.current_retry < context.policy.action_candidate_max_retries[index] + and attempted < context.policy.max_action_attempts ) - if retryable: - return state.model_copy( - update={ - "next_tick": context.current_tick, - "attempted_actions": attempted, - "failed_actions": failed, - "current_retry": state.current_retry + 1, - "last_candidate_id": candidate_id, - "last_action_instance_id": request.action_instance_id, - } - ) + +@dataclass(frozen=True) +class _ActivityProgress: + candidate_id: str + completed: list[str] + cooldowns: dict[str, int] + occurrence: int + lifecycle: str + + +def _completed_activity_progress( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + *, + action_succeeded: bool, + protocol_failure: bool, + attempted: int, +) -> _ActivityProgress: + policy = context.policy + index = state.next_action_index + candidate_id = policy.action_candidate_ids[index] completed = list(state.completed_candidate_ids) if action_succeeded and candidate_id not in completed: completed.append(candidate_id) @@ -269,57 +273,148 @@ def _next_activity_occurrence_state( lifecycle = "failed" elif occurrence >= policy.max_occurrences or attempted >= policy.max_action_attempts: lifecycle = "completed" + return _ActivityProgress( + candidate_id=candidate_id, + completed=completed, + cooldowns=cooldowns, + occurrence=occurrence, + lifecycle=lifecycle, + ) + + +@dataclass(frozen=True) +class _ActivitySchedule: + lifecycle: str + next_tick: int + burst_position: int + burst_size: int + timing_disposition: str + +def _next_activity_schedule( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + control: ParticipantActivityRandomControl, + progress: _ActivityProgress, +) -> _ActivitySchedule: + lifecycle = progress.lifecycle burst_position = state.burst_position burst_size = state.burst_size next_tick = context.current_tick - if lifecycle == "running": - if burst_position + 1 < burst_size: - burst_position += 1 - else: - burst_position = 0 - burst_size = draw_activity_integer( - policy=policy, - participant_address=context.participant_address, - time_segment=state.time_segment, - occurrence_ordinal=occurrence, - control=control, - local_coordinate=2, - minimum=1, - maximum=policy.max_burst_size, - ) - timing = next_activity_timing( - policy=policy, - time_model=context.time_model, + timing_disposition = state.next_timing_disposition + if lifecycle == "running" and burst_position + 1 < burst_size: + burst_position += 1 + elif lifecycle == "running": + burst_position = 0 + burst_size = draw_activity_integer( + ParticipantActivityDrawContext( + policy=context.policy, participant_address=context.participant_address, time_segment=state.time_segment, - occurrence_ordinal=occurrence, - current_tick=context.current_tick, + occurrence_ordinal=progress.occurrence, control=control, - ) - selected_tick = timing.tick - if selected_tick is None: - lifecycle = "completed" - else: - next_tick = selected_tick + ), + local_coordinate=2, + minimum=1, + maximum=context.policy.max_burst_size, + ) + timing = next_activity_timing( + policy=context.policy, + time_model=context.time_model, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=progress.occurrence, + current_tick=context.current_tick, + control=control, + ) + timing_disposition = timing.disposition + if timing.tick is None: + lifecycle = "completed" + else: + next_tick = timing.tick + return _ActivitySchedule( + lifecycle=lifecycle, + next_tick=next_tick, + burst_position=burst_position, + burst_size=burst_size, + timing_disposition=timing_disposition, + ) + + +def _activity_retry_state( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + *, + attempted: int, + failed: int, +) -> ParticipantAutonomousExecutionStateModel: + candidate_id = context.policy.action_candidate_ids[state.next_action_index] return state.model_copy( update={ - "lifecycle_state": lifecycle, - "next_tick": next_tick, + "next_tick": context.current_tick, + "attempted_actions": attempted, + "failed_actions": failed, + "current_retry": state.current_retry + 1, + "last_candidate_id": candidate_id, + "last_action_instance_id": request.action_instance_id, + } + ) + + +def _next_activity_occurrence_state( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + *, + action_succeeded: bool, + failure_class: str | None, + protocol_failure: bool, +) -> ParticipantAutonomousExecutionStateModel: + control = context.activity_control + if control is None: + raise ValueError("participant activity execution requires a random control") + attempted = state.attempted_actions + 1 + failed = state.failed_actions + (0 if action_succeeded else 1) + if _activity_attempt_is_retryable( + context, + state, + action_succeeded=action_succeeded, + failure_class=failure_class, + protocol_failure=protocol_failure, + attempted=attempted, + ): + return _activity_retry_state( + context, + state, + request, + attempted=attempted, + failed=failed, + ) + progress = _completed_activity_progress( + context, + state, + action_succeeded=action_succeeded, + protocol_failure=protocol_failure, + attempted=attempted, + ) + schedule = _next_activity_schedule(context, state, control, progress) + return state.model_copy( + update={ + "lifecycle_state": schedule.lifecycle, + "next_tick": schedule.next_tick, "attempted_actions": attempted, "succeeded_actions": state.succeeded_actions + (1 if action_succeeded else 0), "failed_actions": failed, - "occurrence_ordinal": occurrence, + "occurrence_ordinal": progress.occurrence, "current_retry": 0, - "burst_position": burst_position, - "burst_size": burst_size, - "last_candidate_id": candidate_id, - "completed_candidate_ids": completed, - "candidate_cooldown_until": cooldowns, + "burst_position": schedule.burst_position, + "burst_size": schedule.burst_size, + "last_candidate_id": progress.candidate_id, + "completed_candidate_ids": progress.completed, + "candidate_cooldown_until": progress.cooldowns, "last_action_instance_id": request.action_instance_id, - "next_timing_disposition": ( - timing.disposition if lifecycle == "running" and burst_position == 0 else state.next_timing_disposition - ), + "next_timing_disposition": schedule.timing_disposition, } ) @@ -386,82 +481,95 @@ def _run_one_activity_action( return next_state -def _run_participant_activity_due( +def _activity_action_is_due( context: _DueActionContext, state: ParticipantAutonomousExecutionStateModel, run: SchedulerRunState, -) -> None: - while ( - state.lifecycle_state == "running" - and state.next_tick == context.current_tick - and state.attempted_actions < context.policy.max_action_attempts - and run.failure is None - ): - eligible = activity_eligible_indices(context.policy, state, context.current_tick) +) -> bool: + return all( + ( + state.lifecycle_state == "running", + state.next_tick == context.current_tick, + state.attempted_actions < context.policy.max_action_attempts, + run.failure is None, + ) + ) + + +def _selected_activity_index( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, +) -> int | None: + if state.current_retry: + return state.next_action_index + control = context.activity_control + if control is None: + raise ValueError("participant activity execution requires a random control") + return select_activity_candidate( + policy=context.policy, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=state.occurrence_ordinal, + control=control, + eligible_indices=activity_eligible_indices(context.policy, state, context.current_tick), + ) + + +def _empty_activity_state( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, +) -> ParticipantAutonomousExecutionStateModel: + lifecycle = "completed" if context.policy.empty_eligible_disposition == "complete" else "running" + selected_tick = None + if lifecycle == "running": control = context.activity_control if control is None: raise ValueError("participant activity execution requires a random control") - selected = ( - state.next_action_index - if state.current_retry - else select_activity_candidate( - policy=context.policy, - participant_address=context.participant_address, - time_segment=state.time_segment, - occurrence_ordinal=state.occurrence_ordinal, - control=control, - eligible_indices=eligible, - ) - ) + selected_tick = next_activity_timing( + policy=context.policy, + time_model=context.time_model, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=state.occurrence_ordinal, + current_tick=context.current_tick, + control=control, + ).tick + if selected_tick is None: + lifecycle = "completed" + return state.model_copy( + update={ + "lifecycle_state": lifecycle, + "next_tick": selected_tick if selected_tick is not None else context.current_tick, + } + ) + + +def _run_participant_activity_due( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + run: SchedulerRunState, +) -> None: + while _activity_action_is_due(context, state, run): + selected = _selected_activity_index(context, state) if selected is None: - lifecycle = "completed" if context.policy.empty_eligible_disposition == "complete" else "running" - selected_tick = ( - None - if lifecycle == "completed" - else next_activity_timing( - policy=context.policy, - time_model=context.time_model, - participant_address=context.participant_address, - time_segment=state.time_segment, - occurrence_ordinal=state.occurrence_ordinal, - current_tick=context.current_tick, - control=control, - ).tick - ) - if selected_tick is None: - lifecycle = "completed" - state = state.model_copy( - update={ - "lifecycle_state": lifecycle, - "next_tick": selected_tick if selected_tick is not None else context.current_tick, - } - ) + state = _empty_activity_state(context, state) persist_activity_state(run, context.key, state) return state = state.model_copy(update={"next_action_index": selected}) state = _run_one_activity_action(context, state, run) -def run_participant_due( +def participant_due_context( policy: ParticipantAutonomousExecutionRuntime, time_model: CompiledTimeModel, participant_runtime: object, participant_address: str, current_tick: int, cadence_ticks: int, - run: SchedulerRunState, activity_controls: dict[str, ParticipantActivityRandomControl] | None = None, -) -> None: - """Run one participant at the current governed cadence boundary.""" - +) -> _DueActionContext: key = f"{policy.address}.state.{participant_address}" - state = ParticipantAutonomousExecutionStateModel.model_validate( - run.working.participant_autonomous_execution_states[key] - ) - if state.lifecycle_state == "running" and state.next_tick < current_tick: - run.failure = cadence_missed_result(run.working, key, current_tick, state) - return - action_context = _DueActionContext( + return _DueActionContext( policy=policy, time_model=time_model, participant_runtime=participant_runtime, @@ -471,28 +579,55 @@ def run_participant_due( cadence_ticks=cadence_ticks, activity_control=activity_control_for(policy, activity_controls or {}), ) - if policy.profile in { + + +def _legacy_action_is_due( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + run: SchedulerRunState, +) -> bool: + return all( + ( + state.lifecycle_state == "running", + state.next_tick == context.current_tick, + state.attempted_actions < context.policy.max_action_attempts, + run.failure is None, + ) + ) + + +def _run_legacy_participant_due( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + run: SchedulerRunState, +) -> None: + while _legacy_action_is_due(context, state, run): + state = _run_one_due_action(context, state, run) + + +def run_participant_due( + context: _DueActionContext, + run: SchedulerRunState, +) -> None: + """Run one participant at the current governed cadence boundary.""" + + state = ParticipantAutonomousExecutionStateModel.model_validate( + run.working.participant_autonomous_execution_states[context.key] + ) + if state.lifecycle_state == "running" and state.next_tick < context.current_tick: + run.failure = cadence_missed_result(run.working, context.key, context.current_tick, state) + elif context.policy.profile in { "participant-autonomous-execution/v2", "participant-autonomous-execution/v3", }: - _run_participant_activity_due(action_context, state, run) - return - action_is_due = ( - state.lifecycle_state == "running" - and state.next_tick == current_tick - and state.attempted_actions < policy.max_action_attempts - ) - while action_is_due and run.failure is None: - state = _run_one_due_action(action_context, state, run) - action_is_due = ( - state.lifecycle_state == "running" - and state.next_tick == current_tick - and state.attempted_actions < policy.max_action_attempts - ) + _run_participant_activity_due(context, state, run) + else: + _run_legacy_participant_due(context, state, run) __all__ = [ "SchedulerRunState", + "participant_due_context", "run_participant_due", "run_policy_due_concurrently", ] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_reset.py b/implementations/python/packages/raes_runtime/participant_scheduler_reset.py index 1ea65ebac..4d98a9dff 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_reset.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_reset.py @@ -9,7 +9,12 @@ from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime -from .participant_activity import ParticipantActivityRandomControl, draw_activity_integer, next_activity_timing +from .participant_activity import ( + ParticipantActivityDrawContext, + ParticipantActivityRandomControl, + draw_activity_integer, + next_activity_timing, +) from .participant_scheduler_time import cadence @@ -82,11 +87,13 @@ def reset_scheduler_participant( burst_size = 1 if context.activity_control is not None: burst_size = draw_activity_integer( - policy=context.policy, - participant_address=participant_address, - time_segment=context.segment, - occurrence_ordinal=0, - control=context.activity_control, + ParticipantActivityDrawContext( + policy=context.policy, + participant_address=participant_address, + time_segment=context.segment, + occurrence_ordinal=0, + control=context.activity_control, + ), local_coordinate=2, minimum=1, maximum=context.policy.max_burst_size, diff --git a/implementations/python/tests/test_dsl_437_benign_participant_execution.py b/implementations/python/tests/test_dsl_437_benign_participant_execution.py index 627056bb6..0f5d69547 100644 --- a/implementations/python/tests/test_dsl_437_benign_participant_execution.py +++ b/implementations/python/tests/test_dsl_437_benign_participant_execution.py @@ -755,25 +755,28 @@ def test_activity_policy_v2_rejects_non_window_availability_constraint() -> None payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["work_window_refs"] = [ "green-cadence" ] + rendered = yaml.safe_dump(payload, sort_keys=False) with pytest.raises(SDLValidationError, match="work and pause refs must resolve to window constraints"): - parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + parse_sdl(rendered) def test_activity_policy_v2_rejects_timing_bounds_unreachable_by_stepped_progression() -> None: payload = yaml.safe_load(_activity_policy_yaml()) payload["behavior_specifications"]["participant-behavior"]["autonomous_execution"]["timing"]["minimum_ticks"] = 15 + rendered = yaml.safe_dump(payload, sort_keys=False) with pytest.raises(SDLValidationError, match="activity timing bounds are unreachable by stepped progression"): - parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + parse_sdl(rendered) def test_activity_policy_v2_rejects_window_for_unrelated_subject() -> None: payload = yaml.safe_load(_activity_policy_yaml()) payload["temporal_constraints"]["work-window"]["subject_refs"] = ["nodes.customer-portal"] + rendered = yaml.safe_dump(payload, sort_keys=False) with pytest.raises(SDLValidationError, match="must name the behavior specification or every governed participant"): - parse_sdl(yaml.safe_dump(payload, sort_keys=False)) + parse_sdl(rendered) def test_non_evaluated_autonomous_participant_must_be_green() -> None: @@ -1488,8 +1491,9 @@ def test_runtime_manager_fails_closed_for_unresolved_governed_activity_entropy() } ) + target = create_stub_target() with pytest.raises(ValueError, match="governed entropy without a resolver"): - RuntimeManager(create_stub_target(), stochastic_controls=[governed]) + RuntimeManager(target, stochastic_controls=[governed]) def test_runtime_manager_rolls_back_clock_when_participant_reset_fails() -> None: diff --git a/implementations/python/tests/test_random_stream_profile.py b/implementations/python/tests/test_random_stream_profile.py index e3788f0be..9baf6ed9e 100644 --- a/implementations/python/tests/test_random_stream_profile.py +++ b/implementations/python/tests/test_random_stream_profile.py @@ -97,10 +97,11 @@ def test_rejects_unsupported_but_syntactically_valid_id(self) -> None: random_stream_profile_path("nonexistent-profile-v1") def test_supported_profile_ids_contains_blake3_xof(self) -> None: - assert { + expected_ids = { "blake3-xof-participant-v1", "blake3-xof-v1", - } == SUPPORTED_RANDOM_STREAM_PROFILE_IDS + } + assert not SUPPORTED_RANDOM_STREAM_PROFILE_IDS.symmetric_difference(expected_ids) def test_load_unsupported_profile_fails_closed_without_file_probe(self) -> None: with pytest.raises(ValueError, match="unsupported"): From 18c9ab54abab8c66f33d7582b6209c02496296fc Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 08:10:57 +0200 Subject: [PATCH 51/55] fix: split scheduler helper modules --- .../raes_runtime/participant_scheduler.py | 228 +---------------- .../participant_scheduler_activity_state.py | 213 ++++++++++++++++ .../participant_scheduler_initialization.py | 229 ++++++++++++++++++ .../participant_scheduler_operations.py | 203 +--------------- 4 files changed, 450 insertions(+), 423 deletions(-) create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_activity_state.py create mode 100644 implementations/python/packages/raes_runtime/participant_scheduler_initialization.py diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index eab1b58d2..0f6a68333 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -4,23 +4,13 @@ from collections.abc import Iterable -from raes_contracts.contracts import ( - ParticipantAutonomousExecutionStateModel, -) from raes_contracts.contracts.participant_execution import ParticipantExecutionServiceStateModel from raes_contracts.diagnostics import Diagnostic -from raes_contracts.participant_episode import ( - ParticipantEpisodeInitializeRequest, -) from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime from .participant_activity import ( - ParticipantActivityDrawContext, ParticipantActivityRandomControl, - activity_control_for, - draw_activity_integer, - next_activity_timing, ) from .participant_execution_scheduler_state import ( execution_service_state, @@ -28,6 +18,10 @@ ) from .participant_resource_budgets import initialize_participant_resource_budgets from .participant_scheduler_lifecycle import reset_policy_at_clock +from .participant_scheduler_initialization import ( + clock_tick as _clock_tick, + initialize_participant as _initialize_participant, +) from .participant_scheduler_operations import ( SchedulerRunState, participant_due_context, @@ -38,220 +32,6 @@ from .participant_scheduler_time import cadence as _cadence from .participant_scheduler_time import clock_coordinate -_RESOURCE_GOVERNED_PROFILE = "participant-autonomous-execution/v3" - - -def _state_key(policy_address: str, participant_address: str) -> str: - return f"{policy_address}.state.{participant_address}" - - -def _clock_tick(snapshot: RuntimeSnapshot, clock_address: str) -> int: - if snapshot.time_model_state is None: - raise ValueError("autonomous participant execution requires typed shared-time state") - clock = snapshot.time_model_state.clocks.get(clock_address) - if clock is None: - raise ValueError(f"autonomous participant clock {clock_address!r} has no runtime state") - return clock.coordinate.tick - - -def _state_identity(state: ParticipantAutonomousExecutionStateModel) -> tuple[object, ...]: - return ( - state.policy_address, - state.policy_digest, - state.participant_address, - state.episode_id, - state.participant_implementation_ref, - state.clock_address, - state.time_segment, - state.profile, - state.random_control_id, - state.random_profile_id, - state.random_namespace, - ) - - -def _ensure_participant_episode( - participant_runtime: object, - snapshot: RuntimeSnapshot, - participant_address: str, -) -> ApplyResult: - if participant_address in snapshot.participant_episode_results: - return ApplyResult(success=True, snapshot=snapshot) - return participant_runtime.initialize( - ParticipantEpisodeInitializeRequest( - participant_address=participant_address, - episode_id=f"{participant_address}-autonomous-0", - ), - snapshot, - ) - - -def _activity_control_unbound_result( - policy: ParticipantAutonomousExecutionRuntime, - snapshot: RuntimeSnapshot, -) -> ApplyResult: - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - Diagnostic( - code="runtime.participant-activity-control-unbound", - domain="participant", - address=policy.address, - message=( - f"Participant activity policy requires admitted stochastic control " - f"{policy.stochastic_control_ref!r}." - ), - ) - ], - ) - - -def _initial_activity_schedule( - policy: ParticipantAutonomousExecutionRuntime, - time_model: CompiledTimeModel, - snapshot: RuntimeSnapshot, - participant_address: str, - segment: int, - activity_control: ParticipantActivityRandomControl | None, -) -> tuple[int | None, int, str]: - if activity_control is None: - first_tick, _ = _cadence(policy, time_model) - return first_tick, 1, "cadence" - current_tick = _clock_tick(snapshot, policy.clock_address) - burst_size = draw_activity_integer( - ParticipantActivityDrawContext( - policy=policy, - participant_address=participant_address, - time_segment=segment, - occurrence_ordinal=0, - control=activity_control, - ), - local_coordinate=2, - minimum=1, - maximum=policy.max_burst_size, - ) - timing = next_activity_timing( - policy=policy, - time_model=time_model, - participant_address=participant_address, - time_segment=segment, - occurrence_ordinal=0, - current_tick=current_tick, - control=activity_control, - ) - return timing.tick, burst_size, timing.disposition - - -def _initial_participant_state( - policy: ParticipantAutonomousExecutionRuntime, - time_model: CompiledTimeModel, - snapshot: RuntimeSnapshot, - participant_address: str, - segment: int, - activity_control: ParticipantActivityRandomControl | None, -) -> ParticipantAutonomousExecutionStateModel: - first_tick, burst_size, timing_disposition = _initial_activity_schedule( - policy, - time_model, - snapshot, - participant_address, - segment, - activity_control, - ) - return ParticipantAutonomousExecutionStateModel( - policy_address=policy.address, - policy_digest=_policy_digest(policy, time_model), - participant_address=participant_address, - episode_id=snapshot.participant_episode_results[participant_address]["episode_id"], - participant_implementation_ref=policy.participant_implementation_ref, - clock_address=policy.clock_address, - time_segment=segment, - lifecycle_state="running" if first_tick is not None else "completed", - next_tick=first_tick if first_tick is not None else _clock_tick(snapshot, policy.clock_address), - next_action_index=0, - attempted_actions=0, - succeeded_actions=0, - failed_actions=0, - profile=policy.profile, - random_control_id=activity_control.control_id if activity_control is not None else None, - random_profile_id=activity_control.profile_id if activity_control is not None else None, - random_namespace=activity_control.namespace if activity_control is not None else None, - burst_size=burst_size, - next_timing_disposition=timing_disposition, - ) - - -def _persist_initial_participant_state( - policy: ParticipantAutonomousExecutionRuntime, - snapshot: RuntimeSnapshot, - participant_address: str, - expected: ParticipantAutonomousExecutionStateModel, - changed: list[str], -) -> ApplyResult: - key = _state_key(policy.address, participant_address) - states = dict(snapshot.participant_autonomous_execution_states) - if key in states and _state_identity(ParticipantAutonomousExecutionStateModel.model_validate(states[key])) != ( - _state_identity(expected) - ): - return ApplyResult( - success=False, - snapshot=snapshot, - diagnostics=[ - Diagnostic( - code="runtime.participant-autonomous-state-conflict", - domain="participant", - address=policy.address, - message="Existing autonomous participant state does not match the compiled policy.", - ) - ], - ) - working = snapshot - if key not in states: - states[key] = expected.model_dump(mode="json") - working = snapshot.with_entries( - dict(snapshot.entries), - participant_autonomous_execution_states=states, - ) - changed.append(key) - return ApplyResult(success=True, snapshot=working, changed_addresses=changed) - - -def _initialize_participant( - policy: ParticipantAutonomousExecutionRuntime, - time_model: CompiledTimeModel, - participant_runtime: object, - snapshot: RuntimeSnapshot, - participant_address: str, - activity_controls: dict[str, ParticipantActivityRandomControl], -) -> ApplyResult: - episode_result = _ensure_participant_episode(participant_runtime, snapshot, participant_address) - if not episode_result.success: - return episode_result - working = episode_result.snapshot - changed = list(episode_result.changed_addresses) - segment, _ = clock_coordinate(working, policy.clock_address) - activity_control = activity_control_for(policy, activity_controls) - if ( - policy.profile - in { - "participant-autonomous-execution/v2", - _RESOURCE_GOVERNED_PROFILE, - } - and activity_control is None - ): - return _activity_control_unbound_result(policy, working) - expected = _initial_participant_state( - policy, - time_model, - working, - participant_address, - segment, - activity_control, - ) - return _persist_initial_participant_state(policy, working, participant_address, expected, changed) - - def _missing_execution_service_result( policy: ParticipantAutonomousExecutionRuntime, run: SchedulerRunState, diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_activity_state.py b/implementations/python/packages/raes_runtime/participant_scheduler_activity_state.py new file mode 100644 index 000000000..c5002afd9 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_activity_state.py @@ -0,0 +1,213 @@ +"""State transitions for participant activity occurrences.""" + +from dataclasses import dataclass + +from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.participant_binding import ParticipantActionAdmissionRequest + +from .participant_activity import ( + ParticipantActivityDrawContext, + ParticipantActivityRandomControl, + draw_activity_integer, + next_activity_timing, +) +from .participant_scheduler_types import _DueActionContext + + +def _activity_attempt_is_retryable( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + *, + action_succeeded: bool, + failure_class: str | None, + protocol_failure: bool, + attempted: int, +) -> bool: + index = state.next_action_index + return ( + not protocol_failure + and not action_succeeded + and failure_class in context.policy.action_candidate_retry_failure_classes[index] + and state.current_retry < context.policy.action_candidate_max_retries[index] + and attempted < context.policy.max_action_attempts + ) + + +@dataclass(frozen=True) +class _ActivityProgress: + candidate_id: str + completed: list[str] + cooldowns: dict[str, int] + occurrence: int + lifecycle: str + + +def _completed_activity_progress( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + *, + action_succeeded: bool, + protocol_failure: bool, + attempted: int, +) -> _ActivityProgress: + policy = context.policy + index = state.next_action_index + candidate_id = policy.action_candidate_ids[index] + completed = list(state.completed_candidate_ids) + if action_succeeded and candidate_id not in completed: + completed.append(candidate_id) + cooldowns = dict(state.candidate_cooldown_until) + cooldowns[candidate_id] = context.current_tick + policy.action_candidate_cooldown_ticks[index] + occurrence = state.occurrence_ordinal + 1 + lifecycle = state.lifecycle_state + if protocol_failure or (not action_succeeded and policy.failure_policy == "stop"): + lifecycle = "failed" + elif occurrence >= policy.max_occurrences or attempted >= policy.max_action_attempts: + lifecycle = "completed" + return _ActivityProgress( + candidate_id=candidate_id, + completed=completed, + cooldowns=cooldowns, + occurrence=occurrence, + lifecycle=lifecycle, + ) + + +@dataclass(frozen=True) +class _ActivitySchedule: + lifecycle: str + next_tick: int + burst_position: int + burst_size: int + timing_disposition: str + + +def _next_activity_schedule( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + control: ParticipantActivityRandomControl, + progress: _ActivityProgress, +) -> _ActivitySchedule: + lifecycle = progress.lifecycle + burst_position = state.burst_position + burst_size = state.burst_size + next_tick = context.current_tick + timing_disposition = state.next_timing_disposition + if lifecycle == "running" and burst_position + 1 < burst_size: + burst_position += 1 + elif lifecycle == "running": + burst_position = 0 + burst_size = draw_activity_integer( + ParticipantActivityDrawContext( + policy=context.policy, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=progress.occurrence, + control=control, + ), + local_coordinate=2, + minimum=1, + maximum=context.policy.max_burst_size, + ) + timing = next_activity_timing( + policy=context.policy, + time_model=context.time_model, + participant_address=context.participant_address, + time_segment=state.time_segment, + occurrence_ordinal=progress.occurrence, + current_tick=context.current_tick, + control=control, + ) + timing_disposition = timing.disposition + if timing.tick is None: + lifecycle = "completed" + else: + next_tick = timing.tick + return _ActivitySchedule( + lifecycle=lifecycle, + next_tick=next_tick, + burst_position=burst_position, + burst_size=burst_size, + timing_disposition=timing_disposition, + ) + + +def _activity_retry_state( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + *, + attempted: int, + failed: int, +) -> ParticipantAutonomousExecutionStateModel: + candidate_id = context.policy.action_candidate_ids[state.next_action_index] + return state.model_copy( + update={ + "next_tick": context.current_tick, + "attempted_actions": attempted, + "failed_actions": failed, + "current_retry": state.current_retry + 1, + "last_candidate_id": candidate_id, + "last_action_instance_id": request.action_instance_id, + } + ) + + +def next_activity_occurrence_state( + context: _DueActionContext, + state: ParticipantAutonomousExecutionStateModel, + request: ParticipantActionAdmissionRequest, + *, + action_succeeded: bool, + failure_class: str | None, + protocol_failure: bool, +) -> ParticipantAutonomousExecutionStateModel: + control = context.activity_control + if control is None: + raise ValueError("participant activity execution requires a random control") + attempted = state.attempted_actions + 1 + failed = state.failed_actions + (0 if action_succeeded else 1) + if _activity_attempt_is_retryable( + context, + state, + action_succeeded=action_succeeded, + failure_class=failure_class, + protocol_failure=protocol_failure, + attempted=attempted, + ): + return _activity_retry_state( + context, + state, + request, + attempted=attempted, + failed=failed, + ) + progress = _completed_activity_progress( + context, + state, + action_succeeded=action_succeeded, + protocol_failure=protocol_failure, + attempted=attempted, + ) + schedule = _next_activity_schedule(context, state, control, progress) + return state.model_copy( + update={ + "lifecycle_state": schedule.lifecycle, + "next_tick": schedule.next_tick, + "attempted_actions": attempted, + "succeeded_actions": state.succeeded_actions + (1 if action_succeeded else 0), + "failed_actions": failed, + "occurrence_ordinal": progress.occurrence, + "current_retry": 0, + "burst_position": schedule.burst_position, + "burst_size": schedule.burst_size, + "last_candidate_id": progress.candidate_id, + "completed_candidate_ids": progress.completed, + "candidate_cooldown_until": progress.cooldowns, + "last_action_instance_id": request.action_instance_id, + "next_timing_disposition": schedule.timing_disposition, + } + ) + + +__all__ = ["next_activity_occurrence_state"] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_initialization.py b/implementations/python/packages/raes_runtime/participant_scheduler_initialization.py new file mode 100644 index 000000000..233620aa2 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_scheduler_initialization.py @@ -0,0 +1,229 @@ +"""Participant state initialization for autonomous scheduling.""" + +from raes_contracts.contracts import ParticipantAutonomousExecutionStateModel +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.participant_episode import ParticipantEpisodeInitializeRequest +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from raes_processor.models import CompiledTimeModel, ParticipantAutonomousExecutionRuntime + +from .participant_activity import ( + ParticipantActivityDrawContext, + ParticipantActivityRandomControl, + activity_control_for, + draw_activity_integer, + next_activity_timing, +) +from .participant_scheduler_policy import _policy_digest +from .participant_scheduler_time import cadence, clock_coordinate + +_RESOURCE_GOVERNED_PROFILE = "participant-autonomous-execution/v3" + + +def clock_tick(snapshot: RuntimeSnapshot, clock_address: str) -> int: + if snapshot.time_model_state is None: + raise ValueError("autonomous participant execution requires typed shared-time state") + clock = snapshot.time_model_state.clocks.get(clock_address) + if clock is None: + raise ValueError(f"autonomous participant clock {clock_address!r} has no runtime state") + return clock.coordinate.tick + + +def _state_identity(state: ParticipantAutonomousExecutionStateModel) -> tuple[object, ...]: + return ( + state.policy_address, + state.policy_digest, + state.participant_address, + state.episode_id, + state.participant_implementation_ref, + state.clock_address, + state.time_segment, + state.profile, + state.random_control_id, + state.random_profile_id, + state.random_namespace, + ) + + +def _ensure_participant_episode( + participant_runtime: object, + snapshot: RuntimeSnapshot, + participant_address: str, +) -> ApplyResult: + if participant_address in snapshot.participant_episode_results: + return ApplyResult(success=True, snapshot=snapshot) + return participant_runtime.initialize( + ParticipantEpisodeInitializeRequest( + participant_address=participant_address, + episode_id=f"{participant_address}-autonomous-0", + ), + snapshot, + ) + + +def _activity_control_unbound_result( + policy: ParticipantAutonomousExecutionRuntime, + snapshot: RuntimeSnapshot, +) -> ApplyResult: + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code="runtime.participant-activity-control-unbound", + domain="participant", + address=policy.address, + message=( + f"Participant activity policy requires admitted stochastic control " + f"{policy.stochastic_control_ref!r}." + ), + ) + ], + ) + + +def _initial_activity_schedule( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + snapshot: RuntimeSnapshot, + participant_address: str, + segment: int, + activity_control: ParticipantActivityRandomControl | None, +) -> tuple[int | None, int, str]: + if activity_control is None: + first_tick, _ = cadence(policy, time_model) + return first_tick, 1, "cadence" + current_tick = clock_tick(snapshot, policy.clock_address) + burst_size = draw_activity_integer( + ParticipantActivityDrawContext( + policy=policy, + participant_address=participant_address, + time_segment=segment, + occurrence_ordinal=0, + control=activity_control, + ), + local_coordinate=2, + minimum=1, + maximum=policy.max_burst_size, + ) + timing = next_activity_timing( + policy=policy, + time_model=time_model, + participant_address=participant_address, + time_segment=segment, + occurrence_ordinal=0, + current_tick=current_tick, + control=activity_control, + ) + return timing.tick, burst_size, timing.disposition + + +def _initial_participant_state( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + snapshot: RuntimeSnapshot, + participant_address: str, + segment: int, + activity_control: ParticipantActivityRandomControl | None, +) -> ParticipantAutonomousExecutionStateModel: + first_tick, burst_size, timing_disposition = _initial_activity_schedule( + policy, + time_model, + snapshot, + participant_address, + segment, + activity_control, + ) + return ParticipantAutonomousExecutionStateModel( + policy_address=policy.address, + policy_digest=_policy_digest(policy, time_model), + participant_address=participant_address, + episode_id=snapshot.participant_episode_results[participant_address]["episode_id"], + participant_implementation_ref=policy.participant_implementation_ref, + clock_address=policy.clock_address, + time_segment=segment, + lifecycle_state="running" if first_tick is not None else "completed", + next_tick=first_tick if first_tick is not None else clock_tick(snapshot, policy.clock_address), + next_action_index=0, + attempted_actions=0, + succeeded_actions=0, + failed_actions=0, + profile=policy.profile, + random_control_id=activity_control.control_id if activity_control is not None else None, + random_profile_id=activity_control.profile_id if activity_control is not None else None, + random_namespace=activity_control.namespace if activity_control is not None else None, + burst_size=burst_size, + next_timing_disposition=timing_disposition, + ) + + +def _persist_initial_participant_state( + policy: ParticipantAutonomousExecutionRuntime, + snapshot: RuntimeSnapshot, + participant_address: str, + expected: ParticipantAutonomousExecutionStateModel, + changed: list[str], +) -> ApplyResult: + key = f"{policy.address}.state.{participant_address}" + states = dict(snapshot.participant_autonomous_execution_states) + if key in states and _state_identity(ParticipantAutonomousExecutionStateModel.model_validate(states[key])) != ( + _state_identity(expected) + ): + return ApplyResult( + success=False, + snapshot=snapshot, + diagnostics=[ + Diagnostic( + code="runtime.participant-autonomous-state-conflict", + domain="participant", + address=policy.address, + message="Existing autonomous participant state does not match the compiled policy.", + ) + ], + ) + working = snapshot + if key not in states: + states[key] = expected.model_dump(mode="json") + working = snapshot.with_entries( + dict(snapshot.entries), + participant_autonomous_execution_states=states, + ) + changed.append(key) + return ApplyResult(success=True, snapshot=working, changed_addresses=changed) + + +def initialize_participant( + policy: ParticipantAutonomousExecutionRuntime, + time_model: CompiledTimeModel, + participant_runtime: object, + snapshot: RuntimeSnapshot, + participant_address: str, + activity_controls: dict[str, ParticipantActivityRandomControl], +) -> ApplyResult: + episode_result = _ensure_participant_episode(participant_runtime, snapshot, participant_address) + if not episode_result.success: + return episode_result + working = episode_result.snapshot + changed = list(episode_result.changed_addresses) + segment, _ = clock_coordinate(working, policy.clock_address) + activity_control = activity_control_for(policy, activity_controls) + if ( + policy.profile + in { + "participant-autonomous-execution/v2", + _RESOURCE_GOVERNED_PROFILE, + } + and activity_control is None + ): + return _activity_control_unbound_result(policy, working) + expected = _initial_participant_state( + policy, + time_model, + working, + participant_address, + segment, + activity_control, + ) + return _persist_initial_participant_state(policy, working, participant_address, expected, changed) + + +__all__ = ["clock_tick", "initialize_participant"] diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py index f6eaee236..aade1d2de 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass, replace +from dataclasses import replace from typing import cast from raes_contracts.contracts import ( @@ -17,10 +17,8 @@ from .participant_action_validation import autonomous_action_result_violation from .participant_activity import ( - ParticipantActivityDrawContext, ParticipantActivityRandomControl, activity_control_for, - draw_activity_integer, next_activity_timing, select_activity_candidate, ) @@ -31,6 +29,9 @@ persist_activity_state, ) from .participant_scheduler_concurrency import participant_generation_commit_diagnostic, run_policy_due_concurrently +from .participant_scheduler_activity_state import ( + next_activity_occurrence_state as _next_activity_occurrence_state, +) from .participant_scheduler_resources import ( commit_activity_resources, measurement_requirements, @@ -223,202 +224,6 @@ def _run_one_due_action( return next_state -def _activity_attempt_is_retryable( - context: _DueActionContext, - state: ParticipantAutonomousExecutionStateModel, - *, - action_succeeded: bool, - failure_class: str | None, - protocol_failure: bool, - attempted: int, -) -> bool: - index = state.next_action_index - return ( - not protocol_failure - and not action_succeeded - and failure_class in context.policy.action_candidate_retry_failure_classes[index] - and state.current_retry < context.policy.action_candidate_max_retries[index] - and attempted < context.policy.max_action_attempts - ) - - -@dataclass(frozen=True) -class _ActivityProgress: - candidate_id: str - completed: list[str] - cooldowns: dict[str, int] - occurrence: int - lifecycle: str - - -def _completed_activity_progress( - context: _DueActionContext, - state: ParticipantAutonomousExecutionStateModel, - *, - action_succeeded: bool, - protocol_failure: bool, - attempted: int, -) -> _ActivityProgress: - policy = context.policy - index = state.next_action_index - candidate_id = policy.action_candidate_ids[index] - completed = list(state.completed_candidate_ids) - if action_succeeded and candidate_id not in completed: - completed.append(candidate_id) - cooldowns = dict(state.candidate_cooldown_until) - cooldowns[candidate_id] = context.current_tick + policy.action_candidate_cooldown_ticks[index] - occurrence = state.occurrence_ordinal + 1 - lifecycle = state.lifecycle_state - if protocol_failure or (not action_succeeded and policy.failure_policy == "stop"): - lifecycle = "failed" - elif occurrence >= policy.max_occurrences or attempted >= policy.max_action_attempts: - lifecycle = "completed" - return _ActivityProgress( - candidate_id=candidate_id, - completed=completed, - cooldowns=cooldowns, - occurrence=occurrence, - lifecycle=lifecycle, - ) - - -@dataclass(frozen=True) -class _ActivitySchedule: - lifecycle: str - next_tick: int - burst_position: int - burst_size: int - timing_disposition: str - - -def _next_activity_schedule( - context: _DueActionContext, - state: ParticipantAutonomousExecutionStateModel, - control: ParticipantActivityRandomControl, - progress: _ActivityProgress, -) -> _ActivitySchedule: - lifecycle = progress.lifecycle - burst_position = state.burst_position - burst_size = state.burst_size - next_tick = context.current_tick - timing_disposition = state.next_timing_disposition - if lifecycle == "running" and burst_position + 1 < burst_size: - burst_position += 1 - elif lifecycle == "running": - burst_position = 0 - burst_size = draw_activity_integer( - ParticipantActivityDrawContext( - policy=context.policy, - participant_address=context.participant_address, - time_segment=state.time_segment, - occurrence_ordinal=progress.occurrence, - control=control, - ), - local_coordinate=2, - minimum=1, - maximum=context.policy.max_burst_size, - ) - timing = next_activity_timing( - policy=context.policy, - time_model=context.time_model, - participant_address=context.participant_address, - time_segment=state.time_segment, - occurrence_ordinal=progress.occurrence, - current_tick=context.current_tick, - control=control, - ) - timing_disposition = timing.disposition - if timing.tick is None: - lifecycle = "completed" - else: - next_tick = timing.tick - return _ActivitySchedule( - lifecycle=lifecycle, - next_tick=next_tick, - burst_position=burst_position, - burst_size=burst_size, - timing_disposition=timing_disposition, - ) - - -def _activity_retry_state( - context: _DueActionContext, - state: ParticipantAutonomousExecutionStateModel, - request: ParticipantActionAdmissionRequest, - *, - attempted: int, - failed: int, -) -> ParticipantAutonomousExecutionStateModel: - candidate_id = context.policy.action_candidate_ids[state.next_action_index] - return state.model_copy( - update={ - "next_tick": context.current_tick, - "attempted_actions": attempted, - "failed_actions": failed, - "current_retry": state.current_retry + 1, - "last_candidate_id": candidate_id, - "last_action_instance_id": request.action_instance_id, - } - ) - - -def _next_activity_occurrence_state( - context: _DueActionContext, - state: ParticipantAutonomousExecutionStateModel, - request: ParticipantActionAdmissionRequest, - *, - action_succeeded: bool, - failure_class: str | None, - protocol_failure: bool, -) -> ParticipantAutonomousExecutionStateModel: - control = context.activity_control - if control is None: - raise ValueError("participant activity execution requires a random control") - attempted = state.attempted_actions + 1 - failed = state.failed_actions + (0 if action_succeeded else 1) - if _activity_attempt_is_retryable( - context, - state, - action_succeeded=action_succeeded, - failure_class=failure_class, - protocol_failure=protocol_failure, - attempted=attempted, - ): - return _activity_retry_state( - context, - state, - request, - attempted=attempted, - failed=failed, - ) - progress = _completed_activity_progress( - context, - state, - action_succeeded=action_succeeded, - protocol_failure=protocol_failure, - attempted=attempted, - ) - schedule = _next_activity_schedule(context, state, control, progress) - return state.model_copy( - update={ - "lifecycle_state": schedule.lifecycle, - "next_tick": schedule.next_tick, - "attempted_actions": attempted, - "succeeded_actions": state.succeeded_actions + (1 if action_succeeded else 0), - "failed_actions": failed, - "occurrence_ordinal": progress.occurrence, - "current_retry": 0, - "burst_position": schedule.burst_position, - "burst_size": schedule.burst_size, - "last_candidate_id": progress.candidate_id, - "completed_candidate_ids": progress.completed, - "candidate_cooldown_until": progress.cooldowns, - "last_action_instance_id": request.action_instance_id, - "next_timing_disposition": schedule.timing_disposition, - } - ) - - def _run_one_activity_action( context: _DueActionContext, state: ParticipantAutonomousExecutionStateModel, From e912c77681fca8b4c30881f7b9b9ef8eb8f6f896 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 08:12:57 +0200 Subject: [PATCH 52/55] style: format scheduler module --- .../python/packages/raes_runtime/participant_scheduler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index 0f6a68333..9c6dc3168 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -32,6 +32,7 @@ from .participant_scheduler_time import cadence as _cadence from .participant_scheduler_time import clock_coordinate + def _missing_execution_service_result( policy: ParticipantAutonomousExecutionRuntime, run: SchedulerRunState, From 848de13dc3385239299e8200306989657c0a55ca Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 08:15:17 +0200 Subject: [PATCH 53/55] fix: correct scheduler lint errors --- .../python/packages/raes_runtime/participant_scheduler.py | 6 +++++- .../raes_runtime/participant_scheduler_operations.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/implementations/python/packages/raes_runtime/participant_scheduler.py b/implementations/python/packages/raes_runtime/participant_scheduler.py index 9c6dc3168..2e4cff1b4 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler.py @@ -17,11 +17,13 @@ set_execution_clock_lifecycle, ) from .participant_resource_budgets import initialize_participant_resource_budgets -from .participant_scheduler_lifecycle import reset_policy_at_clock from .participant_scheduler_initialization import ( clock_tick as _clock_tick, +) +from .participant_scheduler_initialization import ( initialize_participant as _initialize_participant, ) +from .participant_scheduler_lifecycle import reset_policy_at_clock from .participant_scheduler_operations import ( SchedulerRunState, participant_due_context, @@ -32,6 +34,8 @@ from .participant_scheduler_time import cadence as _cadence from .participant_scheduler_time import clock_coordinate +_RESOURCE_GOVERNED_PROFILE = "participant-autonomous-execution/v3" + def _missing_execution_service_result( policy: ParticipantAutonomousExecutionRuntime, diff --git a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py index aade1d2de..67ab20a6d 100644 --- a/implementations/python/packages/raes_runtime/participant_scheduler_operations.py +++ b/implementations/python/packages/raes_runtime/participant_scheduler_operations.py @@ -28,10 +28,10 @@ annotate_activity_history, persist_activity_state, ) -from .participant_scheduler_concurrency import participant_generation_commit_diagnostic, run_policy_due_concurrently from .participant_scheduler_activity_state import ( next_activity_occurrence_state as _next_activity_occurrence_state, ) +from .participant_scheduler_concurrency import participant_generation_commit_diagnostic, run_policy_due_concurrently from .participant_scheduler_resources import ( commit_activity_resources, measurement_requirements, From 2f7151702e0ab2446cb4ffdbfc477b9602fc040c Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 08:22:51 +0200 Subject: [PATCH 54/55] fix: preserve activity draw call semantics --- .../python/packages/raes_runtime/participant_activity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/implementations/python/packages/raes_runtime/participant_activity.py b/implementations/python/packages/raes_runtime/participant_activity.py index aeef0ab49..e97d9a4dd 100644 --- a/implementations/python/packages/raes_runtime/participant_activity.py +++ b/implementations/python/packages/raes_runtime/participant_activity.py @@ -197,7 +197,7 @@ def next_activity_timing( minimum = _timing_units(policy.timing_minimum_ticks, step_ticks) maximum = _timing_units(policy.timing_maximum_ticks, step_ticks) interval_units = draw_activity_integer( - ParticipantActivityDrawContext( + context=ParticipantActivityDrawContext( policy=policy, participant_address=participant_address, time_segment=time_segment, @@ -295,7 +295,7 @@ def select_activity_candidate( return None total = sum(policy.action_candidate_weights[index] for index in eligible_indices) selected = draw_activity_integer( - ParticipantActivityDrawContext( + context=ParticipantActivityDrawContext( policy=policy, participant_address=participant_address, time_segment=time_segment, From 13b23753ecc46da686151a3eb587e18254926257 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Mon, 27 Jul 2026 08:39:39 +0200 Subject: [PATCH 55/55] fix: reduce participant behavior complexity --- .../raes/semantics/participant_behavior.py | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/implementations/python/packages/raes/semantics/participant_behavior.py b/implementations/python/packages/raes/semantics/participant_behavior.py index 7a7d19531..d70a75363 100644 --- a/implementations/python/packages/raes/semantics/participant_behavior.py +++ b/implementations/python/packages/raes/semantics/participant_behavior.py @@ -703,36 +703,47 @@ def _autonomous_progression_issues( 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: - issue_code = 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: - minimum_ticks = context.policy.timing.minimum_ticks - maximum_ticks = context.policy.timing.maximum_ticks - reachable = isinstance(step_ticks, int) and not minimum_ticks % step_ticks and not maximum_ticks % step_ticks - if not reachable: - issue_code = "participant.autonomous-activity-timing-unreachable" - elif bindings.cadence_count == 1 and bindings.cadence is not None: - 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 - reachable = ( - 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 - ) - if not reachable: - issue_code = "participant.autonomous-cadence-unreachable" - return issue_code + 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(