diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2b3fe0733..15a717b8c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -27,7 +27,7 @@ - [ ] Code follows project coding standards (`docs/explain/reference/coding-standards.md`) - [ ] FM level classified if semantic change - [ ] Published contract schemas regenerated if models changed -- [ ] Towncrier fragment added under `changelog.d/`, or not needed +- [ ] PR title is a Conventional Commit (release-please derives the version and `CHANGELOG.md` from it) - [ ] Architectural docs updated if applicable ## Notes for Review diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f6d82aa8..4ef1569c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,14 +20,14 @@ jobs: verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8 - name: Resolve policy base revision id: base run: | @@ -58,7 +58,7 @@ jobs: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s verify -- "${verify_args[@]}" - name: Upload coverage report if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-report path: implementations/python/coverage.xml @@ -66,12 +66,12 @@ jobs: fuzz: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8 - name: Run fuzz session run: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s fuzz @@ -82,12 +82,12 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8 - name: Probe for a container runtime id: runtime run: | @@ -110,17 +110,17 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8 - name: Run OSV-scanner (advisory) run: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s osv_scan - name: Upload OSV-scanner report if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: osv-scanner-report path: implementations/python/osv-scanner-report.json @@ -131,14 +131,14 @@ jobs: needs: [verify] if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: coverage-report path: implementations/python/ - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@299e4b793aaa83bf2aba7c9c14bedbb485688ec4 # v7 + uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..945d907ee --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,54 @@ +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. + +on: + push: + branches: [main] + pull_request: + branches: [main, dev] + workflow_dispatch: + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.12" + - name: Install uv + uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # 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: Upload Pages artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + with: + path: docs/_build/html + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml index d2ba7b1ae..b65f78b46 100644 --- a/.github/workflows/pr-title-lint.yml +++ b/.github/workflows/pr-title-lint.yml @@ -34,10 +34,10 @@ jobs: # tools/check_pr_title.py must not be able to weaken its own required # check (codex review finding, issue #567). The shared validator is still # exercised against the PR's own code by the test suite in ci.yml. - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: ref: ${{ github.event.pull_request.base.sha }} - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" - name: Validate PR title diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 638c6e304..2dd7ab4bb 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -36,7 +36,7 @@ jobs: release_created: ${{ steps.rp.outputs.release_created }} tag_name: ${{ steps.rp.outputs.tag_name }} steps: - - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 id: rp with: token: ${{ secrets.GITHUB_TOKEN }} @@ -52,16 +52,16 @@ jobs: contents: write # upload the built distributions to the Release id-token: write # OIDC trusted publishing to PyPI (no stored token) steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8 - name: Build the corpus-bundled wheel + sdist run: uv build --out-dir dist implementations/python @@ -114,7 +114,7 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - name: Open the back-merge PR (main -> dev) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 623f258d1..7a3db3742 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,9 +40,9 @@ uv sync --all-extras surface changes. 5. For documentation changes, follow [`docs/explain/reference/documentation-style-guide.md`](docs/explain/reference/documentation-style-guide.md). -6. Add a towncrier fragment under [`changelog.d/`](changelog.d/) unless the - change is only internal maintenance. See - [`changelog.d/README.md`](changelog.d/README.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`. 7. Run the relevant checks locally. 8. Open a pull request against `dev` with a concrete description of what changed and why. @@ -68,11 +68,11 @@ artifact, or shared runtime changes. ## Changelog -Release notes are generated from towncrier fragments under `changelog.d/`. -Do not hand-edit `CHANGELOG.md`. - -Fragment names follow the pattern described in -[`changelog.d/README.md`](changelog.d/README.md). +`CHANGELOG.md` is generated by +[release-please](https://github.com/googleapis/release-please) from the +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 diff --git a/README.md b/README.md index 8fd8bf04a..8701873af 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,6 @@ uv run aces-mcp - `docs/` - explanatory documentation, API docs, and architecture decisions - `research/` - supporting literature and reference ecosystem material - `tools/` - repository maintenance, policy, and publication tooling -- `changelog.d/` - towncrier release note fragments ## Lineage @@ -191,10 +190,12 @@ conformance, and existing scenario examples. ## Versioning -The Python package currently declares its version in -[`implementations/python/pyproject.toml`](https://github.com/Brad-Edwards/aces/blob/main/implementations/python/pyproject.toml). -Release notes are collated from towncrier fragments in -[`changelog.d/`](https://github.com/Brad-Edwards/aces/tree/main/changelog.d). Do not hand-edit `CHANGELOG.md`. +The Python package version lives in +[`implementations/python/pyproject.toml`](https://github.com/Brad-Edwards/aces/blob/main/implementations/python/pyproject.toml) +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/Brad-Edwards/aces/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. @@ -225,3 +226,5 @@ If you use ACES SDL in academic work, cite the repository: ## License Released under the MIT License. See [LICENSE](https://github.com/Brad-Edwards/aces/blob/main/LICENSE). +Third-party attribution and license notices are recorded in +[THIRD_PARTY_NOTICES.md](https://github.com/Brad-Edwards/aces/blob/main/THIRD_PARTY_NOTICES.md). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..c1f5d1759 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,29 @@ +# Third-Party Notices + +## Open Cyber Range SDL Parser + +Portions of the ACES SDL model structure were translated and adapted from Open +Cyber Range SDL Parser v0.21.2, revision +`fe83e8281fc4b954967fbaa5a0d099007ddcb06c`. + +The MIT License (MIT) + +Copyright (c) 2022 CR14 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/changelog.d/682.changed.md b/changelog.d/682.changed.md deleted file mode 100644 index ed0daaf3f..000000000 --- a/changelog.d/682.changed.md +++ /dev/null @@ -1,4 +0,0 @@ -Narrow SDL objective success to observable state (ADR-073, SEM-206): -`objectives.success` now references `conditions` only. ADR-002's objective-success -clause is amended accordingly, and the published `sdl-authoring-input-v1` and -`instantiated-scenario-v1` schemas are updated with change-ledger entries. diff --git a/changelog.d/682.removed.md b/changelog.d/682.removed.md deleted file mode 100644 index dffea0e29..000000000 --- a/changelog.d/682.removed.md +++ /dev/null @@ -1,6 +0,0 @@ -Remove the OCR-inherited scoring/reward surfaces from the SDL (ADR-073, SEM-206): -the `metrics`, `evaluations`, `tlos`, and `goals` sections, the -`agents.reward_calculator` label, `entities.tlos`, `injects.tlos`, and the -scoring references on workflow predicates. Scenarios using any of these now fail -parsing with a migration pointer. Graded scoring, reward, and evaluation outputs -live in the experiment/evaluator plane (ADR-055/064/069), not the SDL. diff --git a/contracts/README.md b/contracts/README.md index eef1dac7d..221ee422e 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -6,7 +6,10 @@ The goal of this bucket is organizational clarity: - `schemas/` contains published contract schemas - `fixtures/` contains valid and invalid payload corpora for those contracts -- `profiles/` contains capability profile declarations +- `profiles/` contains capability profiles and separately versioned + scientific-completeness taxonomy/assessment declarations +- `realization-envelopes/` contains configuration-bound backend realization + declarations whose identity is carried through manifests, plans, and snapshots `schema-publication-manifest.json` is the authoritative publication inventory for the current machine-readable schema set. The contracts verification gate @@ -88,3 +91,5 @@ The canonical machine-readable manifest of the authority boundary governed by [ADR-019](../docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md) and enforced by `tools/check_authority_boundary.py`. +The `provenance/` family contains revision-pinned SDL lineage, derivation, and +third-party notice dispositions governed by ADR-019 and ADR-080. diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index df5ce81c1..90855ca33 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -722,7 +722,15 @@ "terms": { "conditions": { "title": "Conditions", - "description": "Condition section support." + "description": "Legacy executable probe declaration support." + }, + "propositions": { + "title": "Propositions", + "description": "Backend-neutral typed observable proposition support." + }, + "assertions": { + "title": "Assertions", + "description": "Typed proposition-use and truth composition support." }, "objectives": { "title": "Objectives", diff --git a/contracts/concept-authority/reference-models-v1.json b/contracts/concept-authority/reference-models-v1.json index de4fac9d7..b280328a2 100644 --- a/contracts/concept-authority/reference-models-v1.json +++ b/contracts/concept-authority/reference-models-v1.json @@ -74,7 +74,7 @@ }, "scenario-condition": { "title": "Scenario Condition", - "description": "Shared reference model for observable condition declarations.", + "description": "Shared reference model for legacy executable probe declarations; a condition is a realization mechanism, not an observable proposition.", "concept_family": "observables", "authoritative_schema": { "contract_id": "sdl-authoring-input-v1", @@ -89,12 +89,59 @@ } ], "key_fields": [ + "proposition", "command", "interval", "source", "environment" ] }, + "scenario-proposition": { + "title": "Scenario Proposition", + "description": "Shared reference model for backend-neutral typed claims over declared or observed scenario state.", + "concept_family": "observables", + "authoritative_schema": { + "contract_id": "sdl-authoring-input-v1", + "schema_pointer": "#/$defs/Proposition", + "instance_path": "propositions.*" + }, + "reused_schemas": [ + { + "contract_id": "instantiated-scenario-v1", + "schema_pointer": "#/$defs/Proposition", + "instance_path": "propositions.*" + } + ], + "key_fields": [ + "subjects", + "basis", + "predicate", + "quantifier", + "evidence_requirements" + ] + }, + "scenario-assertion": { + "title": "Scenario Assertion", + "description": "Shared reference model for a precondition, invariant, or postcondition use of a proposition.", + "concept_family": "observables", + "authoritative_schema": { + "contract_id": "sdl-authoring-input-v1", + "schema_pointer": "#/$defs/Assertion", + "instance_path": "assertions.*" + }, + "reused_schemas": [ + { + "contract_id": "instantiated-scenario-v1", + "schema_pointer": "#/$defs/Assertion", + "instance_path": "assertions.*" + } + ], + "key_fields": [ + "proposition", + "role", + "polarity" + ] + }, "scenario-event": { "title": "Scenario Event", "description": "Shared reference model for action and event coordination declarations.", @@ -114,7 +161,7 @@ "key_fields": [ "name", "source", - "conditions", + "assertions", "injects" ] }, 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 c669b664d..8b36ac6ba 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "identity-basis-for-reusable_scenario", 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 1320ef76c..bc870ed47 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "identity-basis-for-reusable_scenario", 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 21ba089c2..1bbe57186 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "identity-basis-for-reusable_scenario", 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 bc0396f5c..430baa968 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "identity-basis-for-reusable_scenario", 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 8f3c6cec5..6bc062369 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "identity-basis-for-reusable_scenario", 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 7b7b1b76e..460d425d1 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "identity-basis-for-reusable_scenario", 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 90fd61ca1..054455210 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "identity-basis-for-reusable_scenario", 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 eac5b50a6..157aed6b4 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 @@ -2,6 +2,30 @@ "schema_version": "reusable-asset-trust-policy/v1", "policy_id": "aces-reusable-asset-trust-policy", "families": [ + { + "asset_family": "associated_artifact_set", + "identity_basis": "associated-artifact-manifest-v1 canonical parent-plus-reference-set digest", + "evidence_requirements": [ + { + "evidence_class": "integrity_digest", + "enforcement": "required", + "mechanism_ref": "associated-artifact-set/v1 derived set_digest", + "description": "The set digest binds the exact parent reference and keyed artifact-reference set without changing parent identity." + }, + { + "evidence_class": "artifact_checksum", + "enforcement": "required", + "mechanism_ref": "validate_associated_artifact_manifest bounded concrete-byte validation", + "description": "Every referenced payload checksum and size is recomputed from an explicitly supplied bounded byte stream." + }, + { + "evidence_class": "authenticity_signature", + "enforcement": "optional", + "mechanism_ref": "downstream signature over the derived associated-artifact set digest", + "description": "A signature may authenticate the set digest, but neither the digest nor payload checksums establish authenticity alone." + } + ] + }, { "asset_family": "reusable_scenario", "identity_basis": "instantiated-scenario-v1 / scenario-instantiation-request-v1 scenario reference id", diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/exact-descriptor-alias.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/exact-descriptor-alias.json new file mode 100644 index 000000000..8fb4bda8e --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/exact-descriptor-alias.json @@ -0,0 +1,42 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "scenario", + "parent_ref": { + "ref_kind": "scenario", + "ref_id": "training-range" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + }, + "guide-copy": { + "artifact_id": "guide-copy", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/keyed-id-mismatch.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/keyed-id-mismatch.json new file mode 100644 index 000000000..03b53e446 --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/keyed-id-mismatch.json @@ -0,0 +1,28 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "scenario", + "parent_ref": { + "ref_kind": "scenario", + "ref_id": "training-range" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "other-id", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/scope-parent-mismatch.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/scope-parent-mismatch.json new file mode 100644 index 000000000..e013a8bb3 --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/scope-parent-mismatch.json @@ -0,0 +1,28 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "experiment", + "parent_ref": { + "ref_kind": "scenario", + "ref_id": "training-range" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/secret-bearing-uri.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/secret-bearing-uri.json new file mode 100644 index 000000000..5717cef9a --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/secret-bearing-uri.json @@ -0,0 +1,28 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "scenario", + "parent_ref": { + "ref_kind": "scenario", + "ref_id": "training-range" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "https://user:secret@example.test/guide", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/unknown-extra.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/unknown-extra.json new file mode 100644 index 000000000..5f0780e6c --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/invalid/unknown-extra.json @@ -0,0 +1,29 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "scenario", + "parent_ref": { + "ref_kind": "scenario", + "ref_id": "training-range" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "credentials": "forbidden" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/apparatus-context.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/apparatus-context.json new file mode 100644 index 000000000..3bc6b96f6 --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/apparatus-context.json @@ -0,0 +1,29 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "apparatus-context-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "experiment", + "parent_ref": { + "ref_kind": "apparatus-context", + "ref_id": "apparatus-001", + "ref_version": "1.0.0" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:da6f81eb2f60aefa063a967f884e5d7aef0f9730a3d7372b0ed54f19832471b1" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/authoring-input.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/authoring-input.json new file mode 100644 index 000000000..2e84b2d3b --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/authoring-input.json @@ -0,0 +1,29 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "authoring-input-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "experiment", + "parent_ref": { + "ref_kind": "authoring-input", + "ref_id": "experiment-input-001", + "ref_version": "1.0.0" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:24afeca049625cb389666110a694a2bc675cbf3750e2f7c405b0364ca0e22647" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/run.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/run.json new file mode 100644 index 000000000..a537c5ac4 --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/run.json @@ -0,0 +1,29 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "run-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "experiment", + "parent_ref": { + "ref_kind": "run", + "ref_id": "run-001", + "ref_version": "1.0.0" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:d7ed45878918be25d6b2bfdb64116651b598f3b2dabb8d55eaef17285919a90e" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/scenario-snapshot.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/scenario-snapshot.json new file mode 100644 index 000000000..282e8593d --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/scenario-snapshot.json @@ -0,0 +1,30 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-snapshot-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "scenario", + "parent_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "training-range", + "ref_version": "1.0.0", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:cd34c3d08b2ed0fae387f1a8ea4d392df73b7878ecde6731a7f0ece63af56f02" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/scenario.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/scenario.json new file mode 100644 index 000000000..65ede38e3 --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/scenario.json @@ -0,0 +1,28 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "scenario", + "parent_ref": { + "ref_kind": "scenario", + "ref_id": "training-range" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:3494a74b2e1475df6141039f75f0115cae6f13f7fc562c6b24e7343e4b32a082" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/study.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/study.json new file mode 100644 index 000000000..52d38ee4a --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/study.json @@ -0,0 +1,29 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "study-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "experiment", + "parent_ref": { + "ref_kind": "study", + "ref_id": "study-001", + "ref_version": "1.0.0" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:dc45958c6e26e9441900e2c8d15309e6f42681a4db6eea2677d471f3232b854d" +} diff --git a/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/task.json b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/task.json new file mode 100644 index 000000000..73c1413de --- /dev/null +++ b/contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/task.json @@ -0,0 +1,29 @@ +{ + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "task-attachments", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "experiment", + "parent_ref": { + "ref_kind": "task", + "ref_id": "task-001", + "ref_version": "1.0.0" + }, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287", + "checksum": { + "algorithm": "sha256", + "value": "ddc7a69ede7530aa87098a487dbae1d82e751dce5cd5bbe8477355ecd12f4287" + }, + "size_bytes": 15, + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal" + } + }, + "set_digest": "sha256:81829cbcad5384c1a69b607522f62f3b2e111fc05922e03a2e8b70793d851228" +} 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 71c184525..3afe181e0 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 @@ -136,7 +136,7 @@ "workflows" ], "supports_workflows": true, - "supports_condition_refs": true, + "supports_assertion_refs": true, "supports_inject_bindings": true, "supported_workflow_features": [ "call", 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 7f47efc16..9f2491705 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 @@ -136,7 +136,7 @@ "workflows" ], "supports_workflows": true, - "supports_condition_refs": true, + "supports_assertion_refs": true, "supports_inject_bindings": true, "supported_workflow_features": [ "call", 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 cb723bd8b..cd0241b07 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 @@ -136,7 +136,7 @@ "workflows" ], "supports_workflows": true, - "supports_condition_refs": true, + "supports_assertion_refs": true, "supports_inject_bindings": true, "supported_workflow_features": [ "call", 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 d44e93375..485f3a493 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 @@ -136,7 +136,7 @@ "workflows" ], "supports_workflows": true, - "supports_condition_refs": true, + "supports_assertion_refs": true, "supports_inject_bindings": true, "supported_workflow_features": [ "call", 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 d7ffc660f..f2ee0191f 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 @@ -136,7 +136,7 @@ "workflows" ], "supports_workflows": true, - "supports_condition_refs": true, + "supports_assertion_refs": true, "supports_inject_bindings": true, "supported_workflow_features": [ "call", 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 77ff3b130..f8d4bdcc1 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -1,148 +1,81 @@ { - "schema_version": "backend-manifest/v2", - "identity": { - "name": "stub", - "version": "0.3.0" - }, - "supported_contract_versions": [ - "backend-manifest-v2", - "provisioning-plan-v1", - "orchestration-plan-v1", - "evaluation-plan-v1", - "operation-receipt-v1", - "operation-status-v1", - "runtime-snapshot-v1", - "workflow-result-envelope-v1", - "workflow-history-event-stream-v1", - "evaluation-result-envelope-v1", - "evaluation-history-event-stream-v1", - "participant-episode-state-envelope-v1", - "participant-episode-history-event-stream-v1", - "participant-behavior-history-event-stream-v1", - "participant-lifecycle-event-v1", - "participant-observation-envelope-v1", - "participant-shared-state-record-v1", - "participant-joint-action-record-v1", - "participant-time-management-context-v1", - "participant-outcome-report-v1", - "experiment-capture-spec-v1", - "experiment-evidence-record-v1", - "experiment-derived-measure-v1", - "experiment-run-v1" - ], - "compatibility": { - "processors": [ - "aces-reference-processor" - ] - }, - "realization_support": [ - { - "domain": "runtime-realization", - "support_mode": "constrained", - "supported_constraint_kinds": [ - "account-feature", - "content-type", - "node-type", - "os-family", - "workflow-feature", - "workflow-state-predicate" + "capabilities": { + "evaluator": { + "constraints": {}, + "name": "stub-evaluator", + "preserves_binding_provenance": true, + "supported_evidence_channels": [ + "api_response", + "file_artifact", + "log" ], - "supported_exact_requirement_kinds": [ - "declared-capability-match" + "supported_predicate_families": [ + "boolean", + "number", + "presence", + "string" ], - "disclosure_kinds": [ - "backend-manifest-v2", - "operation-status-v1", - "runtime-snapshot-v1" + "supported_quantifiers": [ + "all", + "any", + "at_least" ], - "constraints": {} - } - ], - "concept_bindings": [ - { - "scope": "capabilities.provisioner.supported_node_types", - "family": "assets" - }, - { - "scope": "capabilities.provisioner.supported_os_families", - "family": "assets" - }, - { - "scope": "capabilities.provisioner.supported_content_types", - "family": "tools-and-artifacts" - }, - { - "scope": "capabilities.provisioner.supported_account_features", - "family": "identities" - }, - { - "scope": "capabilities.orchestrator.supported_sections", - "family": "actions-and-events" - }, - { - "scope": "capabilities.evaluator.supported_sections", - "family": "observables" - }, - { - "scope": "capabilities.participant_runtime.supported_participant_roles", - "family": "identities" - }, - { - "scope": "capabilities.participant_runtime.supported_behavior_features", - "family": "actions-and-events" - }, - { - "scope": "capabilities.participant_runtime.supported_interaction_features", - "family": "relationships" - }, - { - "scope": "capabilities.observation.supported_capture_kinds", - "family": "provenance-and-evidence" - }, - { - "scope": "capabilities.observation.supported_channel_kinds", - "family": "apparatus-declarations" + "supported_sections": [ + "assertions", + "conditions", + "objectives", + "propositions" + ], + "supported_time_domains": [ + "scenario_time" + ], + "supported_truth_outcomes": [ + "false", + "true", + "unknown", + "unsupported" + ], + "supports_objectives": true, + "supports_scoring": true }, - { - "scope": "capabilities.observation.supported_sealing_modes", - "family": "provenance-and-evidence" - } - ], - "constraints": {}, - "capabilities": { - "provisioner": { - "name": "stub-provisioner", - "supported_node_types": [ - "switch", - "vm" + "observation": { + "constraints": {}, + "name": "stub-observation", + "supported_capture_kinds": [ + "artifact", + "log", + "observation", + "telemetry", + "trace" ], - "supported_os_families": [ - "freebsd", - "linux", - "macos", - "other", - "windows" + "supported_channel_kinds": [ + "backend-log", + "evaluation-history", + "file-artifact", + "participant-observation", + "runtime-snapshot", + "workflow-history" ], - "supported_content_types": [ - "dataset", - "directory", - "file" + "supported_evidence_contracts": [ + "experiment-capture-spec-v1", + "experiment-derived-measure-v1", + "experiment-evidence-record-v1", + "experiment-run-v1" ], - "supported_account_features": [ - "auth_method", - "disabled", - "groups", - "home", - "mail", - "shell", - "spn" + "supported_media_types": [ + "application/json", + "text/plain" ], - "max_total_nodes": null, - "supports_acls": true, - "supports_accounts": true, - "constraints": {} + "supported_sealing_modes": [ + "digest", + "immutable-store" + ], + "supports_chain_of_custody": false, + "supports_loss_disclosure": true, + "supports_redaction": true }, "orchestrator": { + "constraints": {}, "name": "stub-orchestrator", "supported_sections": [ "events", @@ -151,9 +84,6 @@ "stories", "workflows" ], - "supports_workflows": true, - "supports_condition_refs": true, - "supports_inject_bindings": true, "supported_workflow_features": [ "call", "cancellation", @@ -169,26 +99,14 @@ "attempt-counts", "outcome-matching" ], - "constraints": {} - }, - "evaluator": { - "name": "stub-evaluator", - "supported_sections": [ - "conditions", - "objectives" - ], - "supports_scoring": true, - "supports_objectives": true, - "constraints": {} + "supports_assertion_refs": true, + "supports_inject_bindings": true, + "supports_workflows": true }, "participant_runtime": { + "constraints": {}, + "feature_support": [], "name": "stub-participant-runtime", - "supported_participant_roles": [ - "blue", - "green", - "red", - "white" - ], "supported_behavior_features": [ "action_contracts", "attribution_support", @@ -207,44 +125,155 @@ "interference", "shared_state_change" ], - "feature_support": [], - "constraints": {} + "supported_participant_roles": [ + "blue", + "green", + "red", + "white" + ] }, - "observation": { - "name": "stub-observation", - "supported_capture_kinds": [ - "artifact", - "log", - "observation", - "telemetry", - "trace" + "provisioner": { + "constraints": {}, + "max_total_nodes": null, + "name": "stub-provisioner", + "supported_account_features": [ + "auth_method", + "disabled", + "groups", + "home", + "mail", + "shell", + "spn" ], - "supported_channel_kinds": [ - "backend-log", - "evaluation-history", - "file-artifact", - "participant-observation", - "runtime-snapshot", - "workflow-history" + "supported_content_types": [ + "dataset", + "directory", + "file" ], - "supported_evidence_contracts": [ - "experiment-capture-spec-v1", - "experiment-derived-measure-v1", - "experiment-evidence-record-v1", - "experiment-run-v1" + "supported_node_types": [ + "switch", + "vm" ], - "supported_media_types": [ - "application/json", - "text/plain" + "supported_os_families": [ + "freebsd", + "linux", + "macos", + "other", + "windows" ], - "supported_sealing_modes": [ - "digest", - "immutable-store" + "supports_accounts": true, + "supports_acls": true + } + }, + "compatibility": { + "processors": [ + "aces-reference-processor" + ] + }, + "concept_bindings": [ + { + "family": "assets", + "scope": "capabilities.provisioner.supported_node_types" + }, + { + "family": "assets", + "scope": "capabilities.provisioner.supported_os_families" + }, + { + "family": "tools-and-artifacts", + "scope": "capabilities.provisioner.supported_content_types" + }, + { + "family": "identities", + "scope": "capabilities.provisioner.supported_account_features" + }, + { + "family": "actions-and-events", + "scope": "capabilities.orchestrator.supported_sections" + }, + { + "family": "observables", + "scope": "capabilities.evaluator.supported_sections" + }, + { + "family": "identities", + "scope": "capabilities.participant_runtime.supported_participant_roles" + }, + { + "family": "actions-and-events", + "scope": "capabilities.participant_runtime.supported_behavior_features" + }, + { + "family": "relationships", + "scope": "capabilities.participant_runtime.supported_interaction_features" + }, + { + "family": "provenance-and-evidence", + "scope": "capabilities.observation.supported_capture_kinds" + }, + { + "family": "apparatus-declarations", + "scope": "capabilities.observation.supported_channel_kinds" + }, + { + "family": "provenance-and-evidence", + "scope": "capabilities.observation.supported_sealing_modes" + } + ], + "constraints": {}, + "identity": { + "name": "stub", + "version": "0.3.0" + }, + "realization_support": [ + { + "constraints": {}, + "disclosure_kinds": [ + "backend-manifest-v2", + "operation-status-v1", + "runtime-snapshot-v1" ], - "supports_redaction": true, - "supports_loss_disclosure": true, - "supports_chain_of_custody": false, - "constraints": {} + "domain": "runtime-realization", + "support_mode": "constrained", + "supported_constraint_kinds": [ + "account-feature", + "content-type", + "node-type", + "os-family", + "workflow-feature", + "workflow-state-predicate" + ], + "supported_exact_requirement_kinds": [ + "declared-capability-match" + ] } - } + ], + "schema_version": "backend-manifest/v2", + "supported_contract_versions": [ + "backend-manifest-v2", + "provisioning-plan-v1", + "orchestration-plan-v1", + "evaluation-plan-v1", + "operation-receipt-v1", + "operation-status-v1", + "runtime-snapshot-v1", + "workflow-result-envelope-v1", + "workflow-history-event-stream-v1", + "evaluation-result-envelope-v1", + "proposition-truth-result-v1", + "evaluation-history-event-stream-v1", + "participant-episode-state-envelope-v1", + "participant-episode-history-event-stream-v1", + "participant-behavior-history-event-stream-v1", + "participant-lifecycle-event-v1", + "participant-observation-envelope-v1", + "participant-shared-state-record-v1", + "participant-joint-action-record-v1", + "participant-time-management-context-v1", + "participant-outcome-report-v1", + "experiment-capture-spec-v1", + "experiment-evidence-record-v1", + "experiment-derived-measure-v1", + "experiment-run-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 df5ce81c1..90855ca33 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -722,7 +722,15 @@ "terms": { "conditions": { "title": "Conditions", - "description": "Condition section support." + "description": "Legacy executable probe declaration support." + }, + "propositions": { + "title": "Propositions", + "description": "Backend-neutral typed observable proposition support." + }, + "assertions": { + "title": "Assertions", + "description": "Typed proposition-use and truth composition support." }, "objectives": { "title": "Objectives", diff --git a/contracts/fixtures/concept-authority/reference-models-v1/valid/reference.json b/contracts/fixtures/concept-authority/reference-models-v1/valid/reference.json index de4fac9d7..b280328a2 100644 --- a/contracts/fixtures/concept-authority/reference-models-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/reference-models-v1/valid/reference.json @@ -74,7 +74,7 @@ }, "scenario-condition": { "title": "Scenario Condition", - "description": "Shared reference model for observable condition declarations.", + "description": "Shared reference model for legacy executable probe declarations; a condition is a realization mechanism, not an observable proposition.", "concept_family": "observables", "authoritative_schema": { "contract_id": "sdl-authoring-input-v1", @@ -89,12 +89,59 @@ } ], "key_fields": [ + "proposition", "command", "interval", "source", "environment" ] }, + "scenario-proposition": { + "title": "Scenario Proposition", + "description": "Shared reference model for backend-neutral typed claims over declared or observed scenario state.", + "concept_family": "observables", + "authoritative_schema": { + "contract_id": "sdl-authoring-input-v1", + "schema_pointer": "#/$defs/Proposition", + "instance_path": "propositions.*" + }, + "reused_schemas": [ + { + "contract_id": "instantiated-scenario-v1", + "schema_pointer": "#/$defs/Proposition", + "instance_path": "propositions.*" + } + ], + "key_fields": [ + "subjects", + "basis", + "predicate", + "quantifier", + "evidence_requirements" + ] + }, + "scenario-assertion": { + "title": "Scenario Assertion", + "description": "Shared reference model for a precondition, invariant, or postcondition use of a proposition.", + "concept_family": "observables", + "authoritative_schema": { + "contract_id": "sdl-authoring-input-v1", + "schema_pointer": "#/$defs/Assertion", + "instance_path": "assertions.*" + }, + "reused_schemas": [ + { + "contract_id": "instantiated-scenario-v1", + "schema_pointer": "#/$defs/Assertion", + "instance_path": "assertions.*" + } + ], + "key_fields": [ + "proposition", + "role", + "polarity" + ] + }, "scenario-event": { "title": "Scenario Event", "description": "Shared reference model for action and event coordination declarations.", @@ -114,7 +161,7 @@ "key_fields": [ "name", "source", - "conditions", + "assertions", "injects" ] }, diff --git a/contracts/fixtures/control-plane/proposition-truth-result-v1/invalid/decided-without-evidence.json b/contracts/fixtures/control-plane/proposition-truth-result-v1/invalid/decided-without-evidence.json new file mode 100644 index 000000000..e7633f2af --- /dev/null +++ b/contracts/fixtures/control-plane/proposition-truth-result-v1/invalid/decided-without-evidence.json @@ -0,0 +1,17 @@ +{ + "schema_version": "proposition-truth-result/v1", + "result_id": "truth-run-42-unfounded", + "proposition_address": "evaluation.proposition.service-available", + "assertion_address": "evaluation.assertion.service-ready", + "assertion_polarity": "positive", + "proposition_outcome": "true", + "assertion_outcome": "true", + "evaluation_basis": "observed_state", + "indeterminacy_reason": null, + "probe_binding": null, + "evidence_refs": [], + "declared_artifact_digest": null, + "temporal_context": null, + "loss_disclosures": [], + "unsupported_capability_refs": [] +} diff --git a/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/ambiguous-conflicting.json b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/ambiguous-conflicting.json new file mode 100644 index 000000000..29ec9c0c8 --- /dev/null +++ b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/ambiguous-conflicting.json @@ -0,0 +1,40 @@ +{ + "schema_version": "proposition-truth-result/v1", + "result_id": "truth-run-42-service-ambiguous", + "proposition_address": "evaluation.proposition.service-available", + "assertion_address": "evaluation.assertion.service-ready", + "assertion_polarity": "positive", + "proposition_outcome": "unknown", + "assertion_outcome": "unknown", + "evaluation_basis": "observed_state", + "indeterminacy_reason": "conflicting_evidence", + "probe_binding": { + "binding_id": "http-probe-binding", + "implementation_id": "example/http-health-probe", + "implementation_version": "2.1.0", + "artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "backend_manifest_ref": "backend-manifest:range-a", + "proposition_address": "evaluation.proposition.service-available", + "capability_refs": [ + "predicate.string.equals", + "evidence.log" + ] + }, + "evidence_refs": [ + "experiment-evidence:run-42/http-health-a", + "experiment-evidence:run-42/http-health-b" + ], + "declared_artifact_digest": null, + "temporal_context": { + "boundary_ref": "objectives.restore-service:completion", + "time_domain": "scenario_time", + "clock_authority": "experiment-clock:run-42" + }, + "loss_disclosures": [ + { + "kind": "conflicting", + "within_admissible_bound": false + } + ], + "unsupported_capability_refs": [] +} diff --git a/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/equivalent-range-b.json b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/equivalent-range-b.json new file mode 100644 index 000000000..ccea1cbec --- /dev/null +++ b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/equivalent-range-b.json @@ -0,0 +1,34 @@ +{ + "schema_version": "proposition-truth-result/v1", + "result_id": "truth-run-42-service-ready-range-b", + "proposition_address": "evaluation.proposition.service-available", + "assertion_address": "evaluation.assertion.service-ready", + "assertion_polarity": "positive", + "proposition_outcome": "true", + "assertion_outcome": "true", + "evaluation_basis": "observed_state", + "indeterminacy_reason": null, + "probe_binding": { + "binding_id": "agentless-api-binding", + "implementation_id": "example/range-api-observer", + "implementation_version": "7.0.3", + "artifact_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "backend_manifest_ref": "backend-manifest:range-b", + "proposition_address": "evaluation.proposition.service-available", + "capability_refs": [ + "predicate.string.equals", + "evidence.api-response" + ] + }, + "evidence_refs": [ + "experiment-evidence:run-42/range-api-health" + ], + "declared_artifact_digest": null, + "temporal_context": { + "boundary_ref": "objectives.restore-service:completion", + "time_domain": "scenario_time", + "clock_authority": "experiment-clock:run-42" + }, + "loss_disclosures": [], + "unsupported_capability_refs": [] +} diff --git a/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/negative-observed.json b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/negative-observed.json new file mode 100644 index 000000000..584bf7e48 --- /dev/null +++ b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/negative-observed.json @@ -0,0 +1,34 @@ +{ + "schema_version": "proposition-truth-result/v1", + "result_id": "truth-run-42-service-unavailable", + "proposition_address": "evaluation.proposition.service-available", + "assertion_address": "evaluation.assertion.service-unavailable", + "assertion_polarity": "negative", + "proposition_outcome": "false", + "assertion_outcome": "true", + "evaluation_basis": "observed_state", + "indeterminacy_reason": null, + "probe_binding": { + "binding_id": "http-probe-binding", + "implementation_id": "example/http-health-probe", + "implementation_version": "2.1.0", + "artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "backend_manifest_ref": "backend-manifest:range-a", + "proposition_address": "evaluation.proposition.service-available", + "capability_refs": [ + "predicate.string.equals", + "evidence.log" + ] + }, + "evidence_refs": [ + "experiment-evidence:run-42/http-health" + ], + "declared_artifact_digest": null, + "temporal_context": { + "boundary_ref": "objectives.disrupt-service:completion", + "time_domain": "scenario_time", + "clock_authority": "experiment-clock:run-42" + }, + "loss_disclosures": [], + "unsupported_capability_refs": [] +} diff --git a/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/positive-observed.json b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/positive-observed.json new file mode 100644 index 000000000..b57e91275 --- /dev/null +++ b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/positive-observed.json @@ -0,0 +1,34 @@ +{ + "schema_version": "proposition-truth-result/v1", + "result_id": "truth-run-42-service-ready", + "proposition_address": "evaluation.proposition.service-available", + "assertion_address": "evaluation.assertion.service-ready", + "assertion_polarity": "positive", + "proposition_outcome": "true", + "assertion_outcome": "true", + "evaluation_basis": "observed_state", + "indeterminacy_reason": null, + "probe_binding": { + "binding_id": "http-probe-binding", + "implementation_id": "example/http-health-probe", + "implementation_version": "2.1.0", + "artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "backend_manifest_ref": "backend-manifest:range-a", + "proposition_address": "evaluation.proposition.service-available", + "capability_refs": [ + "predicate.string.equals", + "evidence.log" + ] + }, + "evidence_refs": [ + "experiment-evidence:run-42/http-health" + ], + "declared_artifact_digest": null, + "temporal_context": { + "boundary_ref": "objectives.restore-service:completion", + "time_domain": "scenario_time", + "clock_authority": "experiment-clock:run-42" + }, + "loss_disclosures": [], + "unsupported_capability_refs": [] +} diff --git a/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/unsupported.json b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/unsupported.json new file mode 100644 index 000000000..9f496af6d --- /dev/null +++ b/contracts/fixtures/control-plane/proposition-truth-result-v1/valid/unsupported.json @@ -0,0 +1,19 @@ +{ + "schema_version": "proposition-truth-result/v1", + "result_id": "truth-run-42-service-unsupported", + "proposition_address": "evaluation.proposition.packet-loss-bounded", + "assertion_address": "evaluation.assertion.packet-loss-acceptable", + "assertion_polarity": "positive", + "proposition_outcome": "unsupported", + "assertion_outcome": "unsupported", + "evaluation_basis": "observed_state", + "indeterminacy_reason": null, + "probe_binding": null, + "evidence_refs": [], + "declared_artifact_digest": null, + "temporal_context": null, + "loss_disclosures": [], + "unsupported_capability_refs": [ + "predicate.number.less_than_or_equal" + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-authoring-input-v1/invalid/run-plan-both-allocation-and-count.json b/contracts/fixtures/experiment-core/experiment-authoring-input-v1/invalid/run-plan-both-allocation-and-count.json new file mode 100644 index 000000000..4065f901f --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-authoring-input-v1/invalid/run-plan-both-allocation-and-count.json @@ -0,0 +1,65 @@ +{ + "schema_version": "experiment-authoring-input/v1", + "spec_id": "spec-invalid-both-run-count-v1", + "spec_version": "1.0.0", + "title": "Invalid: run plan declares both allocation and target_run_count", + "description": "A run plan must declare exactly one run-count source; this one declares both, so it must fail schema and model validation.", + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "run_plan": { + "stochastic_controls": [ + { + "control_id": "episode-seed", + "role": "seed", + "value": 123456 + } + ], + "episode_control": { + "turn_order": "sequential", + "max_steps": 100, + "termination_rule": "Terminate each episode after 100 logical steps (fixed horizon)." + }, + "target_run_count": 100, + "allocation": { + "allocation_unit": "run", + "allocation_method": "balanced", + "compared_conditions": [ + "cond-aggressive", + "cond-stealthy" + ], + "condition_assignments": { + "cond-aggressive": { + "condition_id": "cond-aggressive", + "factor_levels": { + "red-tactic": "aggressive" + }, + "required_parameters": [ + { + "name": "red_tactic", + "value": "aggressive", + "value_kind": "protocol" + } + ] + }, + "cond-stealthy": { + "condition_id": "cond-stealthy", + "factor_levels": { + "red-tactic": "stealthy" + }, + "required_parameters": [ + { + "name": "red_tactic", + "value": "stealthy", + "value_kind": "protocol" + } + ] + } + }, + "target_runs_per_condition": 100, + "replication_policy": "100 independent seeded runs per red-tactic condition." + } + } +} diff --git a/contracts/fixtures/experiment-core/experiment-authoring-input-v1/invalid/run-plan-neither-allocation-nor-count.json b/contracts/fixtures/experiment-core/experiment-authoring-input-v1/invalid/run-plan-neither-allocation-nor-count.json new file mode 100644 index 000000000..69a9c7391 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-authoring-input-v1/invalid/run-plan-neither-allocation-nor-count.json @@ -0,0 +1,26 @@ +{ + "schema_version": "experiment-authoring-input/v1", + "spec_id": "spec-invalid-neither-run-count-v1", + "spec_version": "1.0.0", + "title": "Invalid: run plan declares neither allocation nor target_run_count", + "description": "A run plan must declare exactly one run-count source; this one declares neither, so it must fail schema and model validation.", + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "run_plan": { + "stochastic_controls": [ + { + "control_id": "episode-seed", + "role": "seed", + "value": 123456 + } + ], + "episode_control": { + "turn_order": "sequential", + "max_steps": 100, + "termination_rule": "Terminate each episode after 100 logical steps (fixed horizon)." + } + } +} diff --git a/contracts/fixtures/experiment-core/experiment-authoring-input-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-authoring-input-v1/valid/reference.json new file mode 100644 index 000000000..442c57aae --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-authoring-input-v1/valid/reference.json @@ -0,0 +1,123 @@ +{ + "schema_version": "experiment-authoring-input/v1", + "spec_id": "spec-techvault-red-tactic-sweep-v1", + "spec_version": "1.0.0", + "title": "TechVault red-tactic sweep", + "description": "Pre-run design comparing aggressive vs stealthy red tactics over the fixed TechVault task, with seeded replication.", + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "intended_scenario_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "apparatus_intent": { + "required_capabilities": [ + "workflow-results", + "evaluation-results" + ], + "notes": [ + "Any backend that realizes evaluation-results may execute this design." + ] + }, + "run_plan": { + "stochastic_controls": [ + { + "control_id": "episode-seed", + "role": "seed", + "value": 123456, + "description": "Base RNG seed; per-run seeds derived deterministically for replication." + } + ], + "episode_control": { + "turn_order": "sequential", + "max_steps": 100, + "termination_rule": "Terminate each episode after 100 logical steps (fixed horizon).", + "description": "Fixed-horizon episode with sequential turns." + }, + "allocation": { + "allocation_unit": "run", + "allocation_method": "balanced", + "compared_conditions": [ + "cond-aggressive", + "cond-stealthy" + ], + "condition_assignments": { + "cond-aggressive": { + "condition_id": "cond-aggressive", + "factor_levels": { + "red-tactic": "aggressive" + }, + "required_parameters": [ + { + "name": "red_tactic", + "value": "aggressive", + "value_kind": "protocol" + } + ] + }, + "cond-stealthy": { + "condition_id": "cond-stealthy", + "factor_levels": { + "red-tactic": "stealthy" + }, + "required_parameters": [ + { + "name": "red_tactic", + "value": "stealthy", + "value_kind": "protocol" + } + ] + } + }, + "target_runs_per_condition": 100, + "randomization_unit": "run", + "replication_policy": "100 independent seeded runs per red-tactic condition." + }, + "red_variant_selections": { + "aggressive": { + "variant_id": "aggressive", + "agent_ref": "red-agent", + "description": "Aggressive red tactic: direct exploitation path." + }, + "stealthy": { + "variant_id": "stealthy", + "agent_ref": "red-agent", + "description": "Stealthy red tactic: low-and-slow exploration." + } + }, + "clock_intent": { + "clock_id": "sim-clock", + "authority": "reference-backend", + "time_domain": "simulated" + } + }, + "factors": { + "red-tactic": { + "name": "Red tactic profile", + "factor_kind": "treatment", + "levels": [ + "aggressive", + "stealthy" + ] + } + }, + "capture_spec_refs": [ + { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + } + ], + "validity_notes": [ + { + "category": "internal", + "note": "Per-run seeds derive deterministically from the base seed.", + "mitigation": "Record derived seeds in each run's stochastic controls." + } + ] +} diff --git a/contracts/fixtures/profiles/scientific-completeness-assessment-v1/invalid/external-without-binding.json b/contracts/fixtures/profiles/scientific-completeness-assessment-v1/invalid/external-without-binding.json new file mode 100644 index 000000000..95c25aeb6 --- /dev/null +++ b/contracts/fixtures/profiles/scientific-completeness-assessment-v1/invalid/external-without-binding.json @@ -0,0 +1,17 @@ +{ + "schema_version": "scientific-completeness-assessment/v1", + "profile_family": "scientific-scenario-completeness", + "taxonomy_revision": "rev1", + "assessment_revision": "2026-07-12", + "assessed_on": "2026-07-12", + "concerns": [ + { + "concern_id": "factor-declaration", + "status": "external-contract", + "evidence": [], + "limitation": "Owned outside SDL.", + "issue_refs": [], + "external_contract_refs": [] + } + ] +} diff --git a/contracts/fixtures/profiles/scientific-completeness-assessment-v1/valid/minimal.json b/contracts/fixtures/profiles/scientific-completeness-assessment-v1/valid/minimal.json new file mode 100644 index 000000000..170a0e998 --- /dev/null +++ b/contracts/fixtures/profiles/scientific-completeness-assessment-v1/valid/minimal.json @@ -0,0 +1,23 @@ +{ + "schema_version": "scientific-completeness-assessment/v1", + "profile_family": "scientific-scenario-completeness", + "taxonomy_revision": "rev1", + "assessment_revision": "2026-07-12", + "assessed_on": "2026-07-12", + "concerns": [ + { + "concern_id": "source-profile-validity", + "status": "implemented", + "evidence": [ + { + "kind": "test", + "path": "implementations/python/tests/test_sdl_parser.py", + "claim": "Source-profile rejection coverage." + } + ], + "limitation": "Does not establish scientific adequacy.", + "issue_refs": [], + "external_contract_refs": [] + } + ] +} diff --git a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/invalid/missing-disposition.json b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/invalid/missing-disposition.json new file mode 100644 index 000000000..b1eb980f9 --- /dev/null +++ b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/invalid/missing-disposition.json @@ -0,0 +1,23 @@ +{ + "schema_version": "scientific-completeness-taxonomy/v1", + "profile_family": "scientific-scenario-completeness", + "revision": "rev1", + "concerns": [ + { + "concern_id": "source-profile-validity", + "title": "Source profile validity", + "definition": "The source obeys a governed profile.", + "semantic_owner": "SDL parser" + } + ], + "profiles": [ + { + "profile_id": "valid-sdl-fragment", + "title": "Valid SDL fragment", + "intended_claim": "The source is admitted as SDL.", + "explicit_non_claims": ["No deployability claim."], + "dispositions": {}, + "example_refs": [] + } + ] +} diff --git a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json new file mode 100644 index 000000000..9b98e2486 --- /dev/null +++ b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json @@ -0,0 +1,23 @@ +{ + "schema_version": "scientific-completeness-taxonomy/v1", + "profile_family": "scientific-scenario-completeness", + "revision": "rev1", + "concerns": [ + { + "concern_id": "source-profile-validity", + "title": "Source profile validity", + "definition": "The source obeys a governed profile.", + "semantic_owner": "SDL parser" + } + ], + "profiles": [ + { + "profile_id": "valid-sdl-fragment", + "title": "Valid SDL fragment", + "intended_claim": "The source is admitted as SDL.", + "explicit_non_claims": ["No deployability or scientific-adequacy claim."], + "dispositions": {"source-profile-validity": "required"}, + "example_refs": [] + } + ] +} diff --git a/contracts/fixtures/provenance/sdl-lineage-ledger-v1/invalid/unpinned-git-source.json b/contracts/fixtures/provenance/sdl-lineage-ledger-v1/invalid/unpinned-git-source.json new file mode 100644 index 000000000..2b70df36f --- /dev/null +++ b/contracts/fixtures/provenance/sdl-lineage-ledger-v1/invalid/unpinned-git-source.json @@ -0,0 +1,28 @@ +{ + "schema_version": "sdl-lineage-ledger/v1", + "reviewed_on": "2026-07-12", + "citations": [ + { + "citation_id": "unpinned", + "title": "Unpinned Source", + "authors_or_maintainer": ["Example Maintainer"], + "year": 2026, + "canonical_url": "https://example.org/source", + "verified_on": "2026-07-12", + "verification_evidence": "docs/example.md" + } + ], + "sources": [ + { + "source_id": "unpinned", + "kind": "git", + "title": "Unpinned Source", + "version_or_edition": "main", + "canonical_url": "https://example.org/source", + "repository_url": "https://example.org/source", + "citation_ref": "unpinned" + } + ], + "subjects": [], + "third_party_dispositions": [] +} diff --git a/contracts/fixtures/provenance/sdl-lineage-ledger-v1/valid/minimal.json b/contracts/fixtures/provenance/sdl-lineage-ledger-v1/valid/minimal.json new file mode 100644 index 000000000..a7867e637 --- /dev/null +++ b/contracts/fixtures/provenance/sdl-lineage-ledger-v1/valid/minimal.json @@ -0,0 +1,61 @@ +{ + "schema_version": "sdl-lineage-ledger/v1", + "reviewed_on": "2026-07-12", + "citations": [ + { + "citation_id": "example-publication", + "title": "Example Publication", + "authors_or_maintainer": [ + "Example Author" + ], + "year": 2026, + "canonical_url": "https://example.org/publication", + "verified_on": "2026-07-12", + "verification_evidence": "docs/example.md" + } + ], + "sources": [ + { + "source_id": "example-publication", + "kind": "publication", + "title": "Example Publication", + "version_or_edition": "2026 edition", + "canonical_url": "https://example.org/publication", + "citation_ref": "example-publication" + } + ], + "subjects": [ + { + "subject_id": "sdl-field:example", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/example.json", + "pointer": "#/properties/example", + "contract_id": "example-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/example.json", + "symbol_or_pointer": "#/properties/example" + } + ], + "source_boundaries": [], + "divergence": "Defined by the example contract.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "ADR-000" + ], + "compatibility_direction": "not_applicable" + } + ] + } + ], + "third_party_dispositions": [] +} diff --git a/contracts/fixtures/realization-envelope/realization-envelope-v1/invalid/missing-digest.json b/contracts/fixtures/realization-envelope/realization-envelope-v1/invalid/missing-digest.json new file mode 100644 index 000000000..c30d298a6 --- /dev/null +++ b/contracts/fixtures/realization-envelope/realization-envelope-v1/invalid/missing-digest.json @@ -0,0 +1,127 @@ +{ + "schema_version": "realization-envelope/v1", + "contract_id": "realization-envelope-v1", + "id": "libvirt-qemu.generic.v1", + "expression": { + "schema_version": "realization-envelope/v1", + "id": "libvirt-qemu.generic.expression.v1", + "scope": "scenario", + "domains": {}, + "bindings": [], + "closure": [] + }, + "configuration": { + "mode": "generic", + "configuration_digest": "sha256:2af0fbd4a95b95a3a51d1488f641985c93e4c470baeb188f3e981f8b36327450", + "architecture": "x86_64", + "image_policy": "local-qcow2", + "network_policy": "libvirt-managed", + "supported_node_types": [ + "switch", + "vm" + ], + "supported_os_families": [ + "linux" + ], + "supported_content_types": [ + "file" + ], + "supported_account_features": [ + "auth_method", + "disabled", + "groups", + "home", + "shell" + ], + "supports_acls": true, + "memory_mib": { + "minimum": 128, + "maximum": null + }, + "vcpus": { + "minimum": 1, + "maximum": null + } + }, + "concerns": [ + { + "concern": "topology", + "disposition": "realized", + "observation_strength": "driver-reported", + "mechanism": "libvirt-domain-network", + "transformations": [] + }, + { + "concern": "architecture", + "disposition": "realized", + "observation_strength": "driver-reported", + "mechanism": "qemu-x86_64-domain", + "transformations": [] + }, + { + "concern": "image", + "disposition": "realized", + "observation_strength": "driver-reported", + "mechanism": "qcow2-disk-attachment", + "transformations": [] + }, + { + "concern": "resource-allocation", + "disposition": "transformed", + "observation_strength": "driver-reported", + "mechanism": "libvirt-domain-xml", + "transformations": [ + "bounded-normalization", + "default-substitution" + ] + }, + { + "concern": "network", + "disposition": "transformed", + "observation_strength": "driver-reported", + "mechanism": "libvirt-network-xml", + "transformations": [ + "default-substitution" + ] + }, + { + "concern": "content-placement", + "disposition": "realized", + "observation_strength": "driver-reported", + "mechanism": "cloud-init-seed", + "transformations": [] + }, + { + "concern": "account-placement", + "disposition": "transformed", + "observation_strength": "driver-reported", + "mechanism": "cloud-init-seed", + "transformations": [ + "default-substitution" + ] + }, + { + "concern": "feature-binding", + "disposition": "transformed", + "observation_strength": "driver-reported", + "mechanism": "cloud-init-seed", + "transformations": [ + "descriptor-substitution" + ] + }, + { + "concern": "service", + "disposition": "unsupported", + "observation_strength": "none", + "mechanism": null, + "transformations": [] + }, + { + "concern": "acl", + "disposition": "realized", + "observation_strength": "driver-reported", + "mechanism": "libvirt-nwfilter", + "transformations": [] + } + ] +} diff --git a/contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json b/contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json new file mode 100644 index 000000000..dcea4855e --- /dev/null +++ b/contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json @@ -0,0 +1,40 @@ +{ + "schema_version": "realization-envelope/v1", + "contract_id": "realization-envelope-v1", + "id": "libvirt-qemu.generic.v1", + "expression": { + "schema_version": "realization-envelope/v1", + "id": "libvirt-qemu.generic.expression.v1", + "scope": "scenario", + "domains": {}, + "bindings": [], + "closure": [] + }, + "configuration": { + "mode": "generic", + "configuration_digest": "sha256:2af0fbd4a95b95a3a51d1488f641985c93e4c470baeb188f3e981f8b36327450", + "architecture": "x86_64", + "image_policy": "local-qcow2", + "network_policy": "libvirt-managed", + "supported_node_types": ["switch", "vm"], + "supported_os_families": ["linux"], + "supported_content_types": ["file"], + "supported_account_features": ["auth_method", "disabled", "groups", "home", "shell"], + "supports_acls": true, + "memory_mib": {"minimum": 128, "maximum": null}, + "vcpus": {"minimum": 1, "maximum": null} + }, + "concerns": [ + {"concern": "topology", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-domain-network", "transformations": []}, + {"concern": "architecture", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qemu-x86_64-domain", "transformations": []}, + {"concern": "image", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qcow2-disk-attachment", "transformations": []}, + {"concern": "resource-allocation", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "libvirt-domain-xml", "transformations": ["bounded-normalization", "default-substitution"]}, + {"concern": "network", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "libvirt-network-xml", "transformations": ["default-substitution"]}, + {"concern": "content-placement", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "cloud-init-seed", "transformations": []}, + {"concern": "account-placement", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "cloud-init-seed", "transformations": ["default-substitution"]}, + {"concern": "feature-binding", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "cloud-init-seed", "transformations": ["descriptor-substitution"]}, + {"concern": "service", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, + {"concern": "acl", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-nwfilter", "transformations": []} + ], + "digest": "sha256:eb3b54b199249da599cdd59e050612314ed16fe6039630307894ee8d8e2a466f" +} 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 new file mode 100644 index 000000000..4161dcd27 --- /dev/null +++ b/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/invalid/wrong-profile.json @@ -0,0 +1,13 @@ +{ + "profile": "aces-sdl-semantic/v1", + "scenario": { + "name": "concrete-scenario", + "instantiation_provenance": { + "authored_digest": { + "profile": "aces-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 new file mode 100644 index 000000000..81e797097 --- /dev/null +++ b/contracts/fixtures/sdl/instantiated-scenario-snapshot-v1/valid/minimal.json @@ -0,0 +1,13 @@ +{ + "profile": "aces-sdl-instantiated-snapshot/v1", + "scenario": { + "name": "concrete-scenario", + "instantiation_provenance": { + "authored_digest": { + "profile": "aces-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 new file mode 100644 index 000000000..ec47d5b4b --- /dev/null +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/imports-empty.json @@ -0,0 +1,11 @@ +{ + "name": "forbidden-imports", + "imports": [], + "instantiation_provenance": { + "authored_digest": { + "profile": "aces-sdl-semantic/v1", + "algorithm": "sha256", + "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } +} +} diff --git a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/missing-provenance.json b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/missing-provenance.json new file mode 100644 index 000000000..68e7e16bc --- /dev/null +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/missing-provenance.json @@ -0,0 +1,3 @@ +{ + "name": "missing-provenance" +} diff --git a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/module-null.json b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/module-null.json new file mode 100644 index 000000000..9fd9fccdb --- /dev/null +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/module-null.json @@ -0,0 +1,11 @@ +{ + "name": "forbidden-module", + "module": null, + "instantiation_provenance": { + "authored_digest": { + "profile": "aces-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 2a1aeea64..6e0e019e6 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/unresolved-variable.json +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/unresolved-variable.json @@ -1,9 +1,11 @@ { "name": "concrete-scenario", "description": "deploy ${region} cluster", - "infrastructure": { - "net": { - "count": "${replicas}" - } + "instantiation_provenance": { + "authored_digest": { + "profile": "aces-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 new file mode 100644 index 000000000..8154bb14d --- /dev/null +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/invalid/variables-empty.json @@ -0,0 +1,11 @@ +{ + "name": "forbidden-variables", + "variables": {}, + "instantiation_provenance": { + "authored_digest": { + "profile": "aces-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 bc5f02939..ff6b3ad2c 100644 --- a/contracts/fixtures/sdl/instantiated-scenario-v1/valid/minimal.json +++ b/contracts/fixtures/sdl/instantiated-scenario-v1/valid/minimal.json @@ -1,4 +1,11 @@ { "name": "concrete-scenario", - "description": "a fully concrete instantiated scenario with no unresolved references" + "description": "a fully concrete instantiated scenario with no unresolved references", + "instantiation_provenance": { + "authored_digest": { + "profile": "aces-sdl-semantic/v1", + "algorithm": "sha256", + "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } +} } diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/README.md b/contracts/fixtures/sdl/sdl-yaml-v1/README.md new file mode 100644 index 000000000..ab92e07be --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/README.md @@ -0,0 +1,14 @@ +# `sdl-yaml/v1` Conformance Corpus + +This directory is the normative example corpus for the raw SDL YAML source +profile defined by `specs/sdl/document-model.md`. + +- `valid/` documents must pass strict source decoding. +- `invalid/` documents must fail strict source decoding. +- `migration/` documents must fail strict decoding, then pass only when an + explicit migration policy is selected and must produce at least one + source-ranged warning. + +These files test YAML presentation rules that JSON Schema cannot express. The +separate `contracts/schemas/sdl/sdl-authoring-input-v1.json` artifact validates +the normalized authoring object after decoding and typed normalization. diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/alias-cycle.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/alias-cycle.yaml new file mode 100644 index 000000000..a63d1eb52 --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/alias-cycle.yaml @@ -0,0 +1,5 @@ +name: alias-cycle +nodes: &nodes + switch: + type: switch + roles: *nodes diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/directive.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/directive.yaml new file mode 100644 index 000000000..d63cd21c8 --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/directive.yaml @@ -0,0 +1,3 @@ +%YAML 1.2 +--- +name: directive diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/duplicate-field.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/duplicate-field.yaml new file mode 100644 index 000000000..b9c430b9e --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/duplicate-field.yaml @@ -0,0 +1,2 @@ +name: first +name: second diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/explicit-tag.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/explicit-tag.yaml new file mode 100644 index 000000000..25242d72b --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/explicit-tag.yaml @@ -0,0 +1 @@ +name: !!str explicit-tag diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/merge-conflict.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/merge-conflict.yaml new file mode 100644 index 000000000..1ae5494a4 --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/merge-conflict.yaml @@ -0,0 +1,7 @@ +name: merge-conflict +nodes: + base: &base + type: switch + conflict: + <<: *base + type: vm diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/multiple-documents.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/multiple-documents.yaml new file mode 100644 index 000000000..f1fcefa34 --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/multiple-documents.yaml @@ -0,0 +1,4 @@ +--- +name: first +--- +name: second diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/non-string-key.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/non-string-key.yaml new file mode 100644 index 000000000..d39601317 --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/non-string-key.yaml @@ -0,0 +1,3 @@ +name: non-string-key +nodes: + true: {type: switch} diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/noncanonical-field.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/noncanonical-field.yaml new file mode 100644 index 000000000..876539aa9 --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/noncanonical-field.yaml @@ -0,0 +1 @@ +Name: noncanonical-field diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/invalid/nonfinite.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/nonfinite.yaml new file mode 100644 index 000000000..4ea5d9044 --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/invalid/nonfinite.yaml @@ -0,0 +1,2 @@ +name: nonfinite +description: .inf diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/migration/field-alias.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/migration/field-alias.yaml new file mode 100644 index 000000000..fc988923d --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/migration/field-alias.yaml @@ -0,0 +1 @@ +Name: migration-alias diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/migration/merge-key.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/migration/merge-key.yaml new file mode 100644 index 000000000..203aebd0b --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/migration/merge-key.yaml @@ -0,0 +1,6 @@ +name: migration-merge +nodes: + shared: &shared + type: switch + reuse: + <<: *shared diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/valid/anchors.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/valid/anchors.yaml new file mode 100644 index 000000000..7406a597c --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/valid/anchors.yaml @@ -0,0 +1,5 @@ +name: canonical-anchors +nodes: + shared: &shared + type: switch + reuse: *shared diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/valid/core-scalars.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/valid/core-scalars.yaml new file mode 100644 index 000000000..13084a3df --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/valid/core-scalars.yaml @@ -0,0 +1,5 @@ +name: core-scalars +description: yes +nodes: + on: + type: switch diff --git a/contracts/fixtures/sdl/sdl-yaml-v1/valid/minimal.yaml b/contracts/fixtures/sdl/sdl-yaml-v1/valid/minimal.yaml new file mode 100644 index 000000000..bb4f040ea --- /dev/null +++ b/contracts/fixtures/sdl/sdl-yaml-v1/valid/minimal.yaml @@ -0,0 +1 @@ +name: canonical-minimal diff --git a/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json b/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json index 53a007ed0..f07ed7e44 100644 --- a/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json +++ b/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/reference.json @@ -98,7 +98,7 @@ "finish": [] }, "join_owners": {}, - "step_condition_addresses": {}, + "step_assertion_addresses": {}, "step_predicate_addresses": {}, "required_features": [], "required_state_predicate_features": [], @@ -161,18 +161,15 @@ "targets": [], "success": { "mode": "all_of", - "conditions": ["health"], - "metrics": [], - "evaluations": [], - "tlos": [], - "goals": [] + "assertions": ["health"], + "threshold": null }, "window": null, "depends_on": [] }, "actor_type": "entity", "actor_name": "blue", - "success_addresses": ["evaluation.condition.vm.health"], + "success_addresses": ["evaluation.assertion.health"], "objective_dependencies": [], "window_story_addresses": [], "window_script_addresses": [], @@ -196,9 +193,58 @@ "requires_start_event": true } }, - "ordering_dependencies": ["evaluation.condition.vm.health"], - "refresh_dependencies": ["evaluation.condition.vm.health"], + "ordering_dependencies": ["evaluation.assertion.health"], + "refresh_dependencies": ["evaluation.assertion.health"], "status": "scheduled" + }, + "evaluation.proposition.health": { + "address": "evaluation.proposition.health", + "domain": "evaluation", + "resource_type": "proposition", + "payload": { + "name": "health", + "spec": { + "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" + }, + "quantifier": "all", + "threshold": null, + "evidence_requirements": [] + }, + "subject_addresses": ["provision.node.vm"], + "predicate_kind": "presence", + "evaluation_basis": "declared_state", + "evidence_requirement_refs": [] + }, + "ordering_dependencies": [], + "refresh_dependencies": [], + "status": "admitted" + }, + "evaluation.assertion.health": { + "address": "evaluation.assertion.health", + "domain": "evaluation", + "resource_type": "assertion", + "payload": { + "name": "health", + "spec": { + "description": "", + "proposition": "health", + "role": "postcondition", + "polarity": "positive" + }, + "proposition_address": "evaluation.proposition.health", + "role": "postcondition", + "polarity": "positive" + }, + "ordering_dependencies": ["evaluation.proposition.health"], + "refresh_dependencies": ["evaluation.proposition.health"], + "status": "admitted" } }, "orchestration_results": { @@ -276,5 +322,6 @@ } ] }, + "proposition_truth_results": {}, "metadata": {} } diff --git a/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json b/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json new file mode 100644 index 000000000..2b1918d20 --- /dev/null +++ b/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json @@ -0,0 +1,238 @@ +{ + "schema_version": "scientific-completeness-assessment/v1", + "profile_family": "scientific-scenario-completeness", + "taxonomy_revision": "rev1", + "assessment_revision": "2026-07-12", + "assessed_on": "2026-07-12", + "concerns": [ + { + "concern_id": "source-profile-validity", "status": "implemented", "limitation": "Admission proves the governed source profile, not scientific adequacy.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/_yaml_loader.py", "claim": "Safe YAML ingress and source-profile enforcement."}, + {"kind": "test", "path": "implementations/python/tests/test_sdl_parser.py", "claim": "Source-profile and parser rejection coverage."} + ] + }, + { + "concern_id": "closed-structural-model", "status": "implemented", "limitation": "Closed structure permits a name-only fragment and is not a completeness claim.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/scenario.py", "claim": "Closed Scenario model."}, + {"kind": "test", "path": "implementations/python/tests/test_sdl_models.py", "claim": "Closed-model validation coverage."} + ] + }, + { + "concern_id": "semantic-reference-integrity", "status": "implemented", "limitation": "Semantic admission covers declared SDL references, not backend realization.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/validator/_core.py", "claim": "Collect-all semantic validation."}, + {"kind": "test", "path": "implementations/python/tests/test_sdl_validator.py", "claim": "Cross-section semantic validation coverage."} + ] + }, + { + "concern_id": "topology-and-resource-intent", "status": "implemented", "limitation": "The SDL expresses intent; realizability remains apparatus and backend relative.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/scenario.py", "claim": "Typed scenario topology and resource surfaces."}, + {"kind": "test", "path": "implementations/python/tests/test_sdl_realworld.py", "claim": "Real-world scenario authoring coverage."} + ] + }, + { + "concern_id": "authored-observed-state-separation", "status": "partial", "limitation": "Plane separation exists across contracts, but the SDL boundary audit and authored-to-observed binding remain open.", "issue_refs": ["#417"], + "evidence": [{"kind": "normative-spec", "path": "specs/sdl/observability-and-evidence.md", "claim": "Defines authored, observed, and evidence planes."}] + }, + { + "concern_id": "scoped-specificity-open-world-intent", "status": "partial", "limitation": "Realization envelopes and explicitness semantics exist, but scenario-scoped default and most-specific override authority remain open.", "issue_refs": ["#539"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_sdl/explicitness.py", "claim": "Current explicitness analysis."}] + }, + { + "concern_id": "parameter-typing", "status": "implemented", "limitation": "SDL variables instantiate scenario values; they do not define an experimental factor design.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/instantiate.py", "claim": "Typed variable binding and instantiation."}, + {"kind": "test", "path": "implementations/python/tests/test_sdl_phase_contracts.py", "claim": "Instantiation and closed-phase coverage."} + ] + }, + { + "concern_id": "factor-declaration", "status": "external-contract", "limitation": "Factors belong to experiment authoring, not Scenario.", + "external_contract_refs": ["experiment-authoring-input-v1"], "satisfiability_witness_refs": {"experiment-authoring-input-v1": "contracts/fixtures/experiment-core/experiment-authoring-input-v1/valid/reference.json"}, "binding_obligation": "The completeness bundle must bind the instantiated scenario identity to an experiment authoring input whose declared factors govern the run plan.", + "evidence": [{"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-authoring-input-v1.json", "contract_id": "experiment-authoring-input-v1", "claim": "Published experiment factor and level contract."}] + }, + { + "concern_id": "controlled-allocation", "status": "external-contract", "limitation": "Allocation is an experiment/study concern and does not alter SDL meaning.", + "external_contract_refs": ["experiment-authoring-input-v1", "experiment-study-v1"], "satisfiability_witness_refs": {"experiment-authoring-input-v1": "contracts/fixtures/experiment-core/experiment-authoring-input-v1/valid/reference.json", "experiment-study-v1": "contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json"}, "binding_obligation": "The authoring allocation must bind to study membership and condition assignments for the same scenario-derived run set.", + "evidence": [ + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-authoring-input-v1.json", "contract_id": "experiment-authoring-input-v1", "claim": "Pre-run allocation and replication plan."}, + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-study-v1.json", "contract_id": "experiment-study-v1", "claim": "Archived study allocation and membership coverage."} + ] + }, + { + "concern_id": "randomization-policy", "status": "external-contract", "limitation": "A policy reference records the mechanism; it does not prove unbiased execution.", + "external_contract_refs": ["experiment-authoring-input-v1"], "satisfiability_witness_refs": {"experiment-authoring-input-v1": "contracts/fixtures/experiment-core/experiment-authoring-input-v1/valid/reference.json"}, "binding_obligation": "The run plan must name the randomization control and preserve it into apparatus and run evidence.", + "evidence": [{"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-authoring-input-v1.json", "contract_id": "experiment-authoring-input-v1", "claim": "Pre-run stochastic and allocation controls."}] + }, + { + "concern_id": "seed-preservation", "status": "external-contract", "limitation": "Seed preservation supports reproduction attempts but is not a reproducibility proof.", + "external_contract_refs": ["experiment-apparatus-context-v1", "experiment-run-v1"], "satisfiability_witness_refs": {"experiment-apparatus-context-v1": "contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json", "experiment-run-v1": "contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json"}, "binding_obligation": "Every relevant pre-run stochastic control must resolve to the apparatus/run record or disclose why no seed applies.", + "evidence": [ + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-apparatus-context-v1.json", "contract_id": "experiment-apparatus-context-v1", "claim": "Apparatus stochastic-control preservation."}, + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-run-v1.json", "contract_id": "experiment-run-v1", "claim": "Run-level stochastic provenance."} + ] + }, + { + "concern_id": "propositions", "status": "implemented", "limitation": "The model defines claims and truth domains; it does not guarantee a backend can decide them.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/propositions.py", "claim": "Typed proposition model."}, + {"kind": "test", "path": "implementations/python/tests/test_proposition_semantics.py", "claim": "Predicate, subject, and truth-composition coverage."} + ] + }, + { + "concern_id": "assertions", "status": "implemented", "limitation": "Assertion composition is not evaluator lifecycle success or graded scoring.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/propositions.py", "claim": "Role-constrained assertion model."}, + {"kind": "test", "path": "implementations/python/tests/test_truth_result_contracts.py", "claim": "Assertion truth-result integrity coverage."} + ] + }, + { + "concern_id": "condition-probe-binding", "status": "implemented", "limitation": "Conditions are probe realizations and cannot substitute command success for truth.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/conditions.py", "claim": "Explicit condition-to-proposition binding."}, + {"kind": "test", "path": "implementations/python/tests/test_proposition_capabilities.py", "claim": "Backend binding capability and rejection coverage."} + ] + }, + { + "concern_id": "workflow-compensation", "status": "implemented", "limitation": "Compensation is bounded workflow behavior, not universal resource rollback.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/orchestration.py", "claim": "Workflow compensation surface."}, + {"kind": "test", "path": "implementations/python/tests/test_fm2_semantics.py", "claim": "Workflow semantic invariants."} + ] + }, + { + "concern_id": "backend-teardown-reconciliation", "status": "partial", "limitation": "Reference backends have teardown paths, but portable authored cleanup and cross-backend reconciliation guarantees are incomplete.", "issue_refs": ["#658"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_backend_libvirt/provisioner.py", "claim": "Libvirt teardown and reconciliation implementation."}] + }, + { + "concern_id": "manual-rollback", "status": "missing", "limitation": "No general authored manual or advanced rollback contract exists.", "issue_refs": ["#658"], "evidence": [] + }, + { + "concern_id": "time-domain-declaration", "status": "partial", "limitation": "Experiment and participant contracts carry bounded time context; the SDL time-domain authoring model remains incomplete.", "issue_refs": ["#283", "#286"], + "evidence": [{"kind": "normative-spec", "path": "specs/formal/participant-runtime/README.md", "claim": "Participant time and ordering model."}] + }, + { + "concern_id": "clock-declaration", "status": "partial", "limitation": "Clock context exists outside SDL, but portable clock identity/origin/rate authoring is incomplete.", "issue_refs": ["#283", "#291"], + "evidence": [{"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-apparatus-context-v1.json", "contract_id": "experiment-apparatus-context-v1", "claim": "Bounded experiment clock context."}] + }, + { + "concern_id": "temporal-ordering-causality", "status": "partial", "limitation": "Workflow and participant records preserve bounded order, but general causal and partial-order SDL semantics remain open.", "issue_refs": ["#285", "#288"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_contracts/participant_concurrency.py", "claim": "Participant concurrency and order invariants."}] + }, + { + "concern_id": "deadlines-and-windows", "status": "partial", "limitation": "Objective windows exist, but a complete time-domain-bound deadline surface is not published.", "issue_refs": ["#285", "#288"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_sdl/semantics/objectives.py", "claim": "Current objective-window analysis."}] + }, + { + "concern_id": "pacing-and-synchronization", "status": "partial", "limitation": "Participant time-management records exist; general advancement and synchronization semantics remain open.", "issue_refs": ["#284", "#287", "#290"], + "evidence": [{"kind": "contract", "path": "contracts/schemas/participant-runtime/participant-time-management-context-v1.json", "contract_id": "participant-time-management-context-v1", "claim": "Participant time-management disclosure contract."}] + }, + { + "concern_id": "participant-declaration", "status": "implemented", "limitation": "Participant declaration does not prove that an implementation respects every authority or observation boundary.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/agents.py", "claim": "Participant identity, scope, and boundary references."}, + {"kind": "test", "path": "implementations/python/tests/test_sem_208_participant_behavior.py", "claim": "Participant semantic validation coverage."} + ] + }, + { + "concern_id": "participant-action-observation", "status": "partial", "limitation": "Action contracts and observation boundaries exist, but participant-relative information-flow admission is not complete.", "issue_refs": ["#747"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_sdl/participant_behavior.py", "claim": "Current action-contract and observation-boundary models."}] + }, + { + "concern_id": "participant-episode-reset", "status": "partial", "limitation": "Runtime episode lifecycle is implemented, while complete authored episode/reset semantics and conformance remain open.", "issue_refs": ["#305", "#307", "#311", "#314"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_contracts/participant_episode.py", "claim": "Runtime episode lifecycle invariants."}] + }, + { + "concern_id": "participant-budgets", "status": "missing", "limitation": "Portable authored budget, exhaustion, and usage-accounting contracts are not complete.", "issue_refs": ["#306", "#308", "#312", "#313"], "evidence": [] + }, + { + "concern_id": "reference-trajectories", "status": "missing", "limitation": "Trajectory, demonstration, corpus, split, lineage, and replay-reference contracts remain planned.", "issue_refs": ["#316", "#317", "#319", "#321", "#322"], "evidence": [] + }, + { + "concern_id": "hidden-benchmark-assets", "status": "missing", "limitation": "Hidden targets and gold-standard asset references lack the required visibility and integrity contracts.", "issue_refs": ["#324", "#328", "#333"], "evidence": [] + }, + { + "concern_id": "verifier-and-adjudication", "status": "missing", "limitation": "Portable verifier, judge, annotation, disagreement, and adjudication contracts remain planned.", "issue_refs": ["#325", "#327", "#329"], "evidence": [] + }, + { + "concern_id": "graded-reward-in-sdl", "status": "deliberately-excluded", "limitation": "Reward and scoring belong to the experiment/evaluator plane rather than scenario truth.", + "exclusion_rationale": "ADR-073 removes the inherited scoring pipeline from SDL to prevent scenario meaning from becoming backend-specific grading logic.", + "evidence": [{"kind": "normative-spec", "path": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", "claim": "Accepted boundary excluding graded reward from SDL."}] + }, + { + "concern_id": "evaluation-outcome-evidence", "status": "external-contract", "limitation": "Outcome records preserve evidence and interpretation; they do not by themselves prove scientific validity.", + "external_contract_refs": ["experiment-evidence-record-v1", "experiment-derived-measure-v1", "experiment-run-v1"], "satisfiability_witness_refs": {"experiment-evidence-record-v1": "contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/reference.json", "experiment-derived-measure-v1": "contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json", "experiment-run-v1": "contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json"}, "binding_obligation": "Evaluation outcomes must bind to evidence, derived measures, the admitted scenario identity, and the same run provenance.", + "evidence": [ + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-evidence-record-v1.json", "contract_id": "experiment-evidence-record-v1", "claim": "Evidence record contract."}, + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-derived-measure-v1.json", "contract_id": "experiment-derived-measure-v1", "claim": "Derived measure contract."}, + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-run-v1.json", "contract_id": "experiment-run-v1", "claim": "Run identity and evidence binding."} + ] + }, + { + "concern_id": "credential-intent", "status": "partial", "limitation": "Account and runtime-value posture exists, but authored credential-bearing setup remains unresolved.", "issue_refs": ["#673", "#708"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_sdl/accounts.py", "claim": "Current account and credential-posture authoring."}] + }, + { + "concern_id": "credential-materialization", "status": "missing", "limitation": "No complete portable binding materializes authored credential-bearing accounts while preserving secret boundaries.", "issue_refs": ["#708"], "evidence": [] + }, + { + "concern_id": "host-architecture-constraints", "status": "missing", "limitation": "Node lacks a portable host CPU architecture constraint.", "issue_refs": ["#674"], "evidence": [] + }, + { + "concern_id": "substrate-constraints", "status": "external-contract", "limitation": "Substrate constraints are apparatus and realization requirements, not universal SDL fields.", + "external_contract_refs": ["experiment-apparatus-context-v1", "realization-envelope-v1"], "satisfiability_witness_refs": {"experiment-apparatus-context-v1": "contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json", "realization-envelope-v1": "contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json"}, "binding_obligation": "The scenario snapshot must bind to apparatus constraints and a backend realization envelope that discloses unsupported requirements.", + "evidence": [ + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-apparatus-context-v1.json", "contract_id": "experiment-apparatus-context-v1", "claim": "Apparatus constraint and realized context contract."}, + {"kind": "contract", "path": "contracts/schemas/realization-envelope/realization-envelope-v1.json", "contract_id": "realization-envelope-v1", "claim": "Backend-relative realizability boundary."} + ] + }, + { + "concern_id": "vulnerability-inventory", "status": "implemented", "limitation": "A declared vulnerability is not proof of reachability, exploitability, or observation.", + "evidence": [ + {"kind": "implementation", "path": "implementations/python/packages/aces_sdl/vulnerabilities.py", "claim": "Typed vulnerability inventory."}, + {"kind": "test", "path": "implementations/python/tests/test_sdl_models.py", "claim": "Vulnerability model validation coverage."} + ] + }, + { + "concern_id": "weakness-exploitability-semantics", "status": "partial", "limitation": "Governed identifiers exist, but weakness classification and exploitability/reachability semantics are not fully reconciled.", "issue_refs": ["#676"], + "evidence": [{"kind": "normative-spec", "path": "specs/sdl/sections.md", "claim": "Current vulnerability section semantics."}] + }, + { + "concern_id": "flexible-step-tooling", "status": "partial", "limitation": "Scripts and workflows exist, but portable tool selection and affordance binding remain incomplete.", "issue_refs": ["#653"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_sdl/orchestration.py", "claim": "Current script and workflow step surfaces."}] + }, + { + "concern_id": "portable-behavior-contracts", "status": "partial", "limitation": "Behavior specifications and action contracts exist, but portable participant affordance and realization coverage is incomplete.", "issue_refs": ["#653"], + "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_sdl/participant_behavior_specification.py", "claim": "Current behavior specification surface."}] + }, + { + "concern_id": "behavioral-relation-taxonomy", "status": "missing", "limitation": "ACES explicitly disclaims bisimulation today; the cross-relation taxonomy and conflation audit are tracked separately.", "issue_refs": ["#747"], + "evidence": [{"kind": "normative-spec", "path": "specs/formal/objectives/proposition-and-assertion-semantics.md", "claim": "Current explicit non-bisimulation boundary."}] + }, + { + "concern_id": "apparatus-constraints", "status": "external-contract", "limitation": "Apparatus constraints/context scope claims to a realization; they do not guarantee availability.", + "external_contract_refs": ["experiment-apparatus-context-v1"], "satisfiability_witness_refs": {"experiment-apparatus-context-v1": "contracts/fixtures/experiment-core/experiment-apparatus-context-v1/valid/reference.json"}, "binding_obligation": "The scenario and run must reference the same apparatus requirement and realized context identities.", + "evidence": [{"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-apparatus-context-v1.json", "contract_id": "experiment-apparatus-context-v1", "claim": "Published apparatus constraints and context."}] + }, + { + "concern_id": "run-plan", "status": "external-contract", "limitation": "Run planning is an experiment authoring concern and is not embedded in Scenario.", + "external_contract_refs": ["experiment-authoring-input-v1"], "satisfiability_witness_refs": {"experiment-authoring-input-v1": "contracts/fixtures/experiment-core/experiment-authoring-input-v1/valid/reference.json"}, "binding_obligation": "The run plan must bind its scenario input, factors, allocation, stochastic controls, and episode intent before execution.", + "evidence": [{"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-authoring-input-v1.json", "contract_id": "experiment-authoring-input-v1", "claim": "Published pre-run plan contract."}] + }, + { + "concern_id": "study-record", "status": "external-contract", "limitation": "A conforming study record preserves design and membership but does not establish adequate analysis or generalizability.", + "external_contract_refs": ["experiment-study-v1"], "satisfiability_witness_refs": {"experiment-study-v1": "contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json"}, "binding_obligation": "The study must bind included runs, allocation, research questions, analysis plan, and validity limitations to the same scenario lineage.", + "evidence": [{"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-study-v1.json", "contract_id": "experiment-study-v1", "claim": "Published study and benchmark record."}] + }, + { + "concern_id": "evidence-provenance", "status": "external-contract", "limitation": "Evidence provenance supports audit and derivation; it does not turn authored requirements into captured evidence.", + "external_contract_refs": ["experiment-evidence-record-v1", "associated-artifact-manifest-v1"], "satisfiability_witness_refs": {"experiment-evidence-record-v1": "contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/reference.json", "associated-artifact-manifest-v1": "contracts/fixtures/associated-artifacts/associated-artifact-manifest-v1/valid/study.json"}, "binding_obligation": "Evidence and associated artifacts must bind semantic bytes, parent identity, capture context, and derivation provenance for the same run.", + "evidence": [ + {"kind": "contract", "path": "contracts/schemas/experiment-core/experiment-evidence-record-v1.json", "contract_id": "experiment-evidence-record-v1", "claim": "Evidence provenance contract."}, + {"kind": "contract", "path": "contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json", "contract_id": "associated-artifact-manifest-v1", "claim": "Associated artifact integrity and parent binding."} + ] + } + ] +} diff --git a/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json new file mode 100644 index 000000000..6f7cbf996 --- /dev/null +++ b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json @@ -0,0 +1,150 @@ +{ + "schema_version": "scientific-completeness-taxonomy/v1", + "profile_family": "scientific-scenario-completeness", + "revision": "rev1", + "concerns": [ + {"concern_id": "source-profile-validity", "title": "Canonical SDL source profile", "definition": "Source uses the governed SDL YAML profile and safe loading rules.", "semantic_owner": "SDL source and parser"}, + {"concern_id": "closed-structural-model", "title": "Closed structural model", "definition": "Authored fields conform to the published closed SDL document model.", "semantic_owner": "SDL document model"}, + {"concern_id": "semantic-reference-integrity", "title": "Semantic reference integrity", "definition": "Named references and cross-section constraints pass semantic validation.", "semantic_owner": "SDL semantic validator"}, + {"concern_id": "topology-and-resource-intent", "title": "Topology and resource intent", "definition": "Scenario topology, resources, services, identities, and relationships are authored as intent.", "semantic_owner": "SDL scenario"}, + {"concern_id": "authored-observed-state-separation", "title": "Authored and observed state separation", "definition": "Authored intent, realized state, observation, and evidence remain distinct claim planes.", "semantic_owner": "SDL and experiment evidence"}, + {"concern_id": "scoped-specificity-open-world-intent", "title": "Scoped specificity and open-world intent", "definition": "Exact, constrained, and open realization posture is explicit at the governed scope.", "semantic_owner": "realization semantics"}, + {"concern_id": "parameter-typing", "title": "Parameter typing", "definition": "Scenario parameters have typed finite or constrained domains and portable bindings.", "semantic_owner": "SDL variables and instantiation"}, + {"concern_id": "factor-declaration", "title": "Experimental factor declaration", "definition": "Controlled factors and levels are declared separately from scenario substitution.", "semantic_owner": "experiment authoring"}, + {"concern_id": "controlled-allocation", "title": "Controlled allocation", "definition": "Conditions, blocking factors, assignment units, and replication allocation are declared.", "semantic_owner": "experiment authoring and study"}, + {"concern_id": "randomization-policy", "title": "Randomization policy", "definition": "Random assignment or sampling policy and its scope are declared.", "semantic_owner": "experiment authoring"}, + {"concern_id": "seed-preservation", "title": "Seed preservation", "definition": "Relevant seed, generator, scheduler, and sampling controls are preserved with provenance.", "semantic_owner": "experiment apparatus and run"}, + {"concern_id": "propositions", "title": "Backend-neutral propositions", "definition": "Claims over stable subjects use typed backend-neutral predicates and evidence bases.", "semantic_owner": "SDL propositions"}, + {"concern_id": "assertions", "title": "Role-constrained assertions", "definition": "Preconditions, invariants, and postconditions reference propositions with explicit polarity.", "semantic_owner": "SDL assertions"}, + {"concern_id": "condition-probe-binding", "title": "Condition-to-proposition binding", "definition": "Executable probes are bindings and do not redefine proposition truth.", "semantic_owner": "SDL conditions and runtime truth results"}, + {"concern_id": "workflow-compensation", "title": "Workflow compensation", "definition": "Authored workflows can name bounded compensation behavior for failed steps.", "semantic_owner": "SDL workflow semantics"}, + {"concern_id": "backend-teardown-reconciliation", "title": "Backend teardown and reconciliation", "definition": "Realizers disclose and execute bounded resource cleanup and reconciliation.", "semantic_owner": "backend lifecycle"}, + {"concern_id": "manual-rollback", "title": "Manual and advanced rollback", "definition": "Operators can author or invoke rollback beyond bounded workflow compensation.", "semantic_owner": "workflow and backend lifecycle"}, + {"concern_id": "time-domain-declaration", "title": "Time-domain declaration", "definition": "Wall, monotonic, logical, simulation, and scenario time domains are explicit.", "semantic_owner": "SDL and experiment time semantics"}, + {"concern_id": "clock-declaration", "title": "Clock declaration", "definition": "Clock identity, origin, rate, and ownership are declared.", "semantic_owner": "SDL and runtime clock semantics"}, + {"concern_id": "temporal-ordering-causality", "title": "Temporal ordering and causality", "definition": "Ordering, causality, concurrency, and partial-order claims have governed semantics.", "semantic_owner": "workflow and runtime semantics"}, + {"concern_id": "deadlines-and-windows", "title": "Deadlines and windows", "definition": "Deadlines, evaluation windows, and temporal bounds use declared time domains.", "semantic_owner": "objectives and experiment timing"}, + {"concern_id": "pacing-and-synchronization", "title": "Pacing and synchronization", "definition": "Advancement, pacing, synchronization, and drift policy are explicit.", "semantic_owner": "runtime and participant time management"}, + {"concern_id": "participant-declaration", "title": "Participant declaration", "definition": "Participant identity, role, operating scope, and authority anchors are explicit.", "semantic_owner": "SDL participants"}, + {"concern_id": "participant-action-observation", "title": "Participant action and observation surfaces", "definition": "Visible actions, perceptions, observation boundaries, and histories are governed.", "semantic_owner": "participant semantics and runtime"}, + {"concern_id": "participant-episode-reset", "title": "Participant episode and reset", "definition": "Episode identity, initialization, reset, termination, and provenance are portable.", "semantic_owner": "participant runtime"}, + {"concern_id": "participant-budgets", "title": "Participant budgets", "definition": "Action, time, cost, token, and other quotas have exhaustion semantics and accounting.", "semantic_owner": "participant runtime"}, + {"concern_id": "reference-trajectories", "title": "Reference trajectories", "definition": "Demonstrations and trajectories have typed references, lineage, splits, and replay limits.", "semantic_owner": "participant experiment contracts"}, + {"concern_id": "hidden-benchmark-assets", "title": "Hidden benchmark assets", "definition": "Gold standards and hidden assets have controlled visibility and provenance.", "semantic_owner": "benchmark and information-flow contracts"}, + {"concern_id": "verifier-and-adjudication", "title": "Verifier and adjudication", "definition": "Verifier, judge, annotation, disagreement, and adjudication assets are governed.", "semantic_owner": "experiment evaluation"}, + {"concern_id": "graded-reward-in-sdl", "title": "Graded reward in SDL", "definition": "Graded reward and scoring are not authored as scenario truth semantics.", "semantic_owner": "SDL boundary"}, + {"concern_id": "evaluation-outcome-evidence", "title": "Evaluation outcome evidence", "definition": "Outcome interpretation, measures, evidence, and evaluation results retain provenance.", "semantic_owner": "experiment evaluation and evidence"}, + {"concern_id": "credential-intent", "title": "Credential intent", "definition": "Intentional scenario identities and credential posture are distinct from operator secrets.", "semantic_owner": "SDL accounts and runtime values"}, + {"concern_id": "credential-materialization", "title": "Credential materialization", "definition": "Credential-bearing setup is materialized and evidenced without exposing operator secrets.", "semantic_owner": "provisioning and apparatus"}, + {"concern_id": "host-architecture-constraints", "title": "Host architecture constraints", "definition": "CPU architecture and host-level compatibility requirements are portable constraints.", "semantic_owner": "apparatus and realization"}, + {"concern_id": "substrate-constraints", "title": "Substrate constraints", "definition": "Backend, hypervisor, container, network, and apparatus constraints are explicit.", "semantic_owner": "apparatus and realization"}, + {"concern_id": "vulnerability-inventory", "title": "Vulnerability inventory", "definition": "Declared vulnerabilities and weaknesses use governed identifiers and affected subjects.", "semantic_owner": "SDL vulnerabilities"}, + {"concern_id": "weakness-exploitability-semantics", "title": "Weakness and exploitability semantics", "definition": "Classification, reachability, exploitability, and observation claims remain distinct.", "semantic_owner": "concept authority and propositions"}, + {"concern_id": "flexible-step-tooling", "title": "Flexible step tooling", "definition": "Workflow steps can bind tools without making backend-native names portable behavior.", "semantic_owner": "SDL workflows and realization"}, + {"concern_id": "portable-behavior-contracts", "title": "Portable behavior contracts", "definition": "Behavior is expressed through preconditions, effects, failures, interactions, and governed action contracts.", "semantic_owner": "participant behavior semantics"}, + {"concern_id": "behavioral-relation-taxonomy", "title": "Behavioral relation taxonomy", "definition": "Conformance, refinement, trace, observational, bisimulation, epistemic, and strategic relations are not conflated.", "semantic_owner": "formal semantics and conformance"}, + {"concern_id": "apparatus-constraints", "title": "Apparatus constraints and context", "definition": "Scientific claims bind to declared apparatus requirements and realized context.", "semantic_owner": "experiment apparatus"}, + {"concern_id": "run-plan", "title": "Run plan", "definition": "Pre-run episode, stochastic, allocation, and stopping intent is versioned.", "semantic_owner": "experiment authoring"}, + {"concern_id": "study-record", "title": "Study and benchmark record", "definition": "Study questions, membership, allocation, analysis plan, and validity notes are archived.", "semantic_owner": "experiment study"}, + {"concern_id": "evidence-provenance", "title": "Evidence provenance", "definition": "Captured evidence, measures, artifacts, and derivations remain attributable and integrity-bound.", "semantic_owner": "experiment evidence"} + ], + "profiles": [ + { + "profile_id": "valid-sdl-fragment", + "title": "Valid SDL fragment", + "intended_claim": "The document is admitted as a structurally and semantically valid SDL authoring fragment.", + "explicit_non_claims": ["Does not claim deployability, realization, experimental control, reproducibility, backend conformance, or behavioral equivalence."], + "example_refs": ["examples/completeness/rev1/valid-sdl-fragment/minimal.sdl.yaml"], + "dispositions": { + "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", + "topology-and-resource-intent": "allowed-underspecified", "authored-observed-state-separation": "excluded", "scoped-specificity-open-world-intent": "excluded", + "parameter-typing": "allowed-underspecified", "factor-declaration": "excluded", "controlled-allocation": "excluded", "randomization-policy": "excluded", "seed-preservation": "excluded", + "propositions": "allowed-underspecified", "assertions": "allowed-underspecified", "condition-probe-binding": "allowed-underspecified", + "workflow-compensation": "allowed-underspecified", "backend-teardown-reconciliation": "excluded", "manual-rollback": "excluded", + "time-domain-declaration": "allowed-underspecified", "clock-declaration": "allowed-underspecified", "temporal-ordering-causality": "allowed-underspecified", "deadlines-and-windows": "allowed-underspecified", "pacing-and-synchronization": "excluded", + "participant-declaration": "allowed-underspecified", "participant-action-observation": "excluded", "participant-episode-reset": "excluded", "participant-budgets": "excluded", "reference-trajectories": "excluded", "hidden-benchmark-assets": "excluded", "verifier-and-adjudication": "excluded", + "graded-reward-in-sdl": "excluded", "evaluation-outcome-evidence": "excluded", "credential-intent": "allowed-underspecified", "credential-materialization": "excluded", + "host-architecture-constraints": "excluded", "substrate-constraints": "excluded", "vulnerability-inventory": "allowed-underspecified", "weakness-exploitability-semantics": "allowed-underspecified", + "flexible-step-tooling": "allowed-underspecified", "portable-behavior-contracts": "allowed-underspecified", "behavioral-relation-taxonomy": "excluded", + "apparatus-constraints": "excluded", "run-plan": "excluded", "study-record": "excluded", "evidence-provenance": "excluded" + } + }, + { + "profile_id": "deployable-scenario-intent", + "title": "Deployable scenario intent", + "intended_claim": "The authored scenario carries sufficient portable intent to request realization subject to an explicit apparatus and backend envelope.", + "explicit_non_claims": ["Does not claim that every backend can realize the scenario, that deployment succeeded, or that realized behavior is equivalent."], + "example_refs": [], + "dispositions": { + "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", + "authored-observed-state-separation": "required", "scoped-specificity-open-world-intent": "required", "parameter-typing": "allowed-underspecified", + "factor-declaration": "excluded", "controlled-allocation": "excluded", "randomization-policy": "excluded", "seed-preservation": "excluded", + "propositions": "required", "assertions": "required", "condition-probe-binding": "allowed-underspecified", "workflow-compensation": "allowed-underspecified", + "backend-teardown-reconciliation": "required", "manual-rollback": "allowed-underspecified", "time-domain-declaration": "allowed-underspecified", "clock-declaration": "allowed-underspecified", + "temporal-ordering-causality": "allowed-underspecified", "deadlines-and-windows": "allowed-underspecified", "pacing-and-synchronization": "allowed-underspecified", + "participant-declaration": "allowed-underspecified", "participant-action-observation": "allowed-underspecified", "participant-episode-reset": "excluded", "participant-budgets": "excluded", + "reference-trajectories": "excluded", "hidden-benchmark-assets": "excluded", "verifier-and-adjudication": "excluded", "graded-reward-in-sdl": "excluded", "evaluation-outcome-evidence": "excluded", + "credential-intent": "required", "credential-materialization": "allowed-underspecified", "host-architecture-constraints": "allowed-underspecified", "substrate-constraints": "required", + "vulnerability-inventory": "allowed-underspecified", "weakness-exploitability-semantics": "allowed-underspecified", "flexible-step-tooling": "allowed-underspecified", "portable-behavior-contracts": "allowed-underspecified", + "behavioral-relation-taxonomy": "excluded", "apparatus-constraints": "required", "run-plan": "excluded", "study-record": "excluded", "evidence-provenance": "allowed-underspecified" + } + }, + { + "profile_id": "participant-evaluation-scenario", + "title": "Participant-evaluation scenario", + "intended_claim": "The scenario and bound experiment contracts define participant-visible work, perception, action, episode control, and outcome evidence for evaluation.", + "explicit_non_claims": ["Does not claim participant implementations are strategically equivalent, that hidden global state is visible, or that finite probes prove bisimulation."], + "example_refs": [], + "dispositions": { + "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", + "authored-observed-state-separation": "required", "scoped-specificity-open-world-intent": "required", "parameter-typing": "allowed-underspecified", "factor-declaration": "allowed-underspecified", + "controlled-allocation": "allowed-underspecified", "randomization-policy": "allowed-underspecified", "seed-preservation": "required", "propositions": "required", "assertions": "required", "condition-probe-binding": "required", + "workflow-compensation": "allowed-underspecified", "backend-teardown-reconciliation": "required", "manual-rollback": "allowed-underspecified", + "time-domain-declaration": "required", "clock-declaration": "required", "temporal-ordering-causality": "required", "deadlines-and-windows": "required", "pacing-and-synchronization": "required", + "participant-declaration": "required", "participant-action-observation": "required", "participant-episode-reset": "required", "participant-budgets": "required", "reference-trajectories": "allowed-underspecified", + "hidden-benchmark-assets": "allowed-underspecified", "verifier-and-adjudication": "required", "graded-reward-in-sdl": "excluded", "evaluation-outcome-evidence": "required", + "credential-intent": "required", "credential-materialization": "allowed-underspecified", "host-architecture-constraints": "allowed-underspecified", "substrate-constraints": "required", + "vulnerability-inventory": "allowed-underspecified", "weakness-exploitability-semantics": "allowed-underspecified", "flexible-step-tooling": "required", "portable-behavior-contracts": "required", + "behavioral-relation-taxonomy": "required", "apparatus-constraints": "required", "run-plan": "required", "study-record": "allowed-underspecified", "evidence-provenance": "required" + } + }, + { + "profile_id": "controlled-experiment-scenario", + "title": "Controlled experiment scenario", + "intended_claim": "The scenario is bound to a pre-run design with controlled variation, allocation, apparatus, timing, capture, and analysis intent.", + "explicit_non_claims": ["Does not claim causal validity, successful execution, adequate power, reproducibility, or backend behavioral equivalence."], + "example_refs": [], + "dispositions": { + "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", + "authored-observed-state-separation": "required", "scoped-specificity-open-world-intent": "required", "parameter-typing": "required", "factor-declaration": "required", "controlled-allocation": "required", + "randomization-policy": "required", "seed-preservation": "required", "propositions": "required", "assertions": "required", "condition-probe-binding": "required", + "workflow-compensation": "allowed-underspecified", "backend-teardown-reconciliation": "required", "manual-rollback": "allowed-underspecified", + "time-domain-declaration": "required", "clock-declaration": "required", "temporal-ordering-causality": "required", "deadlines-and-windows": "required", "pacing-and-synchronization": "required", + "participant-declaration": "allowed-underspecified", "participant-action-observation": "allowed-underspecified", "participant-episode-reset": "allowed-underspecified", "participant-budgets": "allowed-underspecified", + "reference-trajectories": "excluded", "hidden-benchmark-assets": "excluded", "verifier-and-adjudication": "required", "graded-reward-in-sdl": "excluded", "evaluation-outcome-evidence": "required", + "credential-intent": "required", "credential-materialization": "allowed-underspecified", "host-architecture-constraints": "required", "substrate-constraints": "required", + "vulnerability-inventory": "allowed-underspecified", "weakness-exploitability-semantics": "allowed-underspecified", "flexible-step-tooling": "allowed-underspecified", "portable-behavior-contracts": "allowed-underspecified", + "behavioral-relation-taxonomy": "required", "apparatus-constraints": "required", "run-plan": "required", "study-record": "required", "evidence-provenance": "required" + } + }, + { + "profile_id": "reproducible-benchmark-study-input", + "title": "Reproducible benchmark or study input", + "intended_claim": "The scenario and governed experiment bundle preserve sufficient design, apparatus, participant, evidence, lineage, and analysis context for a bounded reproduction attempt.", + "explicit_non_claims": ["Does not guarantee bitwise replay, equal outcomes, scientific validity, generalizability, or behavioral equivalence across apparatuses."], + "example_refs": [], + "dispositions": { + "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", + "authored-observed-state-separation": "required", "scoped-specificity-open-world-intent": "required", "parameter-typing": "required", "factor-declaration": "required", "controlled-allocation": "required", + "randomization-policy": "required", "seed-preservation": "required", "propositions": "required", "assertions": "required", "condition-probe-binding": "required", + "workflow-compensation": "required", "backend-teardown-reconciliation": "required", "manual-rollback": "allowed-underspecified", + "time-domain-declaration": "required", "clock-declaration": "required", "temporal-ordering-causality": "required", "deadlines-and-windows": "required", "pacing-and-synchronization": "required", + "participant-declaration": "required", "participant-action-observation": "required", "participant-episode-reset": "required", "participant-budgets": "required", "reference-trajectories": "required", + "hidden-benchmark-assets": "required", "verifier-and-adjudication": "required", "graded-reward-in-sdl": "excluded", "evaluation-outcome-evidence": "required", + "credential-intent": "required", "credential-materialization": "required", "host-architecture-constraints": "required", "substrate-constraints": "required", + "vulnerability-inventory": "allowed-underspecified", "weakness-exploitability-semantics": "required", "flexible-step-tooling": "required", "portable-behavior-contracts": "required", + "behavioral-relation-taxonomy": "required", "apparatus-constraints": "required", "run-plan": "required", "study-record": "required", "evidence-provenance": "required" + } + } + ] +} diff --git a/contracts/provenance/sdl-lineage-ledger-v1.json b/contracts/provenance/sdl-lineage-ledger-v1.json new file mode 100644 index 000000000..53cc46e9d --- /dev/null +++ b/contracts/provenance/sdl-lineage-ledger-v1.json @@ -0,0 +1,3100 @@ +{ + "schema_version": "sdl-lineage-ledger/v1", + "reviewed_on": "2026-07-12", + "citations": [ + { + "citation_id": "ocr-sdl-v0.21.2", + "title": "Open Cyber Range SDL Parser v0.21.2", + "authors_or_maintainer": [ + "CR14 / Norwegian Cyber Range" + ], + "year": 2024, + "canonical_url": "https://github.com/Open-Cyber-Range/SDL-parser/commit/fe83e8281fc4b954967fbaa5a0d099007ddcb06c", + "verified_on": "2026-07-12", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#open-cyber-range-sdl" + }, + { + "citation_id": "cacao-v2.0", + "title": "CACAO Security Playbooks Version 2.0", + "authors_or_maintainer": [ + "OASIS Open" + ], + "year": 2023, + "canonical_url": "https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html", + "verified_on": "2026-07-12", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#cacao-v20" + }, + { + "citation_id": "stix-v2.1", + "title": "STIX Version 2.1", + "authors_or_maintainer": [ + "OASIS Open" + ], + "year": 2021, + "canonical_url": "https://docs.oasis-open.org/cti/stix/v2.1/stix-v2.1.html", + "verified_on": "2026-07-12", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#stix-v21" + }, + { + "citation_id": "cyris-2018", + "title": "Cybersecurity Education and Training Support System: CyRIS", + "authors_or_maintainer": [ + "Razvan Beuran", + "Cuong Pham", + "Dat Tang", + "Ken-ichi Chinen", + "Yasuo Tan", + "Yoichi Shinoda" + ], + "year": 2018, + "container_title": "IEICE Transactions on Information and Systems", + "doi": "10.1587/transinf.2017EDP7207", + "canonical_url": "https://doi.org/10.1587/transinf.2017EDP7207", + "verified_on": "2026-07-12", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#cyris" + }, + { + "citation_id": "cyborg-2021", + "title": "CybORG: A Gym for the Development of Autonomous Cyber Agents", + "authors_or_maintainer": [ + "Max Standen et al." + ], + "year": 2021, + "container_title": "IJCAI-21 1st International Workshop on Adaptive Cyber Defense", + "canonical_url": "https://arxiv.org/abs/2108.09118", + "verified_on": "2026-07-12", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#cyborg" + }, + { + "citation_id": "crack-nca-2018", + "title": "Scenario Design and Validation for Next Generation Cyber Ranges", + "authors_or_maintainer": [ + "Enrico Russo", + "Gabriele Costa", + "Alessandro Armando" + ], + "year": 2018, + "container_title": "2018 IEEE 17th International Symposium on Network Computing and Applications (NCA)", + "doi": "10.1109/NCA.2018.8548324", + "canonical_url": "https://doi.org/10.1109/NCA.2018.8548324", + "verified_on": "2026-07-12", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#crack-publications" + }, + { + "citation_id": "crack-cose-2020", + "title": "Building next generation Cyber Ranges with CRACK", + "authors_or_maintainer": [ + "Enrico Russo", + "Gabriele Costa", + "Alessandro Armando" + ], + "year": 2020, + "container_title": "Computers & Security", + "doi": "10.1016/j.cose.2020.101837", + "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" + } + ], + "sources": [ + { + "source_id": "ocr-sdl-v0.21.2", + "kind": "git", + "title": "Open Cyber Range SDL Parser", + "version_or_edition": "v0.21.2", + "canonical_url": "https://github.com/Open-Cyber-Range/SDL-parser/tree/fe83e8281fc4b954967fbaa5a0d099007ddcb06c", + "repository_url": "https://github.com/Open-Cyber-Range/SDL-parser", + "commit": "fe83e8281fc4b954967fbaa5a0d099007ddcb06c", + "citation_ref": "ocr-sdl-v0.21.2", + "license_expression": "MIT", + "license_url": "https://github.com/Open-Cyber-Range/SDL-parser/blob/fe83e8281fc4b954967fbaa5a0d099007ddcb06c/LICENSE", + "license_evidence_ref": "docs/research/lineage/source-audit-2026-07-12.md#open-cyber-range-sdl" + }, + { + "source_id": "cacao-v2.0", + "kind": "standard", + "title": "CACAO Security Playbooks", + "version_or_edition": "Version 2.0 Committee Specification 01", + "canonical_url": "https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html", + "maintaining_body": "OASIS Open", + "citation_ref": "cacao-v2.0" + }, + { + "source_id": "stix-v2.1", + "kind": "standard", + "title": "Structured Threat Information Expression", + "version_or_edition": "Version 2.1 OASIS Standard", + "canonical_url": "https://docs.oasis-open.org/cti/stix/v2.1/stix-v2.1.html", + "maintaining_body": "OASIS Open", + "citation_ref": "stix-v2.1" + }, + { + "source_id": "cyris-2018", + "kind": "publication", + "title": "CyRIS", + "version_or_edition": "published 2018", + "canonical_url": "https://doi.org/10.1587/transinf.2017EDP7207", + "citation_ref": "cyris-2018" + }, + { + "source_id": "cyborg-2021", + "kind": "publication", + "title": "CybORG", + "version_or_edition": "arXiv:2108.09118v1", + "canonical_url": "https://arxiv.org/abs/2108.09118", + "citation_ref": "cyborg-2021" + } + ], + "subjects": [ + { + "subject_id": "sdl-field:accounts", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/accounts", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "cyris-2018" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/accounts" + } + ], + "source_boundaries": [ + { + "artifact": "CyRIS account operations", + "symbol_or_pointer": "add_account and modify_account" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "none", + "citation_refs": [ + "cyris-2018" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:action_contracts", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/action_contracts", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/action_contracts" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:agents", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/agents", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "cyborg-2021" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/agents" + } + ], + "source_boundaries": [ + { + "artifact": "CybORG scenario YAML", + "symbol_or_pointer": "Agents and scenario agent configuration" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "none", + "citation_refs": [ + "cyborg-2021" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:assertions", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/assertions", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/assertions" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:behavior_specifications", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/behavior_specifications", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/behavior_specifications" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:conditions", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/conditions", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/conditions" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/condition.rs", + "symbol_or_pointer": "v0.21.2 model for conditions" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/conditions.py", + "symbol_or_pointer": "conditions" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/condition.rs", + "symbol_or_pointer": "v0.21.2 model for conditions" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:content", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/content", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "cyris-2018" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/content" + } + ], + "source_boundaries": [ + { + "artifact": "CyRIS content operations", + "symbol_or_pointer": "copy_content and emulate_traffic_capture" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "none", + "citation_refs": [ + "cyris-2018" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:description", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/description", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/description" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/lib.rs", + "symbol_or_pointer": "v0.21.2 model for description" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/scenario.py", + "symbol_or_pointer": "description" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/lib.rs", + "symbol_or_pointer": "v0.21.2 model for description" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:entities", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/entities", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/entities" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/entity.rs", + "symbol_or_pointer": "v0.21.2 model for entities" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/entities.py", + "symbol_or_pointer": "entities" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/entity.rs", + "symbol_or_pointer": "v0.21.2 model for entities" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:events", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/events", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/events" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/event.rs", + "symbol_or_pointer": "v0.21.2 model for events" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/orchestration.py", + "symbol_or_pointer": "events" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/event.rs", + "symbol_or_pointer": "v0.21.2 model for events" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:evidence_requirements", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/evidence_requirements", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/evidence_requirements" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:features", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/features", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/features" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/feature.rs", + "symbol_or_pointer": "v0.21.2 model for features" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/features.py", + "symbol_or_pointer": "features" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/feature.rs", + "symbol_or_pointer": "v0.21.2 model for features" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:forwarding_agents", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/forwarding_agents", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/forwarding_agents" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:imports", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/imports", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/imports" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:infrastructure", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/infrastructure", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/infrastructure" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/infrastructure.rs", + "symbol_or_pointer": "v0.21.2 model for infrastructure" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/infrastructure.py", + "symbol_or_pointer": "infrastructure" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/infrastructure.rs", + "symbol_or_pointer": "v0.21.2 model for infrastructure" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:injects", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/injects", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/injects" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/inject.rs", + "symbol_or_pointer": "v0.21.2 model for injects" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/orchestration.py", + "symbol_or_pointer": "injects" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/inject.rs", + "symbol_or_pointer": "v0.21.2 model for injects" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:module", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/module", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/module" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:name", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/name", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/name" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/lib.rs", + "symbol_or_pointer": "v0.21.2 model for name" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/scenario.py", + "symbol_or_pointer": "name" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/lib.rs", + "symbol_or_pointer": "v0.21.2 model for name" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:nodes", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/nodes", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/nodes" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/node.rs", + "symbol_or_pointer": "v0.21.2 model for nodes" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/nodes.py", + "symbol_or_pointer": "nodes" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/node.rs", + "symbol_or_pointer": "v0.21.2 model for nodes" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:objectives", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/objectives", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "cacao-v2.0" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/objectives" + } + ], + "source_boundaries": [ + { + "artifact": "CACAO v2.0", + "symbol_or_pointer": "playbook objective and workflow context" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "none", + "citation_refs": [ + "cacao-v2.0" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:observation_boundaries", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/observation_boundaries", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/observation_boundaries" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:outcome_interpretation_rules", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/outcome_interpretation_rules", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/outcome_interpretation_rules" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:propositions", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/propositions", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/propositions" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/sections.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "sdl-field:relationships", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/relationships", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "stix-v2.1" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/relationships" + } + ], + "source_boundaries": [ + { + "artifact": "STIX v2.1", + "symbol_or_pointer": "Relationship SRO" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "partial", + "citation_refs": [ + "stix-v2.1" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:scripts", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/scripts", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/scripts" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/script.rs", + "symbol_or_pointer": "v0.21.2 model for scripts" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/orchestration.py", + "symbol_or_pointer": "scripts" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/script.rs", + "symbol_or_pointer": "v0.21.2 model for scripts" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:stories", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/stories", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/stories" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/story.rs", + "symbol_or_pointer": "v0.21.2 model for stories" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/orchestration.py", + "symbol_or_pointer": "stories" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/story.rs", + "symbol_or_pointer": "v0.21.2 model for stories" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:variables", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/variables", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "cacao-v2.0" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/variables" + } + ], + "source_boundaries": [ + { + "artifact": "CACAO v2.0", + "symbol_or_pointer": "playbook_variables" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "partial", + "citation_refs": [ + "cacao-v2.0" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:version", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/version", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/version" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/lib.rs", + "symbol_or_pointer": "v0.21.2 model for version" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/scenario.py", + "symbol_or_pointer": "version" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/lib.rs", + "symbol_or_pointer": "v0.21.2 model for version" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:vulnerabilities", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/vulnerabilities", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/vulnerabilities" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/vulnerability.rs", + "symbol_or_pointer": "v0.21.2 model for vulnerabilities" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/vulnerabilities.py", + "symbol_or_pointer": "vulnerabilities" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/vulnerability.rs", + "symbol_or_pointer": "v0.21.2 model for vulnerabilities" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:workflows", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/workflows", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "cacao-v2.0" + ], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/workflows" + } + ], + "source_boundaries": [ + { + "artifact": "CACAO v2.0", + "symbol_or_pointer": "workflow step graph" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "partial", + "citation_refs": [ + "cacao-v2.0" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "runtime-family:service_listeners", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/service_listeners", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/service_listeners" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:applications", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/applications", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/applications" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:database_services", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/database_services", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/database_services" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:dns_services", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/dns_services", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/dns_services" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:identity_authorities", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/identity_authorities", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/identity_authorities" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:file_services", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/file_services", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/file_services" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:mail_services", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/mail_services", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/mail_services" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:network_sensors", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/network_sensors", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/network_sensors" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:network_detection_engines", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/network_detection_engines", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/network_detection_engines" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:security_monitoring_managers", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/security_monitoring_managers", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/security_monitoring_managers" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:ssh_servers", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/ssh_servers", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/ssh_servers" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:app_authorizations", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/app_authorizations", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/app_authorizations" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:scheduled_jobs", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/scheduled_jobs", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/scheduled_jobs" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:datastore_services", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/datastore_services", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/datastore_services" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:platform_applications", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/platform_applications", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/platform_applications" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:forwarding_agents", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/forwarding_agents", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/forwarding_agents" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "runtime-family:orchestration_authorities", + "subject_kind": "runtime_family", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/$defs/RuntimeConfiguration/properties/orchestration_authorities", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/$defs/RuntimeConfiguration/properties/orchestration_authorities" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/sdl/runtime-inventory.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:actions-and-events", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/actions-and-events" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/actions-and-events" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:apparatus-declarations", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/apparatus-declarations" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/apparatus-declarations" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:assets", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/assets" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/assets" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:episodes", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/episodes" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/episodes" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:identities", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/identities" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/identities" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:observables", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/observables" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/observables" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:provenance-and-evidence", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/provenance-and-evidence" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/provenance-and-evidence" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:realization-and-disclosure", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/realization-and-disclosure" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/realization-and-disclosure" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:relationships", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/relationships" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/relationships" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:runtime-inventory", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/runtime-inventory" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/runtime-inventory" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:scenarios", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/scenarios" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/scenarios" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:tasks-runs-studies", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/tasks-runs-studies" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/tasks-runs-studies" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:time-and-apparatus", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/time-and-apparatus" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/time-and-apparatus" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "concept-family:tools-and-artifacts", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/tools-and-artifacts" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/tools-and-artifacts" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/concept-authority.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "reference-model:scenario-account", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-account" + }, + "claims": [ + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "cyris-2018" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-account" + } + ], + "source_boundaries": [ + { + "artifact": "CyRIS account operations", + "symbol_or_pointer": "add_account and modify_account" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "none", + "citation_refs": [ + "cyris-2018" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "reference-model:scenario-assertion", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-assertion" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-assertion" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/reference-models.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "reference-model:scenario-condition", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-condition" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-condition" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/condition.rs", + "symbol_or_pointer": "v0.21.2 model for conditions" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/conditions.py", + "symbol_or_pointer": "conditions" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/condition.rs", + "symbol_or_pointer": "v0.21.2 model for conditions" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "reference-model:scenario-content", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-content" + }, + "claims": [ + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "cyris-2018" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-content" + } + ], + "source_boundaries": [ + { + "artifact": "CyRIS content operations", + "symbol_or_pointer": "copy_content and emulate_traffic_capture" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "none", + "citation_refs": [ + "cyris-2018" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "reference-model:scenario-event", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-event" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-event" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/event.rs", + "symbol_or_pointer": "v0.21.2 model for events" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/orchestration.py", + "symbol_or_pointer": "events" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/event.rs", + "symbol_or_pointer": "v0.21.2 model for events" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "reference-model:scenario-node", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-node" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-node" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/node.rs", + "symbol_or_pointer": "v0.21.2 model for nodes" + } + ], + "divergence": "Translated into the ACES Python/Pydantic authoring contract and subsequently extended or narrowed; field-name overlap does not imply full parser compatibility.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + }, + { + "plane": "artifact_code", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "implementations/python/packages/aces_sdl/nodes.py", + "symbol_or_pointer": "nodes" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/node.rs", + "symbol_or_pointer": "v0.21.2 model for nodes" + } + ], + "divergence": "The initial ACES implementation explicitly described these structures as ports; current code has diverged materially but retains translated model structure.", + "compatibility": "partial", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "reference-model:scenario-node-runtime", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-node-runtime" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-node-runtime" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/reference-models.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "reference-model:scenario-proposition", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-proposition" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-proposition" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/concept-authority/reference-models.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, + { + "subject_id": "reference-model:scenario-relationship", + "subject_kind": "reference_model", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "pointer": "#/models/scenario-relationship" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "stix-v2.1" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/reference-models-v1.json", + "symbol_or_pointer": "#/models/scenario-relationship" + } + ], + "source_boundaries": [ + { + "artifact": "STIX v2.1", + "symbol_or_pointer": "Relationship SRO" + } + ], + "divergence": "ACES preserves only the stated concern and applies its own closed models, reference rules, lifecycle, and validation semantics.", + "compatibility": "partial", + "citation_refs": [ + "stix-v2.1" + ], + "internal_authority_refs": [], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:metrics", + "subject_kind": "top_level_field", + "disposition": "removed", + "authority": { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "pointer": "#removed-metrics" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "symbol_or_pointer": "removed OCR metrics surface" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/metric.rs", + "symbol_or_pointer": "v0.21.2 metrics model" + } + ], + "divergence": "Removed from authored SDL; graded evaluation and scoring are separate experiment/evaluator-plane concerns.", + "compatibility": "none", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [ + "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md" + ], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:evaluations", + "subject_kind": "top_level_field", + "disposition": "removed", + "authority": { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "pointer": "#removed-evaluations" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "symbol_or_pointer": "removed OCR evaluations surface" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/evaluation.rs", + "symbol_or_pointer": "v0.21.2 evaluations model" + } + ], + "divergence": "Removed from authored SDL; graded evaluation and scoring are separate experiment/evaluator-plane concerns.", + "compatibility": "none", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [ + "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md" + ], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:tlos", + "subject_kind": "top_level_field", + "disposition": "removed", + "authority": { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "pointer": "#removed-tlos" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "symbol_or_pointer": "removed OCR tlos surface" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/training_learning_objective.rs", + "symbol_or_pointer": "v0.21.2 tlos model" + } + ], + "divergence": "Removed from authored SDL; graded evaluation and scoring are separate experiment/evaluator-plane concerns.", + "compatibility": "none", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [ + "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md" + ], + "compatibility_direction": "aces_relative_to_source" + } + ] + }, + { + "subject_id": "sdl-field:goals", + "subject_kind": "top_level_field", + "disposition": "removed", + "authority": { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "pointer": "#removed-goals" + }, + "claims": [ + { + "plane": "syntax", + "classification": "adapted", + "source_refs": [ + "ocr-sdl-v0.21.2" + ], + "aces_boundaries": [ + { + "artifact": "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md", + "symbol_or_pointer": "removed OCR goals surface" + } + ], + "source_boundaries": [ + { + "artifact": "sdl-parser/src/goal.rs", + "symbol_or_pointer": "v0.21.2 goals model" + } + ], + "divergence": "Removed from authored SDL; graded evaluation and scoring are separate experiment/evaluator-plane concerns.", + "compatibility": "none", + "citation_refs": [ + "ocr-sdl-v0.21.2" + ], + "internal_authority_refs": [ + "docs/decisions/adrs/adr-073-scoring-reward-language-scope.md" + ], + "compatibility_direction": "aces_relative_to_source" + } + ] + } + ], + "third_party_dispositions": [ + { + "source_ref": "ocr-sdl-v0.21.2", + "derivation_scope": [ + { + "artifact": "implementations/python/packages/aces_sdl/scenario.py", + "symbol_or_pointer": "Scenario identity fields and OCR-derived top-level section structure" + }, + { + "artifact": "implementations/python/packages/aces_sdl/nodes.py", + "symbol_or_pointer": "Node, VM, Switch, Resources, and Role model lineage" + }, + { + "artifact": "implementations/python/packages/aces_sdl/conditions.py", + "symbol_or_pointer": "Condition model lineage" + }, + { + "artifact": "implementations/python/packages/aces_sdl/entities.py", + "symbol_or_pointer": "Entity model lineage" + }, + { + "artifact": "implementations/python/packages/aces_sdl/orchestration.py", + "symbol_or_pointer": "Inject, Event, Script, and Story model lineage" + }, + { + "artifact": "implementations/python/packages/aces_sdl/infrastructure.py", + "symbol_or_pointer": "InfraNode model lineage" + }, + { + "artifact": "implementations/python/packages/aces_sdl/features.py", + "symbol_or_pointer": "Feature model lineage" + }, + { + "artifact": "implementations/python/packages/aces_sdl/vulnerabilities.py", + "symbol_or_pointer": "Vulnerability model lineage" + } + ], + "derivation_extent": "Translated and structurally adapted model definitions identified as OCR ports in the initial ACES history; later ACES changes substantially extend and narrow the current implementation.", + "reviewed_license_ref": "ocr-sdl-v0.21.2 license at fe83e8281fc4b954967fbaa5a0d099007ddcb06c", + "notice_decision": "required_included", + "notice_artifact": "THIRD_PARTY_NOTICES.md", + "rationale": "Conservative compliance disposition: the pinned MIT license requires its copyright and permission notice in copies or substantial portions, and ACES history explicitly identifies translated OCR model structure.", + "reviewed_on": "2026-07-12", + "evidence_refs": [ + "docs/research/lineage/source-audit-2026-07-12.md#open-cyber-range-sdl", + "git:2e73ee6ce11ef42fef10e1837ee2bb96570d030d" + ] + } + ] +} diff --git a/contracts/realization-envelopes/libvirt-qemu/generic-v1.json b/contracts/realization-envelopes/libvirt-qemu/generic-v1.json new file mode 100644 index 000000000..dcea4855e --- /dev/null +++ b/contracts/realization-envelopes/libvirt-qemu/generic-v1.json @@ -0,0 +1,40 @@ +{ + "schema_version": "realization-envelope/v1", + "contract_id": "realization-envelope-v1", + "id": "libvirt-qemu.generic.v1", + "expression": { + "schema_version": "realization-envelope/v1", + "id": "libvirt-qemu.generic.expression.v1", + "scope": "scenario", + "domains": {}, + "bindings": [], + "closure": [] + }, + "configuration": { + "mode": "generic", + "configuration_digest": "sha256:2af0fbd4a95b95a3a51d1488f641985c93e4c470baeb188f3e981f8b36327450", + "architecture": "x86_64", + "image_policy": "local-qcow2", + "network_policy": "libvirt-managed", + "supported_node_types": ["switch", "vm"], + "supported_os_families": ["linux"], + "supported_content_types": ["file"], + "supported_account_features": ["auth_method", "disabled", "groups", "home", "shell"], + "supports_acls": true, + "memory_mib": {"minimum": 128, "maximum": null}, + "vcpus": {"minimum": 1, "maximum": null} + }, + "concerns": [ + {"concern": "topology", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-domain-network", "transformations": []}, + {"concern": "architecture", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qemu-x86_64-domain", "transformations": []}, + {"concern": "image", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qcow2-disk-attachment", "transformations": []}, + {"concern": "resource-allocation", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "libvirt-domain-xml", "transformations": ["bounded-normalization", "default-substitution"]}, + {"concern": "network", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "libvirt-network-xml", "transformations": ["default-substitution"]}, + {"concern": "content-placement", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "cloud-init-seed", "transformations": []}, + {"concern": "account-placement", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "cloud-init-seed", "transformations": ["default-substitution"]}, + {"concern": "feature-binding", "disposition": "transformed", "observation_strength": "driver-reported", "mechanism": "cloud-init-seed", "transformations": ["descriptor-substitution"]}, + {"concern": "service", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, + {"concern": "acl", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-nwfilter", "transformations": []} + ], + "digest": "sha256:eb3b54b199249da599cdd59e050612314ed16fe6039630307894ee8d8e2a466f" +} diff --git a/contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json b/contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json new file mode 100644 index 000000000..0018d09cf --- /dev/null +++ b/contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json @@ -0,0 +1,118 @@ +{ + "schema_version": "realization-envelope/v1", + "contract_id": "realization-envelope-v1", + "id": "libvirt-qemu.guest-certified-appliance.v1", + "expression": { + "schema_version": "realization-envelope/v1", + "id": "libvirt-qemu.guest-certified-appliance.expression.v1", + "scope": "scenario", + "domains": {}, + "bindings": [], + "closure": [] + }, + "configuration": { + "mode": "guest-certified-appliance", + "architecture": "x86_64", + "image_policy": "guest-certified-appliance/serial-fact-channel/v1", + "network_policy": "generated-appliance-network", + "supported_node_types": [ + "switch", + "vm" + ], + "supported_os_families": [ + "linux" + ], + "supported_content_types": [ + "file" + ], + "supported_account_features": [ + "disabled", + "groups", + "home", + "shell" + ], + "supports_acls": false, + "memory_mib": { + "minimum": 64, + "maximum": 256 + }, + "vcpus": { + "minimum": 1, + "maximum": 2 + }, + "configuration_digest": "sha256:b33ad469eaf1a47963e49da8adc7376fc22880ac3e6126b427239daa00dc6d99" + }, + "concerns": [ + { + "concern": "topology", + "disposition": "realized", + "observation_strength": "daemon-observed", + "mechanism": "libvirt-domain-network-readback", + "transformations": [] + }, + { + "concern": "architecture", + "disposition": "realized", + "observation_strength": "guest-observed", + "mechanism": "guest-uname-readback", + "transformations": [] + }, + { + "concern": "image", + "disposition": "realized", + "observation_strength": "daemon-observed", + "mechanism": "generated-appliance-attachment-readback", + "transformations": [] + }, + { + "concern": "resource-allocation", + "disposition": "realized", + "observation_strength": "guest-observed", + "mechanism": "guest-proc-resource-readback", + "transformations": [] + }, + { + "concern": "network", + "disposition": "realized", + "observation_strength": "guest-observed", + "mechanism": "guest-link-address-readback", + "transformations": [] + }, + { + "concern": "content-placement", + "disposition": "realized", + "observation_strength": "guest-observed", + "mechanism": "guest-file-content-readback", + "transformations": [] + }, + { + "concern": "account-placement", + "disposition": "realized", + "observation_strength": "guest-observed", + "mechanism": "guest-account-posture-readback", + "transformations": [] + }, + { + "concern": "feature-binding", + "disposition": "unsupported", + "observation_strength": "none", + "mechanism": null, + "transformations": [] + }, + { + "concern": "service", + "disposition": "realized", + "observation_strength": "guest-observed", + "mechanism": "guest-service-state-readback", + "transformations": [] + }, + { + "concern": "acl", + "disposition": "unsupported", + "observation_strength": "none", + "mechanism": null, + "transformations": [] + } + ], + "digest": "sha256:8416a600a6f1864e1b80e49fa60eef5f423f9249f2ea7dc56e14d8a669e33e2e" +} diff --git a/contracts/realization-envelopes/libvirt-qemu/techvault-appliance-v1.json b/contracts/realization-envelopes/libvirt-qemu/techvault-appliance-v1.json new file mode 100644 index 000000000..a08f79dcd --- /dev/null +++ b/contracts/realization-envelopes/libvirt-qemu/techvault-appliance-v1.json @@ -0,0 +1,40 @@ +{ + "schema_version": "realization-envelope/v1", + "contract_id": "realization-envelope-v1", + "id": "libvirt-qemu.techvault-appliance.v1", + "expression": { + "schema_version": "realization-envelope/v1", + "id": "libvirt-qemu.techvault-appliance.expression.v1", + "scope": "scenario", + "domains": {}, + "bindings": [], + "closure": [] + }, + "configuration": { + "mode": "techvault-appliance", + "configuration_digest": "sha256:227dc29681c61c341291ef4ef30b8c777e166103a5e76a74d87cc2164a7bf5ca", + "architecture": "x86_64", + "image_policy": "generated-initramfs-appliance", + "network_policy": "generated-appliance-network", + "supported_node_types": ["switch", "vm"], + "supported_os_families": ["linux"], + "supported_content_types": [], + "supported_account_features": [], + "supports_acls": false, + "memory_mib": {"minimum": 64, "maximum": 128}, + "vcpus": {"minimum": 1, "maximum": 2} + }, + "concerns": [ + {"concern": "topology", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "libvirt-domain-network-readback", "transformations": []}, + {"concern": "architecture", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "libvirt-domain-xml-readback", "transformations": []}, + {"concern": "image", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "generated-initramfs-attachment-readback", "transformations": []}, + {"concern": "resource-allocation", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "libvirt-domain-xml-readback", "transformations": []}, + {"concern": "network", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "libvirt-network-domain-xml-readback", "transformations": []}, + {"concern": "content-placement", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, + {"concern": "account-placement", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, + {"concern": "feature-binding", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, + {"concern": "service", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, + {"concern": "acl", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []} + ], + "digest": "sha256:b0cdf9c60cf42a50782f0d417cd931462c4d630e35e7546734710219477622ed" +} diff --git a/contracts/schema-publication-manifest.json b/contracts/schema-publication-manifest.json index 1f5156a7e..e26bb949a 100644 --- a/contracts/schema-publication-manifest.json +++ b/contracts/schema-publication-manifest.json @@ -8,6 +8,16 @@ "stability": "draft", "content_hash": "e7b858c93b7ec763c361439b1d9c7cc3979a1d150ca64a7f41ebc12c050f5cff" }, + { + "contract_id": "associated-artifact-manifest-v1", + "schema_path": "contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json", + "stability": "draft", + "content_hash": "50cb495731d7274371dd01116acfe67dda3dd3261a1a911136cae02dab7f7927", + "last_change": { + "summary": "Initial publication of the ADR-077 scenario- and experiment-associated artifact manifest, canonical set identity, and required semantic byte-binding invariant.", + "content_hash": "50cb495731d7274371dd01116acfe67dda3dd3261a1a911136cae02dab7f7927" + } + }, { "contract_id": "atlas-tactics-source-v1", "schema_path": "contracts/schemas/concept-authority/atlas-tactics-source-v1.json", @@ -32,20 +42,20 @@ "contract_id": "backend-manifest-v2", "schema_path": "contracts/schemas/backend-manifest/backend-manifest-v2.json", "stability": "draft", - "content_hash": "4485bf2485e1f98be2c540552d42828a170dc6ff5beeb6b091a4e87a55b922ab", + "content_hash": "e2beb9d9f20f7564c8b485b653c706b8b3d83ebdc535bff62e80fbcd82d8200f", "last_change": { - "summary": "Added experiment-run-v1 to the governed backend manifest contract vocabulary for run-level observability and augmentation evidence disclosure.", - "content_hash": "4485bf2485e1f98be2c540552d42828a170dc6ff5beeb6b091a4e87a55b922ab" + "summary": "Published evaluator proposition, assertion, evidence, truth-outcome, temporal, and binding-provenance capability declarations and assertion-aware orchestration support.", + "content_hash": "e2beb9d9f20f7564c8b485b653c706b8b3d83ebdc535bff62e80fbcd82d8200f" } }, { "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "3f4152b2d7f1e4a01b30e2782a4d6001654e2785f4b9fe54121ed7e84a37c604", + "content_hash": "7796e171b81cc47e15446d77097c63d56564235b96a5f82e8e813753d4f7b709", "last_change": { - "summary": "Added experiment-run-v1 to the backend profile contract vocabulary so profile fixture conformance can exercise run-level observability semantics.", - "content_hash": "3f4152b2d7f1e4a01b30e2782a4d6001654e2785f4b9fe54121ed7e84a37c604" + "summary": "Added proposition-truth-result-v1 to the governed backend contract vocabulary.", + "content_hash": "7796e171b81cc47e15446d77097c63d56564235b96a5f82e8e813753d4f7b709" } }, { @@ -74,7 +84,11 @@ "contract_id": "evaluation-plan-v1", "schema_path": "contracts/schemas/plans/evaluation-plan-v1.json", "stability": "draft", - "content_hash": "eeff605ae19059eb23648c717438189758f22ea8a35510c4561aa6f5f469a573" + "content_hash": "804f1970cbf9d80e80c3f984deb1d0f034470df7b25ee10ce7c7959b6ad69a4b", + "last_change": { + "summary": "Added proposition and assertion semantic-admission resources to the closed evaluation-plan vocabulary.", + "content_hash": "804f1970cbf9d80e80c3f984deb1d0f034470df7b25ee10ce7c7959b6ad69a4b" + } }, { "contract_id": "evaluation-result-envelope-v1", @@ -86,80 +100,100 @@ "contract_id": "experiment-apparatus-context-v1", "schema_path": "contracts/schemas/experiment-core/experiment-apparatus-context-v1.json", "stability": "draft", - "content_hash": "565558814655c9fc3cd790fb441633622e4846d416ffaba43812143024e33ae3", + "content_hash": "a2c8fcfc2fec00a20ee9e46af3160c88b9e7aeb8b2315afbaa3781b3e25c0f93", "last_change": { - "summary": "Extended experiment-core references for the EXP-707/EXP-708/EXP-709 evidence and measure contract boundary.", - "content_hash": "565558814655c9fc3cd790fb441633622e4846d416ffaba43812143024e33ae3" + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "a2c8fcfc2fec00a20ee9e46af3160c88b9e7aeb8b2315afbaa3781b3e25c0f93" + } + }, + { + "contract_id": "experiment-authoring-input-v1", + "schema_path": "contracts/schemas/experiment-core/experiment-authoring-input-v1.json", + "stability": "draft", + "content_hash": "2f399830a126d155bbb3940ad1f0bbb2c41fe540ea261d12e37d74400393d976", + "last_change": { + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "2f399830a126d155bbb3940ad1f0bbb2c41fe540ea261d12e37d74400393d976" } }, { "contract_id": "experiment-capture-spec-v1", "schema_path": "contracts/schemas/experiment-core/experiment-capture-spec-v1.json", "stability": "draft", - "content_hash": "1c479291aed4c60aa7a28d93839d3633f8dee585ca3098b83bd4fbb03bea86ad", + "content_hash": "b262408eda4309d265a292ffd2fbcb1e34ba58c28205e4d99996f1b854a70876", "last_change": { - "summary": "Published the SEM-224 observability/evidence plane (authored_evidence_requirement) as a portable x-aces-plane annotation sourced from the carrier-oriented plane classifier.", - "content_hash": "1c479291aed4c60aa7a28d93839d3633f8dee585ca3098b83bd4fbb03bea86ad" + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "b262408eda4309d265a292ffd2fbcb1e34ba58c28205e4d99996f1b854a70876" } }, { "contract_id": "experiment-derived-measure-v1", "schema_path": "contracts/schemas/experiment-core/experiment-derived-measure-v1.json", "stability": "draft", - "content_hash": "162aeeb41906008b03ef211b20646f451101c59cdcabc1614427ca3fa7945afe", + "content_hash": "3619f07b4e075a47a99e026aed5395a6a9c06b663a81e62f67bba7fd196ccf34", "last_change": { - "summary": "Published the SEM-224 observability/evidence plane (derived_analysis) as a portable x-aces-plane annotation sourced from the carrier-oriented plane classifier.", - "content_hash": "162aeeb41906008b03ef211b20646f451101c59cdcabc1614427ca3fa7945afe" + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "3619f07b4e075a47a99e026aed5395a6a9c06b663a81e62f67bba7fd196ccf34" } }, { "contract_id": "experiment-evidence-record-v1", "schema_path": "contracts/schemas/experiment-core/experiment-evidence-record-v1.json", "stability": "draft", - "content_hash": "3483ac4ff2cb61278d64253257f70dc73d14eaff3bc5501eee20a86a55d7d707", + "content_hash": "b6b14039002d04329d760a30b26cb191d795de3b49e377edd084be4d10f9c32b", "last_change": { - "summary": "Published the SEM-224 observability/evidence plane (captured_evidence) as a portable x-aces-plane annotation sourced from the carrier-oriented plane classifier.", - "content_hash": "3483ac4ff2cb61278d64253257f70dc73d14eaff3bc5501eee20a86a55d7d707" + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "b6b14039002d04329d760a30b26cb191d795de3b49e377edd084be4d10f9c32b" } }, { "contract_id": "experiment-run-v1", "schema_path": "contracts/schemas/experiment-core/experiment-run-v1.json", "stability": "draft", - "content_hash": "e1e7ca5e74439140cd27340a7754d8b871266e518ce93c98bf6ff0dbaa8bd134", + "content_hash": "1503644443be7e61d5710a6aed9437a66c53a7871eb0ff1541fb2449b684742b", "last_change": { - "summary": "Added SEM-225 run-level augmentation disclosures for processor/backend augmentation classification, visibility, observer-effect, comparability, and evidence provenance.", - "content_hash": "e1e7ca5e74439140cd27340a7754d8b871266e518ce93c98bf6ff0dbaa8bd134" + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "1503644443be7e61d5710a6aed9437a66c53a7871eb0ff1541fb2449b684742b" } }, { "contract_id": "experiment-study-v1", "schema_path": "contracts/schemas/experiment-core/experiment-study-v1.json", "stability": "draft", - "content_hash": "b769512ccf43a3cae303d363da84cf1d4bbd97099d7f9085f065bbe4d801b4d3", + "content_hash": "d180220068358f42d6ae850eccb9991dc9284db4b244ef70960837ea68d9b098", "last_change": { - "summary": "Extended experiment-core references for the EXP-707/EXP-708/EXP-709 evidence and measure contract boundary.", - "content_hash": "b769512ccf43a3cae303d363da84cf1d4bbd97099d7f9085f065bbe4d801b4d3" + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "d180220068358f42d6ae850eccb9991dc9284db4b244ef70960837ea68d9b098" } }, { "contract_id": "experiment-task-v1", "schema_path": "contracts/schemas/experiment-core/experiment-task-v1.json", "stability": "draft", - "content_hash": "45fb2eb011d89b9be057d236b9d4e58c2ce01f8c1365f7d521afc8192d68a73a", + "content_hash": "b82f084841720563089206f9a8dc94b93fde10035da9b3a64662b7660b30d823", + "last_change": { + "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", + "content_hash": "b82f084841720563089206f9a8dc94b93fde10035da9b3a64662b7660b30d823" + } + }, + { + "contract_id": "instantiated-scenario-snapshot-v1", + "schema_path": "contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json", + "stability": "draft", + "content_hash": "8670e3cf3aafc452f25311e4eb98ed82bb89561faaaf368a9232448c8440a3c3", "last_change": { - "summary": "Extended experiment-core references for the EXP-707/EXP-708/EXP-709 evidence and measure contract boundary.", - "content_hash": "45fb2eb011d89b9be057d236b9d4e58c2ce01f8c1365f7d521afc8192d68a73a" + "summary": "Published backend-neutral propositions and assertions in canonical instantiated scenario snapshots.", + "content_hash": "8670e3cf3aafc452f25311e4eb98ed82bb89561faaaf368a9232448c8440a3c3" } }, { "contract_id": "instantiated-scenario-v1", "schema_path": "contracts/schemas/sdl/instantiated-scenario-v1.json", "stability": "draft", - "content_hash": "e0a28239e30855be948b4c3718ae34811de231b3f8d8e146d149d862af0b7cc5", + "content_hash": "58d4f2e69b91ad1639717d7892499bd1b2f007e23c0cb980314cbd701c2cd000", "last_change": { - "summary": "Removed the OCR scoring pipeline (metrics/evaluations/tlos/goals), agents.reward_calculator, entities.tlos, injects.tlos, and workflow-predicate/objective-success scoring references; narrowed objectives.success to conditions (observable state) per ADR-073.", - "content_hash": "e0a28239e30855be948b4c3718ae34811de231b3f8d8e146d149d862af0b7cc5" + "summary": "Published backend-neutral propositions, assertions, role-constrained truth references, and explicit condition-to-proposition bindings.", + "content_hash": "58d4f2e69b91ad1639717d7892499bd1b2f007e23c0cb980314cbd701c2cd000" } }, { @@ -172,13 +206,21 @@ "contract_id": "operation-status-v1", "schema_path": "contracts/schemas/control-plane/operation-status-v1.json", "stability": "draft", - "content_hash": "a4db430e1bef55cc954e9620fdd91897175baeab8d132088647642c02792f0ec" + "content_hash": "fd9af93db03252516e91ffe2cc4eae6c006221b5a10ef6b9998762ec51a5ed53", + "last_change": { + "summary": "Constrained backend-reported changed addresses to unique canonical compiled addresses for DSL-101/DSL-102.", + "content_hash": "fd9af93db03252516e91ffe2cc4eae6c006221b5a10ef6b9998762ec51a5ed53" + } }, { "contract_id": "orchestration-plan-v1", "schema_path": "contracts/schemas/plans/orchestration-plan-v1.json", "stability": "draft", - "content_hash": "9f132b651267f5cafa460879550a49eb18d735ab1e02b241e664ec6c73275bae" + "content_hash": "2ada1a85e8049b7ab52e8c867ab6360fb7fe2833c62ab9469d0b7a49aa0dd2e7", + "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" + } }, { "contract_id": "participant-behavior-history-event-stream-v1", @@ -308,13 +350,41 @@ "contract_id": "processor-manifest-v2", "schema_path": "contracts/schemas/processor-manifest/processor-manifest-v2.json", "stability": "draft", - "content_hash": "f47537f72e1f607b634b0d1f9d8854cc8abd11e0edec7272549d4d910f38993e" + "content_hash": "882ea02e0294d3ed7fb62b6bafec699236a5e3d42f4e22de27107ef3f2c25510", + "last_change": { + "summary": "Added proposition-truth-result-v1 to the processor contract surface.", + "content_hash": "882ea02e0294d3ed7fb62b6bafec699236a5e3d42f4e22de27107ef3f2c25510" + } + }, + { + "contract_id": "proposition-truth-result-v1", + "schema_path": "contracts/schemas/control-plane/proposition-truth-result-v1.json", + "stability": "draft", + "content_hash": "4a37fa964b8a7e3e641f381a27ea6960e9bc1efb8edfe426b9c19e173f7a045f", + "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" + } }, { "contract_id": "provisioning-plan-v1", "schema_path": "contracts/schemas/plans/provisioning-plan-v1.json", "stability": "draft", - "content_hash": "e200fb7e8e68f05eb61c35cc2f724882e3b8977df7f4d5ae97e70401a1a76e01" + "content_hash": "e3d9a1357af4e49b78d591cca10547a4d0113aee794686e0f8d96e32bb940e49", + "last_change": { + "summary": "Added immutable realization-envelope identity carriage and constrained provisioning operations to canonical provision addresses and resource types.", + "content_hash": "e3d9a1357af4e49b78d591cca10547a4d0113aee794686e0f8d96e32bb940e49" + } + }, + { + "contract_id": "realization-envelope-v1", + "schema_path": "contracts/schemas/realization-envelope/realization-envelope-v1.json", + "stability": "draft", + "content_hash": "a5992e81fbc935a7ee6eab3b173a89d6e664274f0f143475e303a54d01a27374", + "last_change": { + "summary": "Added explicit service-concern disclosure so backend envelopes cannot hide declared guest services inside another concern.", + "content_hash": "a5992e81fbc935a7ee6eab3b173a89d6e664274f0f143475e303a54d01a27374" + } }, { "contract_id": "reference-models-v1", @@ -326,36 +396,70 @@ "contract_id": "reusable-asset-trust-policy-v1", "schema_path": "contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json", "stability": "draft", - "content_hash": "68f18e107bddb0b037a316bd334258da7329fc60460ee38ef415c89ce58ad7e0", + "content_hash": "de371c60018cc27664915404443df13dd881a57264a1539028409a14af1823b5", "last_change": { - "summary": "Initial publication of the GOV-913 reusable-asset trust/authenticity/integrity policy contract: per-family evidence-class requirements (integrity/authenticity/provenance/governance) referencing existing ACES mechanisms.", - "content_hash": "68f18e107bddb0b037a316bd334258da7329fc60460ee38ef415c89ce58ad7e0" + "summary": "Added the associated_artifact_set family with required set-integrity and concrete payload-checksum evidence (ADR-077, issue #738).", + "content_hash": "de371c60018cc27664915404443df13dd881a57264a1539028409a14af1823b5" } }, { "contract_id": "runtime-snapshot-v1", "schema_path": "contracts/schemas/snapshots/runtime-snapshot-v1.json", "stability": "draft", - "content_hash": "cb6a599ef5137bcc5954ffd4f451167562186112c7595ec20b50eb94b97e1550", + "content_hash": "4e20e5c91ccd425c88be75a0e55be97f07d311e6499fc6bf73c3db4b37fa51bf", "last_change": { - "summary": "Added first-class joint_action_records and time_management_contexts fields to the runtime snapshot envelope for RUN-308 concurrent participant execution.", - "content_hash": "cb6a599ef5137bcc5954ffd4f451167562186112c7595ec20b50eb94b97e1550" + "summary": "Added portable proposition truth results as a distinct runtime snapshot carrier outside ordinary evaluation result envelopes.", + "content_hash": "4e20e5c91ccd425c88be75a0e55be97f07d311e6499fc6bf73c3db4b37fa51bf" } }, { "contract_id": "scenario-instantiation-request-v1", "schema_path": "contracts/schemas/sdl/scenario-instantiation-request-v1.json", "stability": "draft", - "content_hash": "72a4a1969bc39d551efd0be364804dcdfca7694eceb2a58c402a026d981cd470" + "content_hash": "130a103664dd69819c1dbe13437e94436dfb2b5db871f135e9f1750c741b412f", + "last_change": { + "summary": "Constrained instantiation parameter names to portable local identifiers while leaving parameter values outside declaration identity for DSL-101.", + "content_hash": "130a103664dd69819c1dbe13437e94436dfb2b5db871f135e9f1750c741b412f" + } + }, + { + "contract_id": "scientific-completeness-assessment-v1", + "schema_path": "contracts/schemas/profiles/scientific-completeness-assessment-v1.json", + "stability": "draft", + "content_hash": "f70a3b3769982ec179fd0d231829ebef4f2cd4c003d62ad26f813848330eee2d", + "last_change": { + "summary": "Initial publication of the separately revisioned REV1 concern-delivery assessment, evidence, limitation, satisfiability-witness, and external-binding contract for GOV-942.", + "content_hash": "f70a3b3769982ec179fd0d231829ebef4f2cd4c003d62ad26f813848330eee2d" + } + }, + { + "contract_id": "scientific-completeness-taxonomy-v1", + "schema_path": "contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json", + "stability": "draft", + "content_hash": "87eabab61eb27b6d66288ebce40b628e7405ba3c66ad116fd98b493c4b9bff21", + "last_change": { + "summary": "Initial publication of the REV1 scientific-scenario completeness taxonomy, atomic concern, intended-use disposition, and explicit non-claim contract for GOV-942.", + "content_hash": "87eabab61eb27b6d66288ebce40b628e7405ba3c66ad116fd98b493c4b9bff21" + } }, { "contract_id": "sdl-authoring-input-v1", "schema_path": "contracts/schemas/sdl/sdl-authoring-input-v1.json", "stability": "draft", - "content_hash": "820fbd6619a100e382d722890edcdde01da977158fd86f958f4928a9d32ad51a", + "content_hash": "17f941e916ffcbeb3e36b894029332326257bc7e4d1ab3419bc6e06c7f06f801", + "last_change": { + "summary": "Published backend-neutral proposition and assertion authoring, role-constrained objective/event/workflow/participant references, and explicit probe bindings.", + "content_hash": "17f941e916ffcbeb3e36b894029332326257bc7e4d1ab3419bc6e06c7f06f801" + } + }, + { + "contract_id": "sdl-lineage-ledger-v1", + "schema_path": "contracts/schemas/provenance/sdl-lineage-ledger-v1.json", + "stability": "draft", + "content_hash": "ac8a7737debd83fe70cfe791a85f72455d6e474c806e3faf9692cdce38123e6d", "last_change": { - "summary": "Removed the OCR scoring pipeline (metrics/evaluations/tlos/goals), agents.reward_calculator, entities.tlos, injects.tlos, and workflow-predicate/objective-success scoring references; narrowed objectives.success to conditions (observable state) per ADR-073.", - "content_hash": "820fbd6619a100e382d722890edcdde01da977158fd86f958f4928a9d32ad51a" + "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" } }, { diff --git a/contracts/schemas/README.md b/contracts/schemas/README.md index 64a8adffb..e5cf6f1f1 100644 --- a/contracts/schemas/README.md +++ b/contracts/schemas/README.md @@ -8,10 +8,10 @@ be the home of the authoritative machine-readable artifacts, independent of any single implementation language or package layout. Current published schemas cover: -- SDL authoring input -- instantiated scenarios -- backend manifests (`v1` legacy plus shared-apparatus `v2`) -- processor manifests (`v1` legacy plus shared-apparatus `v2`) +- SDL normalized authoring objects (not raw YAML presentation) +- instantiated scenarios and canonical instantiated snapshots +- backend manifests (shared-apparatus `v2`) +- processor manifests (shared-apparatus `v2`) - concept-authority catalogs - reference model catalogs - UCO alignment evidence @@ -28,6 +28,32 @@ Current published schemas cover: - experiment-core task, run, apparatus-context, study/collection, capture specification, raw evidence, and derived measure contracts +`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 +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 +`contracts/fixtures/sdl/sdl-yaml-v1/`; JSON Schema cannot express them. + +`sdl/instantiated-scenario-v1.json` is a distinct closed derived-artifact +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 +`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 +corresponding valid and invalid fixtures live under +`contracts/fixtures/sdl/instantiated-scenario-v1/` and +`instantiated-scenario-snapshot-v1/`. + +These schemas enforce portable structure and scalar constraints. Cross-field +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. + 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 @@ -53,9 +79,9 @@ particular: - backend capability blocks must declare concrete provisioning and orchestration surfaces rather than empty shells -`v1` backend and processor manifests remain checked in as deprecated legacy -schema artifacts. The reference stack, contract tests, and conformance profiles -use `v2`. +Only the shared-apparatus `v2` backend and processor manifests are published; +no `v1` backend or processor manifest schema is checked in. The reference +stack, contract tests, and conformance profiles use `v2`. ## Concept Authority Catalog @@ -92,6 +118,12 @@ assumptions required across authoring, exchange, processing, and execution phases. The initial profile lives at `contracts/profiles/semantic/reference-stack-v1.json`. + +The `scientific-completeness-taxonomy-v1` and +`scientific-completeness-assessment-v1` schemas keep stable intended-use +profiles separate from time-varying delivery evidence. Their normative +artifacts live under `contracts/profiles/scientific-completeness/`; neither is +an SDL validation-strength profile or a backend capability declaration. Its processing and execution phases also declare required concept bindings for the governed apparatus-manifest vocabulary surfaces introduced by GOV-918. 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 efffe7da7..611448ffd 100644 --- a/contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json +++ b/contracts/schemas/asset-trust/reusable-asset-trust-policy-v1.json @@ -267,6 +267,7 @@ "asset_family": { "enum": [ "reusable_scenario", + "associated_artifact_set", "sdl_module", "experiment_task", "experiment_study", @@ -331,6 +332,19 @@ "type": "object" } }, + { + "contains": { + "properties": { + "asset_family": { + "const": "associated_artifact_set" + } + }, + "required": [ + "asset_family" + ], + "type": "object" + } + }, { "contains": { "properties": { @@ -413,8 +427,8 @@ "items": { "$ref": "#/$defs/ReusableAssetFamilyTrustPolicyModel" }, - "maxItems": 7, - "minItems": 7, + "maxItems": 8, + "minItems": 8, "title": "Families", "type": "array" }, diff --git a/contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json b/contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json new file mode 100644 index 000000000..cf42abdd8 --- /dev/null +++ b/contracts/schemas/associated-artifacts/associated-artifact-manifest-v1.json @@ -0,0 +1,475 @@ +{ + "$defs": { + "AssociatedArtifactParentReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a supported associated-artifact parent.", + "properties": { + "ref_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": "Ref Digest" + }, + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "enum": [ + "scenario", + "scenario-snapshot", + "task", + "authoring-input", + "apparatus-context", + "run", + "study" + ], + "title": "Ref Kind", + "type": "string" + }, + "ref_path": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Path" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "AssociatedArtifactParentReferenceModel", + "type": "object" + }, + "ExperimentArtifactRefModel": { + "additionalProperties": false, + "description": "Reference to an artifact that supports a task, run, apparatus, or study.", + "properties": { + "artifact_id": { + "minLength": 1, + "title": "Artifact Id", + "type": "string" + }, + "checksum": { + "$ref": "#/$defs/ExperimentChecksumModel" + }, + "created_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": "Created At", + "type": "string" + }, + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "media_type": { + "minLength": 1, + "title": "Media Type", + "type": "string" + }, + "role": { + "enum": [ + "protocol", + "metric-definition", + "scenario-snapshot", + "manifest", + "apparatus-evidence", + "observation", + "result", + "analysis", + "report", + "export", + "starter-file", + "evaluator", + "subtask", + "gold-step", + "milestone", + "human-assistance", + "scaffold", + "baseline", + "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", + "other" + ], + "title": "Role", + "type": "string" + }, + "satisfies_refs": { + "items": { + "$ref": "#/$defs/ExperimentEvidenceSatisfactionReferenceModel" + }, + "title": "Satisfies Refs", + "type": "array" + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "restricted", + "redacted" + ], + "title": "Sensitivity", + "type": "string" + }, + "size_bytes": { + "minimum": 0, + "title": "Size Bytes", + "type": "integer" + }, + "source": { + "minLength": 1, + "title": "Source", + "type": "string" + }, + "uri": { + "minLength": 1, + "title": "Uri", + "type": "string" + } + }, + "required": [ + "artifact_id", + "role", + "media_type", + "uri", + "checksum", + "size_bytes", + "created_at", + "source", + "sensitivity" + ], + "title": "ExperimentArtifactRefModel", + "type": "object" + }, + "ExperimentChecksumModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "algorithm": { + "const": "sha256" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{64}$" + } + } + } + }, + { + "if": { + "properties": { + "algorithm": { + "const": "sha384" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{96}$" + } + } + } + }, + { + "if": { + "properties": { + "algorithm": { + "const": "sha512" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{128}$" + } + } + } + }, + { + "if": { + "properties": { + "algorithm": { + "const": "blake3" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{64}$" + } + } + } + } + ], + "description": "Checksum metadata for an experiment-core artifact reference.", + "properties": { + "algorithm": { + "enum": [ + "sha256", + "sha384", + "sha512", + "blake3" + ], + "title": "Algorithm", + "type": "string" + }, + "value": { + "minLength": 1, + "pattern": "^[A-Fa-f0-9]+$", + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ], + "title": "ExperimentChecksumModel", + "type": "object" + }, + "ExperimentEvidenceSatisfactionReferenceModel": { + "additionalProperties": false, + "description": "Evidence concept reference that an artifact claims to satisfy.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "evidence", + "title": "Ref Kind", + "type": "string" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentEvidenceSatisfactionReferenceModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/associated-artifact-manifest-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "scope": { + "const": "scenario" + } + }, + "required": [ + "scope" + ] + }, + "then": { + "properties": { + "parent_ref": { + "properties": { + "ref_kind": { + "enum": [ + "scenario", + "scenario-snapshot" + ] + } + } + } + } + } + }, + { + "if": { + "properties": { + "scope": { + "const": "experiment" + } + }, + "required": [ + "scope" + ] + }, + "then": { + "properties": { + "parent_ref": { + "properties": { + "ref_kind": { + "enum": [ + "task", + "authoring-input", + "apparatus-context", + "run", + "study" + ] + } + } + } + } + } + } + ], + "description": "One exact non-semantic artifact-reference set attached to one parent.", + "properties": { + "artifacts": { + "additionalProperties": { + "$ref": "#/$defs/ExperimentArtifactRefModel" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Artifacts", + "type": "object" + }, + "canonicalization_profile": { + "const": "associated-artifact-set/v1", + "title": "Canonicalization Profile", + "type": "string" + }, + "manifest_id": { + "minLength": 1, + "title": "Manifest Id", + "type": "string" + }, + "manifest_version": { + "minLength": 1, + "title": "Manifest Version", + "type": "string" + }, + "parent_ref": { + "$ref": "#/$defs/AssociatedArtifactParentReferenceModel" + }, + "schema_version": { + "const": "associated-artifact-manifest/v1", + "title": "Schema Version", + "type": "string" + }, + "scope": { + "enum": [ + "scenario", + "experiment" + ], + "title": "Scope", + "type": "string" + }, + "set_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Set Digest", + "type": "string" + } + }, + "required": [ + "schema_version", + "manifest_id", + "manifest_version", + "canonicalization_profile", + "scope", + "parent_ref", + "artifacts", + "set_digest" + ], + "title": "AssociatedArtifactManifestModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "associated-artifact-manifest-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.associated_artifacts.validate_associated_artifact_manifest" + } + ], + "x-aces-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", + "required": true, + "uri": "https://aces.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 26ca658cc..5aec46db7 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -164,6 +164,35 @@ "title": "Name", "type": "string" }, + "preserves_binding_provenance": { + "default": false, + "title": "Preserves Binding Provenance", + "type": "boolean" + }, + "supported_evidence_channels": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Supported Evidence Channels", + "type": "array" + }, + "supported_predicate_families": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Supported Predicate Families", + "type": "array" + }, + "supported_quantifiers": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Supported Quantifiers", + "type": "array" + }, "supported_sections": { "items": { "minLength": 1, @@ -173,6 +202,22 @@ "title": "Supported Sections", "type": "array" }, + "supported_time_domains": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Supported Time Domains", + "type": "array" + }, + "supported_truth_outcomes": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Supported Truth Outcomes", + "type": "array" + }, "supports_objectives": { "default": true, "title": "Supports Objectives", @@ -381,9 +426,9 @@ "title": "Supported Workflow State Predicates", "type": "array" }, - "supports_condition_refs": { + "supports_assertion_refs": { "default": true, - "title": "Supports Condition Refs", + "title": "Supports Assertion Refs", "type": "boolean" }, "supports_inject_bindings": { @@ -666,6 +711,46 @@ "title": "ProvisionerCapabilitiesModel", "type": "object" }, + "RealizationEnvelopeIdentityModel": { + "additionalProperties": false, + "description": "Immutable realization-envelope identity carried across runtime contracts.", + "properties": { + "configuration_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Configuration Digest", + "type": "string" + }, + "contract_id": { + "const": "realization-envelope-v1", + "default": "realization-envelope-v1", + "title": "Contract Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "envelope_id": { + "minLength": 1, + "title": "Envelope Id", + "type": "string" + }, + "schema_version": { + "const": "realization-envelope/v1", + "default": "realization-envelope/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "envelope_id", + "digest", + "configuration_digest" + ], + "title": "RealizationEnvelopeIdentityModel", + "type": "object" + }, "RealizationSupportDeclarationModel": { "additionalProperties": false, "allOf": [ @@ -808,6 +893,57 @@ "$id": "https://aces.dev/schemas/backend-manifest-v2.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "realization_envelope": { + "not": { + "type": "null" + } + } + }, + "required": [ + "realization_envelope" + ] + }, + "then": { + "properties": { + "supported_contract_versions": { + "contains": { + "const": "realization-envelope-v1" + } + } + } + } + }, + { + "if": { + "properties": { + "supported_contract_versions": { + "contains": { + "const": "realization-envelope-v1" + } + } + }, + "required": [ + "supported_contract_versions" + ] + }, + "then": { + "properties": { + "realization_envelope": { + "not": { + "type": "null" + } + } + }, + "required": [ + "realization_envelope" + ] + } + } + ], "properties": { "capabilities": { "$ref": "#/$defs/BackendCapabilitiesV2Model" @@ -833,6 +969,17 @@ "identity": { "$ref": "#/$defs/ApparatusIdentityModel" }, + "realization_envelope": { + "anyOf": [ + { + "$ref": "#/$defs/RealizationEnvelopeIdentityModel" + }, + { + "type": "null" + } + ], + "default": null + }, "realization_support": { "items": { "$ref": "#/$defs/RealizationSupportDeclarationModel" @@ -851,6 +998,7 @@ "items": { "enum": [ "backend-manifest-v2", + "realization-envelope-v1", "provisioning-plan-v1", "orchestration-plan-v1", "evaluation-plan-v1", @@ -860,6 +1008,7 @@ "workflow-result-envelope-v1", "workflow-history-event-stream-v1", "evaluation-result-envelope-v1", + "proposition-truth-result-v1", "evaluation-history-event-stream-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", diff --git a/contracts/schemas/control-plane/operation-status-v1.json b/contracts/schemas/control-plane/operation-status-v1.json index 952b3b9c4..ab21c4175 100644 --- a/contracts/schemas/control-plane/operation-status-v1.json +++ b/contracts/schemas/control-plane/operation-status-v1.json @@ -5,6 +5,12 @@ "properties": { "changed_addresses": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Changed Addresses", diff --git a/contracts/schemas/control-plane/proposition-truth-result-v1.json b/contracts/schemas/control-plane/proposition-truth-result-v1.json new file mode 100644 index 000000000..4c0a76a30 --- /dev/null +++ b/contracts/schemas/control-plane/proposition-truth-result-v1.json @@ -0,0 +1,402 @@ +{ + "$defs": { + "PropositionAssertionPolarity": { + "enum": [ + "positive", + "negative" + ], + "title": "PropositionAssertionPolarity", + "type": "string" + }, + "PropositionEvaluationBasis": { + "enum": [ + "declared_state", + "observed_state" + ], + "title": "PropositionEvaluationBasis", + "type": "string" + }, + "PropositionIndeterminacyReason": { + "enum": [ + "missing_evidence", + "stale_evidence", + "partial_evidence", + "conflicting_evidence", + "redacted_evidence", + "lossy_evidence", + "probe_failure" + ], + "title": "PropositionIndeterminacyReason", + "type": "string" + }, + "PropositionLossDisclosureModel": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/$defs/PropositionLossKind" + }, + "within_admissible_bound": { + "title": "Within Admissible Bound", + "type": "boolean" + } + }, + "required": [ + "kind", + "within_admissible_bound" + ], + "title": "PropositionLossDisclosureModel", + "type": "object" + }, + "PropositionLossKind": { + "enum": [ + "missing", + "stale", + "partial", + "conflicting", + "redacted", + "lossy", + "probe_failure" + ], + "title": "PropositionLossKind", + "type": "string" + }, + "PropositionProbeBindingModel": { + "additionalProperties": false, + "description": "Provenance for one capability-bound proposition realization.", + "properties": { + "artifact_digest": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Artifact Digest", + "type": "string" + }, + "backend_manifest_ref": { + "minLength": 1, + "title": "Backend Manifest Ref", + "type": "string" + }, + "binding_id": { + "minLength": 1, + "title": "Binding Id", + "type": "string" + }, + "capability_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Capability Refs", + "type": "array" + }, + "implementation_id": { + "minLength": 1, + "title": "Implementation Id", + "type": "string" + }, + "implementation_version": { + "minLength": 1, + "title": "Implementation Version", + "type": "string" + }, + "proposition_address": { + "minLength": 1, + "title": "Proposition Address", + "type": "string" + } + }, + "required": [ + "binding_id", + "implementation_id", + "implementation_version", + "artifact_digest", + "backend_manifest_ref", + "proposition_address", + "capability_refs" + ], + "title": "PropositionProbeBindingModel", + "type": "object" + }, + "PropositionTemporalContextModel": { + "additionalProperties": false, + "description": "Governed boundary and clock context used for one evaluation.", + "properties": { + "boundary_ref": { + "minLength": 1, + "title": "Boundary Ref", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "time_domain": { + "minLength": 1, + "title": "Time Domain", + "type": "string" + } + }, + "required": [ + "boundary_ref", + "time_domain", + "clock_authority" + ], + "title": "PropositionTemporalContextModel", + "type": "object" + }, + "PropositionTruthOutcome": { + "description": "Portable proposition-evaluation outcome domain.\n\n``unsupported`` is a realization disposition included in the portable\nenvelope, not a logical truth value.", + "enum": [ + "true", + "false", + "unknown", + "unsupported" + ], + "title": "PropositionTruthOutcome", + "type": "string" + } + }, + "$id": "https://aces.dev/schemas/proposition-truth-result-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "evaluation_basis": { + "const": "observed_state" + }, + "proposition_outcome": { + "enum": [ + "true", + "false" + ] + } + }, + "required": [ + "evaluation_basis", + "proposition_outcome" + ] + }, + "then": { + "properties": { + "evidence_refs": { + "minItems": 1 + }, + "probe_binding": { + "not": { + "type": "null" + } + }, + "temporal_context": { + "not": { + "type": "null" + } + } + } + } + }, + { + "else": { + "properties": { + "indeterminacy_reason": { + "type": "null" + } + } + }, + "if": { + "properties": { + "proposition_outcome": { + "const": "unknown" + } + }, + "required": [ + "proposition_outcome" + ] + }, + "then": { + "properties": { + "indeterminacy_reason": { + "not": { + "type": "null" + } + } + } + } + }, + { + "else": { + "properties": { + "unsupported_capability_refs": { + "maxItems": 0 + } + } + }, + "if": { + "properties": { + "proposition_outcome": { + "const": "unsupported" + } + }, + "required": [ + "proposition_outcome" + ] + }, + "then": { + "properties": { + "unsupported_capability_refs": { + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "proposition_outcome": { + "enum": [ + "true", + "false" + ] + } + }, + "required": [ + "proposition_outcome" + ] + }, + "then": { + "properties": { + "loss_disclosures": { + "items": { + "$ref": "#/$defs/PropositionLossDisclosureModel", + "properties": { + "within_admissible_bound": { + "const": true + } + }, + "required": [ + "within_admissible_bound" + ] + } + } + } + } + } + ], + "description": "Evidence-bearing truth result separate from evaluator lifecycle state.", + "properties": { + "assertion_address": { + "minLength": 1, + "title": "Assertion Address", + "type": "string" + }, + "assertion_outcome": { + "$ref": "#/$defs/PropositionTruthOutcome" + }, + "assertion_polarity": { + "$ref": "#/$defs/PropositionAssertionPolarity" + }, + "declared_artifact_digest": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declared Artifact Digest" + }, + "evaluation_basis": { + "$ref": "#/$defs/PropositionEvaluationBasis" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "indeterminacy_reason": { + "anyOf": [ + { + "$ref": "#/$defs/PropositionIndeterminacyReason" + }, + { + "type": "null" + } + ], + "default": null + }, + "loss_disclosures": { + "items": { + "$ref": "#/$defs/PropositionLossDisclosureModel" + }, + "title": "Loss Disclosures", + "type": "array" + }, + "probe_binding": { + "anyOf": [ + { + "$ref": "#/$defs/PropositionProbeBindingModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "proposition_address": { + "minLength": 1, + "title": "Proposition Address", + "type": "string" + }, + "proposition_outcome": { + "$ref": "#/$defs/PropositionTruthOutcome" + }, + "result_id": { + "minLength": 1, + "title": "Result Id", + "type": "string" + }, + "schema_version": { + "const": "proposition-truth-result/v1", + "default": "proposition-truth-result/v1", + "title": "Schema Version", + "type": "string" + }, + "temporal_context": { + "anyOf": [ + { + "$ref": "#/$defs/PropositionTemporalContextModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "unsupported_capability_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Unsupported Capability Refs", + "type": "array" + } + }, + "required": [ + "result_id", + "proposition_address", + "assertion_address", + "assertion_polarity", + "proposition_outcome", + "assertion_outcome", + "evaluation_basis" + ], + "title": "PropositionTruthResultModel", + "type": "object" +} diff --git a/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json b/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json index 01dee6072..a89e2f323 100644 --- a/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json +++ b/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json @@ -179,6 +179,11 @@ "scaffold", "baseline", "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", "other" ], "title": "Role", @@ -790,6 +795,7 @@ "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", diff --git a/contracts/schemas/experiment-core/experiment-authoring-input-v1.json b/contracts/schemas/experiment-core/experiment-authoring-input-v1.json new file mode 100644 index 000000000..ed49baec9 --- /dev/null +++ b/contracts/schemas/experiment-core/experiment-authoring-input-v1.json @@ -0,0 +1,1870 @@ +{ + "$defs": { + "ExperimentApparatusConstraintModel": { + "additionalProperties": false, + "anyOf": [ + { + "properties": { + "allowed_processor_refs": { + "minItems": 1 + } + }, + "required": [ + "allowed_processor_refs" + ] + }, + { + "properties": { + "allowed_backend_refs": { + "minItems": 1 + } + }, + "required": [ + "allowed_backend_refs" + ] + }, + { + "properties": { + "required_manifest_refs": { + "minItems": 1 + } + }, + "required": [ + "required_manifest_refs" + ] + }, + { + "properties": { + "required_capabilities": { + "minItems": 1 + } + }, + "required": [ + "required_capabilities" + ] + }, + { + "properties": { + "notes": { + "minItems": 1 + } + }, + "required": [ + "notes" + ] + } + ], + "description": "Apparatus compatibility and capability constraints for a task.", + "properties": { + "allowed_backend_refs": { + "items": { + "$ref": "#/$defs/ExperimentBackendReferenceModel" + }, + "title": "Allowed Backend Refs", + "type": "array" + }, + "allowed_processor_refs": { + "items": { + "$ref": "#/$defs/ExperimentProcessorReferenceModel" + }, + "title": "Allowed Processor Refs", + "type": "array" + }, + "notes": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "required_capabilities": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Required Capabilities", + "type": "array" + }, + "required_manifest_refs": { + "items": { + "$ref": "#/$defs/ExperimentManifestReferenceModel" + }, + "title": "Required Manifest Refs", + "type": "array" + } + }, + "title": "ExperimentApparatusConstraintModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "experiment-task-v1", + "instance_path": "#/apparatus_constraints" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentApparatusConstraintModel._validate_allowed_identity_manifest_refs" + } + ] + }, + "ExperimentArtifactRefModel": { + "additionalProperties": false, + "description": "Reference to an artifact that supports a task, run, apparatus, or study.", + "properties": { + "artifact_id": { + "minLength": 1, + "title": "Artifact Id", + "type": "string" + }, + "checksum": { + "$ref": "#/$defs/ExperimentChecksumModel" + }, + "created_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": "Created At", + "type": "string" + }, + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "media_type": { + "minLength": 1, + "title": "Media Type", + "type": "string" + }, + "role": { + "enum": [ + "protocol", + "metric-definition", + "scenario-snapshot", + "manifest", + "apparatus-evidence", + "observation", + "result", + "analysis", + "report", + "export", + "starter-file", + "evaluator", + "subtask", + "gold-step", + "milestone", + "human-assistance", + "scaffold", + "baseline", + "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", + "other" + ], + "title": "Role", + "type": "string" + }, + "satisfies_refs": { + "items": { + "$ref": "#/$defs/ExperimentEvidenceSatisfactionReferenceModel" + }, + "title": "Satisfies Refs", + "type": "array" + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "restricted", + "redacted" + ], + "title": "Sensitivity", + "type": "string" + }, + "size_bytes": { + "minimum": 0, + "title": "Size Bytes", + "type": "integer" + }, + "source": { + "minLength": 1, + "title": "Source", + "type": "string" + }, + "uri": { + "minLength": 1, + "title": "Uri", + "type": "string" + } + }, + "required": [ + "artifact_id", + "role", + "media_type", + "uri", + "checksum", + "size_bytes", + "created_at", + "source", + "sensitivity" + ], + "title": "ExperimentArtifactRefModel", + "type": "object" + }, + "ExperimentBackendReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a backend identity.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "backend", + "title": "Ref Kind", + "type": "string" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentBackendReferenceModel", + "type": "object" + }, + "ExperimentCaptureSpecReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a declarative capture specification.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "capture-spec", + "title": "Ref Kind", + "type": "string" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentCaptureSpecReferenceModel", + "type": "object" + }, + "ExperimentChecksumModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "algorithm": { + "const": "sha256" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{64}$" + } + } + } + }, + { + "if": { + "properties": { + "algorithm": { + "const": "sha384" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{96}$" + } + } + } + }, + { + "if": { + "properties": { + "algorithm": { + "const": "sha512" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{128}$" + } + } + } + }, + { + "if": { + "properties": { + "algorithm": { + "const": "blake3" + } + }, + "required": [ + "algorithm" + ] + }, + "then": { + "properties": { + "value": { + "pattern": "^[A-Fa-f0-9]{64}$" + } + } + } + } + ], + "description": "Checksum metadata for an experiment-core artifact reference.", + "properties": { + "algorithm": { + "enum": [ + "sha256", + "sha384", + "sha512", + "blake3" + ], + "title": "Algorithm", + "type": "string" + }, + "value": { + "minLength": 1, + "pattern": "^[A-Fa-f0-9]+$", + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ], + "title": "ExperimentChecksumModel", + "type": "object" + }, + "ExperimentClockContextModel": { + "additionalProperties": false, + "description": "Clock authority and time-domain metadata for run interpretation.", + "properties": { + "authority": { + "minLength": 1, + "title": "Authority", + "type": "string" + }, + "clock_id": { + "minLength": 1, + "title": "Clock Id", + "type": "string" + }, + "synchronization": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Synchronization" + }, + "time_domain": { + "enum": [ + "wall-clock", + "monotonic", + "simulated", + "logical", + "other" + ], + "title": "Time Domain", + "type": "string" + } + }, + "required": [ + "clock_id", + "authority", + "time_domain" + ], + "title": "ExperimentClockContextModel", + "type": "object" + }, + "ExperimentConditionAssignmentModel": { + "additionalProperties": false, + "anyOf": [ + { + "properties": { + "required_refs": { + "minItems": 1 + } + }, + "required": [ + "required_refs" + ] + }, + { + "properties": { + "required_parameters": { + "minItems": 1 + } + }, + "required": [ + "required_parameters" + ] + } + ], + "description": "Concrete treatment-condition assignment criteria for study evaluation runs.", + "properties": { + "condition_id": { + "minLength": 1, + "title": "Condition Id", + "type": "string" + }, + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "factor_levels": { + "additionalProperties": { + "minLength": 1, + "type": "string" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Factor Levels", + "type": "object" + }, + "required_parameters": { + "items": { + "$ref": "#/$defs/ExperimentConditionAssignmentParameterModel" + }, + "title": "Required Parameters", + "type": "array" + }, + "required_refs": { + "items": { + "$ref": "#/$defs/ExperimentConditionAssignmentReferenceModel" + }, + "title": "Required Refs", + "type": "array" + } + }, + "required": [ + "condition_id", + "factor_levels" + ], + "title": "ExperimentConditionAssignmentModel", + "type": "object" + }, + "ExperimentConditionAssignmentParameterModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "redaction": { + "enum": [ + "redacted", + "withheld" + ] + } + }, + "required": [ + "redaction" + ] + }, + "then": { + "properties": { + "value": { + "type": "null" + } + } + } + } + ], + "description": "Auditable parameter value that can ground a study condition assignment.", + "properties": { + "name": { + "minLength": 1, + "title": "Name", + "type": "string" + }, + "redaction": { + "const": "none", + "default": "none", + "title": "Redaction", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Value" + }, + "value_kind": { + "enum": [ + "configuration", + "protocol", + "apparatus", + "analysis" + ], + "title": "Value Kind", + "type": "string" + } + }, + "required": [ + "name", + "value", + "value_kind" + ], + "title": "ExperimentConditionAssignmentParameterModel", + "type": "object" + }, + "ExperimentConditionAssignmentReferenceModel": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "ref_digest": { + "type": "null" + }, + "ref_path": { + "type": "null" + } + } + } + ], + "description": "Auditable run-level reference that can ground a study condition assignment.", + "properties": { + "ref_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": "Ref Digest" + }, + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "enum": [ + "processor", + "backend", + "participant-implementation", + "scenario-snapshot", + "task", + "apparatus-context", + "manifest", + "profile", + "capability", + "measurement-channel" + ], + "title": "Ref Kind", + "type": "string" + }, + "ref_path": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Path" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentConditionAssignmentReferenceModel", + "type": "object" + }, + "ExperimentEpisodeControlModel": { + "additionalProperties": false, + "description": "Declarative episode execution controls for a planned experiment.\n\nCaptures the pre-run execution-control facts \u2014 turn order, logical step\ncount, and episode termination \u2014 that ADR-069 requires for CAGE-2\nexecution-control equivalence but that the archival experiment-core\ncontracts only record after a run has executed.", + "properties": { + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "max_steps": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Steps" + }, + "termination_condition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Termination Condition Refs", + "type": "array", + "uniqueItems": true + }, + "termination_rule": { + "minLength": 1, + "title": "Termination Rule", + "type": "string" + }, + "turn_order": { + "enum": [ + "sequential", + "simultaneous", + "round-robin", + "scenario-defined", + "other" + ], + "title": "Turn Order", + "type": "string" + } + }, + "required": [ + "turn_order", + "termination_rule" + ], + "title": "ExperimentEpisodeControlModel", + "type": "object" + }, + "ExperimentEvidenceSatisfactionReferenceModel": { + "additionalProperties": false, + "description": "Evidence concept reference that an artifact claims to satisfy.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "evidence", + "title": "Ref Kind", + "type": "string" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentEvidenceSatisfactionReferenceModel", + "type": "object" + }, + "ExperimentManifestReferenceModel": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "ref_path": { + "type": "null" + } + } + }, + { + "if": { + "properties": { + "ref_digest": { + "type": "string" + } + }, + "required": [ + "ref_digest" + ] + }, + "then": { + "properties": { + "subject_ref": { + "properties": { + "ref_kind": { + "enum": [ + "processor", + "backend" + ] + } + }, + "required": [ + "ref_kind" + ] + } + }, + "required": [ + "subject_ref" + ] + } + }, + { + "if": { + "properties": { + "ref_digest": { + "type": "string" + }, + "subject_ref": { + "properties": { + "ref_kind": { + "const": "processor" + } + }, + "required": [ + "ref_kind" + ] + } + }, + "required": [ + "ref_digest", + "subject_ref" + ] + }, + "then": { + "properties": { + "ref_version": { + "const": "processor-manifest/v2" + } + } + } + }, + { + "if": { + "properties": { + "ref_digest": { + "type": "string" + }, + "subject_ref": { + "properties": { + "ref_kind": { + "const": "backend" + } + }, + "required": [ + "ref_kind" + ] + } + }, + "required": [ + "ref_digest", + "subject_ref" + ] + }, + "then": { + "properties": { + "ref_version": { + "const": "backend-manifest/v2" + } + } + } + }, + { + "if": { + "properties": { + "subject_ref": { + "type": "object" + } + }, + "required": [ + "subject_ref" + ] + }, + "then": { + "properties": { + "subject_ref": { + "properties": { + "ref_digest": { + "type": "null" + }, + "ref_path": { + "type": "null" + } + } + } + } + } + } + ], + "description": "Reference constrained to an apparatus or capability manifest.", + "properties": { + "ref_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": "Ref Digest" + }, + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "manifest", + "title": "Ref Kind", + "type": "string" + }, + "ref_path": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Path" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + }, + "subject_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentManifestReferenceModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "experiment-task-v1", + "instance_path": "#/$defs/ExperimentManifestReferenceModel" + }, + { + "contract_id": "experiment-apparatus-context-v1", + "instance_path": "#/$defs/ExperimentManifestReferenceModel" + }, + { + "contract_id": "experiment-run-v1", + "instance_path": "#/$defs/ExperimentManifestReferenceModel" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentManifestReferenceModel._validate_manifest_reference_scope" + } + ] + }, + "ExperimentParameterModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "redaction": { + "enum": [ + "redacted", + "withheld" + ] + } + }, + "required": [ + "redaction" + ] + }, + "then": { + "properties": { + "value": { + "type": "null" + } + } + } + } + ], + "description": "Redaction-aware parameter captured for a task, run, or apparatus context.", + "properties": { + "name": { + "minLength": 1, + "title": "Name", + "type": "string" + }, + "redaction": { + "default": "none", + "enum": [ + "none", + "redacted", + "withheld" + ], + "title": "Redaction", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Value" + }, + "value_kind": { + "enum": [ + "configuration", + "protocol", + "apparatus", + "analysis", + "other" + ], + "title": "Value Kind", + "type": "string" + } + }, + "required": [ + "name", + "value", + "value_kind" + ], + "title": "ExperimentParameterModel", + "type": "object" + }, + "ExperimentProcessorReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a processor identity.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "processor", + "title": "Ref Kind", + "type": "string" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentProcessorReferenceModel", + "type": "object" + }, + "ExperimentRedVariantSelectionModel": { + "additionalProperties": false, + "description": "Selection of one red-agent variant bound into a planned experiment.", + "properties": { + "agent_ref": { + "minLength": 1, + "title": "Agent Ref", + "type": "string" + }, + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "parameters": { + "items": { + "$ref": "#/$defs/ExperimentParameterModel" + }, + "title": "Parameters", + "type": "array" + }, + "variant_id": { + "minLength": 1, + "title": "Variant Id", + "type": "string" + } + }, + "required": [ + "variant_id", + "agent_ref" + ], + "title": "ExperimentRedVariantSelectionModel", + "type": "object" + }, + "ExperimentReferenceModel": { + "additionalProperties": false, + "description": "Typed reference to an experiment-core or adjacent ACES artifact.", + "properties": { + "ref_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": "Ref Digest" + }, + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "enum": [ + "processor", + "backend", + "participant-implementation", + "scenario", + "scenario-snapshot", + "task", + "authoring-input", + "protocol", + "apparatus-context", + "run", + "metric-definition", + "result", + "study", + "manifest", + "profile", + "capability", + "capture-spec", + "evidence", + "evidence-record", + "derived-measure", + "measurement-channel", + "analysis-artifact", + "other" + ], + "title": "Ref Kind", + "type": "string" + }, + "ref_path": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Path" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentReferenceModel", + "type": "object" + }, + "ExperimentRunAllocationPlanModel": { + "additionalProperties": false, + "description": "Structured run allocation, replication, and assignment plan.", + "properties": { + "allocation_method": { + "minLength": 1, + "title": "Allocation Method", + "type": "string" + }, + "allocation_unit": { + "minLength": 1, + "title": "Allocation Unit", + "type": "string" + }, + "blocking_factors": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Blocking Factors", + "type": "array", + "uniqueItems": true + }, + "compared_conditions": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Compared Conditions", + "type": "array", + "uniqueItems": true + }, + "condition_assignments": { + "additionalProperties": { + "$ref": "#/$defs/ExperimentConditionAssignmentModel" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Condition Assignments", + "type": "object" + }, + "randomization_unit": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Randomization Unit" + }, + "replication_policy": { + "minLength": 1, + "title": "Replication Policy", + "type": "string" + }, + "stopping_rule": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stopping Rule" + }, + "target_runs_per_condition": { + "minimum": 1, + "title": "Target Runs Per Condition", + "type": "integer" + } + }, + "required": [ + "allocation_unit", + "allocation_method", + "compared_conditions", + "condition_assignments", + "target_runs_per_condition", + "replication_policy" + ], + "title": "ExperimentRunAllocationPlanModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "experiment-study-v1", + "instance_path": "#/run_allocation" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentRunAllocationPlanModel._validate_condition_assignments" + } + ] + }, + "ExperimentRunPlanModel": { + "additionalProperties": false, + "description": "Pre-run replication, stochastic, episode, and red-variant plan.\n\nReuses the archival-family value models for stochastic controls, run\nallocation, and clock intent, and adds the authoring-only episode and\nred-variant selections. Exactly one of ``allocation`` (condition-based)\nor ``target_run_count`` (simple, no-condition) declares the run count.", + "oneOf": [ + { + "properties": { + "allocation": { + "not": { + "type": "null" + } + }, + "target_run_count": { + "type": "null" + } + }, + "required": [ + "allocation" + ] + }, + { + "properties": { + "allocation": { + "type": "null" + }, + "target_run_count": { + "not": { + "type": "null" + } + } + }, + "required": [ + "target_run_count" + ] + } + ], + "properties": { + "allocation": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentRunAllocationPlanModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "clock_intent": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentClockContextModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "episode_control": { + "$ref": "#/$defs/ExperimentEpisodeControlModel" + }, + "red_variant_selections": { + "additionalProperties": { + "$ref": "#/$defs/ExperimentRedVariantSelectionModel" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Red Variant Selections", + "type": "object" + }, + "stochastic_controls": { + "items": { + "$ref": "#/$defs/ExperimentStochasticControlModel" + }, + "minItems": 1, + "title": "Stochastic Controls", + "type": "array" + }, + "target_run_count": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Target Run Count" + } + }, + "required": [ + "stochastic_controls", + "episode_control" + ], + "title": "ExperimentRunPlanModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "experiment-authoring-input-v1", + "instance_path": "#/run_plan" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentRunPlanModel._validate_run_plan" + } + ] + }, + "ExperimentScenarioReferenceModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "ref_kind": { + "const": "scenario" + } + }, + "required": [ + "ref_kind" + ] + }, + "then": { + "properties": { + "ref_digest": { + "type": "null" + }, + "ref_path": { + "type": "null" + }, + "ref_version": { + "type": "null" + } + } + } + } + ], + "description": "Reference constrained to authored scenario material.", + "properties": { + "ref_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": "Ref Digest" + }, + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "enum": [ + "scenario", + "scenario-snapshot" + ], + "title": "Ref Kind", + "type": "string" + }, + "ref_path": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Path" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentScenarioReferenceModel", + "type": "object" + }, + "ExperimentStochasticControlModel": { + "additionalProperties": false, + "description": "Seed, randomization, sampling, or scheduler control for reproducibility.", + "properties": { + "control_id": { + "minLength": 1, + "title": "Control Id", + "type": "string" + }, + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "role": { + "enum": [ + "seed", + "randomization", + "sampling", + "scheduler", + "agent-policy", + "other" + ], + "title": "Role", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Value" + } + }, + "required": [ + "control_id", + "role" + ], + "title": "ExperimentStochasticControlModel", + "type": "object" + }, + "ExperimentStudyFactorModel": { + "additionalProperties": false, + "description": "Treatment, control, blocking, or apparatus factor for study analysis.", + "properties": { + "factor_kind": { + "enum": [ + "treatment", + "control", + "blocking", + "stratification", + "apparatus", + "other" + ], + "title": "Factor Kind", + "type": "string" + }, + "levels": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Levels", + "type": "array" + }, + "name": { + "minLength": 1, + "title": "Name", + "type": "string" + } + }, + "required": [ + "name", + "factor_kind" + ], + "title": "ExperimentStudyFactorModel", + "type": "object" + }, + "ExperimentTaskReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to an experiment task.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "task", + "title": "Ref Kind", + "type": "string" + }, + "ref_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref Version" + } + }, + "required": [ + "ref_kind", + "ref_id" + ], + "title": "ExperimentTaskReferenceModel", + "type": "object" + }, + "ExperimentValidityNoteModel": { + "additionalProperties": false, + "description": "Validity threat, limitation, or mitigation note for experiment interpretation.", + "properties": { + "category": { + "enum": [ + "construct", + "internal", + "external", + "conclusion", + "statistical", + "apparatus", + "reproducibility", + "security", + "other" + ], + "title": "Category", + "type": "string" + }, + "mitigation": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mitigation" + }, + "note": { + "minLength": 1, + "title": "Note", + "type": "string" + } + }, + "required": [ + "category", + "note" + ], + "title": "ExperimentValidityNoteModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/experiment-authoring-input-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "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": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentApparatusConstraintModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "artifact_refs": { + "items": { + "$ref": "#/$defs/ExperimentArtifactRefModel" + }, + "title": "Artifact Refs", + "type": "array" + }, + "capture_spec_refs": { + "items": { + "$ref": "#/$defs/ExperimentCaptureSpecReferenceModel" + }, + "title": "Capture Spec Refs", + "type": "array" + }, + "description": { + "minLength": 1, + "title": "Description", + "type": "string" + }, + "factors": { + "additionalProperties": { + "$ref": "#/$defs/ExperimentStudyFactorModel" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Factors", + "type": "object" + }, + "intended_scenario_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentScenarioReferenceModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "run_plan": { + "$ref": "#/$defs/ExperimentRunPlanModel" + }, + "schema_version": { + "const": "experiment-authoring-input/v1", + "title": "Schema Version", + "type": "string" + }, + "spec_id": { + "minLength": 1, + "title": "Spec Id", + "type": "string" + }, + "spec_version": { + "minLength": 1, + "title": "Spec Version", + "type": "string" + }, + "task_ref": { + "$ref": "#/$defs/ExperimentTaskReferenceModel" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + }, + "validity_notes": { + "items": { + "$ref": "#/$defs/ExperimentValidityNoteModel" + }, + "title": "Validity Notes", + "type": "array" + } + }, + "required": [ + "schema_version", + "spec_id", + "spec_version", + "title", + "description", + "task_ref", + "run_plan" + ], + "title": "ExperimentSpecModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "experiment-authoring-input-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentSpecModel._validate_experiment_spec" + } + ], + "x-aces-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", + "required": true, + "uri": "https://aces.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 75a330107..f0257288f 100644 --- a/contracts/schemas/experiment-core/experiment-capture-spec-v1.json +++ b/contracts/schemas/experiment-core/experiment-capture-spec-v1.json @@ -58,6 +58,11 @@ "scaffold", "baseline", "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", "other" ], "title": "Role", @@ -598,6 +603,7 @@ "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", diff --git a/contracts/schemas/experiment-core/experiment-derived-measure-v1.json b/contracts/schemas/experiment-core/experiment-derived-measure-v1.json index 85711af88..fdf58f0e9 100644 --- a/contracts/schemas/experiment-core/experiment-derived-measure-v1.json +++ b/contracts/schemas/experiment-core/experiment-derived-measure-v1.json @@ -197,6 +197,7 @@ "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", diff --git a/contracts/schemas/experiment-core/experiment-evidence-record-v1.json b/contracts/schemas/experiment-core/experiment-evidence-record-v1.json index c252e3213..189f56ac1 100644 --- a/contracts/schemas/experiment-core/experiment-evidence-record-v1.json +++ b/contracts/schemas/experiment-core/experiment-evidence-record-v1.json @@ -58,6 +58,11 @@ "scaffold", "baseline", "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", "other" ], "title": "Role", @@ -452,6 +457,7 @@ "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", diff --git a/contracts/schemas/experiment-core/experiment-run-v1.json b/contracts/schemas/experiment-core/experiment-run-v1.json index fcdae6567..138dbacc2 100644 --- a/contracts/schemas/experiment-core/experiment-run-v1.json +++ b/contracts/schemas/experiment-core/experiment-run-v1.json @@ -377,6 +377,11 @@ "scaffold", "baseline", "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", "other" ], "title": "Role", @@ -1632,6 +1637,7 @@ "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", diff --git a/contracts/schemas/experiment-core/experiment-study-v1.json b/contracts/schemas/experiment-core/experiment-study-v1.json index 7d9a49e54..e8cbc31bc 100644 --- a/contracts/schemas/experiment-core/experiment-study-v1.json +++ b/contracts/schemas/experiment-core/experiment-study-v1.json @@ -126,6 +126,11 @@ "scaffold", "baseline", "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", "other" ], "title": "Role", @@ -654,6 +659,7 @@ "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", diff --git a/contracts/schemas/experiment-core/experiment-task-v1.json b/contracts/schemas/experiment-core/experiment-task-v1.json index e92cbfac6..cd38853b1 100644 --- a/contracts/schemas/experiment-core/experiment-task-v1.json +++ b/contracts/schemas/experiment-core/experiment-task-v1.json @@ -169,6 +169,11 @@ "scaffold", "baseline", "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", "other" ], "title": "Role", @@ -955,6 +960,7 @@ "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", diff --git a/contracts/schemas/plans/evaluation-plan-v1.json b/contracts/schemas/plans/evaluation-plan-v1.json index 0fb9da547..24fc35040 100644 --- a/contracts/schemas/plans/evaluation-plan-v1.json +++ b/contracts/schemas/plans/evaluation-plan-v1.json @@ -8,11 +8,28 @@ "type": "string" }, "address": { + "allOf": [ + { + "pattern": "^evaluation\\." + } + ], + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "title": "Address", "type": "string" }, "ordering_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Ordering Dependencies", @@ -25,12 +42,24 @@ }, "refresh_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Refresh Dependencies", "type": "array" }, "resource_type": { + "enum": [ + "assertion", + "condition-binding", + "objective", + "proposition" + ], "title": "Resource Type", "type": "string" } @@ -65,6 +94,12 @@ }, "startup_order": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Startup Order", diff --git a/contracts/schemas/plans/orchestration-plan-v1.json b/contracts/schemas/plans/orchestration-plan-v1.json index 1493be5a2..544aad407 100644 --- a/contracts/schemas/plans/orchestration-plan-v1.json +++ b/contracts/schemas/plans/orchestration-plan-v1.json @@ -8,11 +8,28 @@ "type": "string" }, "address": { + "allOf": [ + { + "pattern": "^orchestration\\." + } + ], + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "title": "Address", "type": "string" }, "ordering_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Ordering Dependencies", @@ -25,12 +42,26 @@ }, "refresh_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Refresh Dependencies", "type": "array" }, "resource_type": { + "enum": [ + "event", + "inject", + "inject-binding", + "script", + "story", + "workflow" + ], "title": "Resource Type", "type": "string" } @@ -65,6 +96,12 @@ }, "startup_order": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Startup Order", diff --git a/contracts/schemas/plans/provisioning-plan-v1.json b/contracts/schemas/plans/provisioning-plan-v1.json index 263809b0d..fa826226f 100644 --- a/contracts/schemas/plans/provisioning-plan-v1.json +++ b/contracts/schemas/plans/provisioning-plan-v1.json @@ -8,11 +8,28 @@ "type": "string" }, "address": { + "allOf": [ + { + "pattern": "^provision\\." + } + ], + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "title": "Address", "type": "string" }, "ordering_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Ordering Dependencies", @@ -25,12 +42,25 @@ }, "refresh_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Refresh Dependencies", "type": "array" }, "resource_type": { + "enum": [ + "account-placement", + "content-placement", + "feature-binding", + "network", + "node" + ], "title": "Resource Type", "type": "string" } @@ -42,6 +72,46 @@ ], "title": "PlanOperationModel", "type": "object" + }, + "RealizationEnvelopeIdentityModel": { + "additionalProperties": false, + "description": "Immutable realization-envelope identity carried across runtime contracts.", + "properties": { + "configuration_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Configuration Digest", + "type": "string" + }, + "contract_id": { + "const": "realization-envelope-v1", + "default": "realization-envelope-v1", + "title": "Contract Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "envelope_id": { + "minLength": 1, + "title": "Envelope Id", + "type": "string" + }, + "schema_version": { + "const": "realization-envelope/v1", + "default": "realization-envelope/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "envelope_id", + "digest", + "configuration_digest" + ], + "title": "RealizationEnvelopeIdentityModel", + "type": "object" } }, "$id": "https://aces.dev/schemas/provisioning-plan-v1.json", @@ -62,6 +132,17 @@ }, "title": "Operations", "type": "array" + }, + "realization_envelope": { + "anyOf": [ + { + "$ref": "#/$defs/RealizationEnvelopeIdentityModel" + }, + { + "type": "null" + } + ], + "default": null } }, "title": "ProvisioningPlanModel", diff --git a/contracts/schemas/processor-manifest/processor-manifest-v2.json b/contracts/schemas/processor-manifest/processor-manifest-v2.json index 8b24f2215..29417fb31 100644 --- a/contracts/schemas/processor-manifest/processor-manifest-v2.json +++ b/contracts/schemas/processor-manifest/processor-manifest-v2.json @@ -159,6 +159,7 @@ "workflow-result-envelope-v1", "workflow-history-event-stream-v1", "evaluation-result-envelope-v1", + "proposition-truth-result-v1", "evaluation-history-event-stream-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index 8de99d213..e1e6ee959 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", + "realization-envelope-v1", "provisioning-plan-v1", "orchestration-plan-v1", "evaluation-plan-v1", @@ -22,6 +23,7 @@ "workflow-result-envelope-v1", "workflow-history-event-stream-v1", "evaluation-result-envelope-v1", + "proposition-truth-result-v1", "evaluation-history-event-stream-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", diff --git a/contracts/schemas/profiles/scientific-completeness-assessment-v1.json b/contracts/schemas/profiles/scientific-completeness-assessment-v1.json new file mode 100644 index 000000000..4d43dbb1a --- /dev/null +++ b/contracts/schemas/profiles/scientific-completeness-assessment-v1.json @@ -0,0 +1,238 @@ +{ + "$defs": { + "CompletenessEvidenceModel": { + "additionalProperties": false, + "properties": { + "claim": { + "minLength": 1, + "title": "Claim", + "type": "string" + }, + "contract_id": { + "anyOf": [ + { + "pattern": "^[a-z0-9][a-z0-9-]*-v[1-9][0-9]*$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Contract Id" + }, + "kind": { + "$ref": "#/$defs/EvidenceKind" + }, + "path": { + "pattern": "^[^/\\s][^\\s]*$", + "title": "Path", + "type": "string" + } + }, + "required": [ + "kind", + "path", + "claim" + ], + "title": "CompletenessEvidenceModel", + "type": "object" + }, + "ConcernDeliveryAssessmentModel": { + "additionalProperties": false, + "properties": { + "binding_obligation": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Binding Obligation" + }, + "concern_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Concern Id", + "type": "string" + }, + "evidence": { + "items": { + "$ref": "#/$defs/CompletenessEvidenceModel" + }, + "title": "Evidence", + "type": "array" + }, + "exclusion_rationale": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Exclusion Rationale" + }, + "external_contract_refs": { + "items": { + "pattern": "^[a-z0-9][a-z0-9-]*-v[1-9][0-9]*$", + "type": "string" + }, + "title": "External Contract Refs", + "type": "array" + }, + "issue_refs": { + "items": { + "pattern": "^#[1-9][0-9]*$", + "type": "string" + }, + "title": "Issue Refs", + "type": "array" + }, + "limitation": { + "minLength": 1, + "title": "Limitation", + "type": "string" + }, + "satisfiability_witness_refs": { + "patternProperties": { + "^[a-z0-9][a-z0-9-]*-v[1-9][0-9]*$": { + "pattern": "^[^/\\s][^\\s]*$", + "type": "string" + } + }, + "title": "Satisfiability Witness Refs", + "type": "object" + }, + "status": { + "$ref": "#/$defs/DeliveryStatus" + } + }, + "required": [ + "concern_id", + "status", + "limitation" + ], + "title": "ConcernDeliveryAssessmentModel", + "type": "object" + }, + "DeliveryStatus": { + "enum": [ + "implemented", + "partial", + "external-contract", + "deliberately-excluded", + "missing" + ], + "title": "DeliveryStatus", + "type": "string" + }, + "EvidenceKind": { + "enum": [ + "normative-spec", + "published-schema", + "contract", + "implementation", + "test", + "conformance", + "example", + "documentation" + ], + "title": "EvidenceKind", + "type": "string" + } + }, + "$id": "https://aces.dev/schemas/scientific-completeness-assessment-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "assessed_on": { + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "title": "Assessed On", + "type": "string" + }, + "assessment_revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Assessment Revision", + "type": "string" + }, + "concerns": { + "items": { + "$ref": "#/$defs/ConcernDeliveryAssessmentModel" + }, + "minItems": 1, + "title": "Concerns", + "type": "array" + }, + "profile_family": { + "const": "scientific-scenario-completeness", + "default": "scientific-scenario-completeness", + "title": "Profile Family", + "type": "string" + }, + "schema_version": { + "const": "scientific-completeness-assessment/v1", + "default": "scientific-completeness-assessment/v1", + "title": "Schema Version", + "type": "string" + }, + "taxonomy_revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Taxonomy Revision", + "type": "string" + } + }, + "required": [ + "taxonomy_revision", + "assessment_revision", + "assessed_on", + "concerns" + ], + "title": "ScientificCompletenessAssessmentModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "scientific-completeness-assessment-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.scientific_completeness.ScientificCompletenessAssessmentModel" + }, + { + "description": "Assessment family, taxonomy revision, and concern ids must exactly match the joined taxonomy before completeness is computed.", + "id": "scientific-completeness-taxonomy-assessment-join", + "inputs": [ + { + "contract_id": "scientific-completeness-taxonomy-v1", + "instance_path": "#" + }, + { + "contract_id": "scientific-completeness-assessment-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.scientific_completeness.evaluate_profile_completeness" + } + ], + "x-aces-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", + "required": true, + "uri": "https://aces.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 new file mode 100644 index 000000000..cdb8b28a3 --- /dev/null +++ b/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json @@ -0,0 +1,169 @@ +{ + "$defs": { + "CompletenessConcernModel": { + "additionalProperties": false, + "properties": { + "concern_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Concern Id", + "type": "string" + }, + "definition": { + "minLength": 1, + "title": "Definition", + "type": "string" + }, + "semantic_owner": { + "minLength": 1, + "title": "Semantic Owner", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + } + }, + "required": [ + "concern_id", + "title", + "definition", + "semantic_owner" + ], + "title": "CompletenessConcernModel", + "type": "object" + }, + "CompletenessProfileModel": { + "additionalProperties": false, + "properties": { + "dispositions": { + "patternProperties": { + "^[a-z][a-z0-9-]*$": { + "$ref": "#/$defs/ProfileDisposition" + } + }, + "title": "Dispositions", + "type": "object" + }, + "example_refs": { + "items": { + "pattern": "^[^/\\s][^\\s]*$", + "type": "string" + }, + "title": "Example Refs", + "type": "array" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "intended_claim": { + "minLength": 1, + "title": "Intended Claim", + "type": "string" + }, + "profile_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Profile Id", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + } + }, + "required": [ + "profile_id", + "title", + "intended_claim", + "explicit_non_claims", + "dispositions" + ], + "title": "CompletenessProfileModel", + "type": "object" + }, + "ProfileDisposition": { + "enum": [ + "required", + "allowed-underspecified", + "excluded" + ], + "title": "ProfileDisposition", + "type": "string" + } + }, + "$id": "https://aces.dev/schemas/scientific-completeness-taxonomy-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "concerns": { + "items": { + "$ref": "#/$defs/CompletenessConcernModel" + }, + "minItems": 1, + "title": "Concerns", + "type": "array" + }, + "profile_family": { + "const": "scientific-scenario-completeness", + "default": "scientific-scenario-completeness", + "title": "Profile Family", + "type": "string" + }, + "profiles": { + "items": { + "$ref": "#/$defs/CompletenessProfileModel" + }, + "minItems": 1, + "title": "Profiles", + "type": "array" + }, + "revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Revision", + "type": "string" + }, + "schema_version": { + "const": "scientific-completeness-taxonomy/v1", + "default": "scientific-completeness-taxonomy/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "revision", + "concerns", + "profiles" + ], + "title": "ScientificCompletenessTaxonomyModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "scientific-completeness-taxonomy-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.scientific_completeness.ScientificCompletenessTaxonomyModel" + } + ], + "x-aces-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", + "required": true, + "uri": "https://aces.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 new file mode 100644 index 000000000..3059de23c --- /dev/null +++ b/contracts/schemas/provenance/sdl-lineage-ledger-v1.json @@ -0,0 +1,651 @@ +{ + "$defs": { + "ArtifactBoundaryModel": { + "additionalProperties": false, + "properties": { + "artifact": { + "minLength": 1, + "title": "Artifact", + "type": "string" + }, + "symbol_or_pointer": { + "minLength": 1, + "title": "Symbol Or Pointer", + "type": "string" + } + }, + "required": [ + "artifact", + "symbol_or_pointer" + ], + "title": "ArtifactBoundaryModel", + "type": "object" + }, + "AuthorityCoordinateModel": { + "additionalProperties": false, + "properties": { + "artifact": { + "minLength": 1, + "title": "Artifact", + "type": "string" + }, + "contract_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Contract Id" + }, + "pointer": { + "pattern": "^#[^\\s]*$", + "title": "Pointer", + "type": "string" + } + }, + "required": [ + "artifact", + "pointer" + ], + "title": "AuthorityCoordinateModel", + "type": "object" + }, + "BibliographicIdentityModel": { + "additionalProperties": false, + "properties": { + "authors_or_maintainer": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authors Or Maintainer", + "type": "array" + }, + "canonical_url": { + "pattern": "^https://[^\\s]+$", + "title": "Canonical Url", + "type": "string" + }, + "citation_id": { + "minLength": 1, + "title": "Citation Id", + "type": "string" + }, + "container_title": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Container Title" + }, + "doi": { + "anyOf": [ + { + "pattern": "^10\\.\\d{4,9}/\\S+$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + }, + "verification_evidence": { + "minLength": 1, + "title": "Verification Evidence", + "type": "string" + }, + "verified_on": { + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "title": "Verified On", + "type": "string" + }, + "year": { + "maximum": 2200, + "minimum": 1800, + "title": "Year", + "type": "integer" + } + }, + "required": [ + "citation_id", + "title", + "authors_or_maintainer", + "year", + "canonical_url", + "verified_on", + "verification_evidence" + ], + "title": "BibliographicIdentityModel", + "type": "object" + }, + "CompatibilityDirection": { + "enum": [ + "aces_relative_to_source", + "not_applicable" + ], + "title": "CompatibilityDirection", + "type": "string" + }, + "CompatibilityStatus": { + "enum": [ + "not_applicable", + "none", + "partial", + "compatible" + ], + "title": "CompatibilityStatus", + "type": "string" + }, + "GitLineageSourceModel": { + "additionalProperties": false, + "properties": { + "canonical_url": { + "pattern": "^https://[^\\s]+$", + "title": "Canonical Url", + "type": "string" + }, + "citation_ref": { + "minLength": 1, + "title": "Citation Ref", + "type": "string" + }, + "commit": { + "pattern": "^[0-9a-f]{40}$", + "title": "Commit", + "type": "string" + }, + "kind": { + "const": "git", + "title": "Kind", + "type": "string" + }, + "license_evidence_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "License Evidence Ref" + }, + "license_expression": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "License Expression" + }, + "license_url": { + "anyOf": [ + { + "pattern": "^https://[^\\s]+$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "License Url" + }, + "repository_url": { + "pattern": "^https://[^\\s]+$", + "title": "Repository Url", + "type": "string" + }, + "source_id": { + "minLength": 1, + "title": "Source Id", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + }, + "version_or_edition": { + "minLength": 1, + "title": "Version Or Edition", + "type": "string" + } + }, + "required": [ + "source_id", + "title", + "version_or_edition", + "canonical_url", + "citation_ref", + "kind", + "repository_url", + "commit" + ], + "title": "GitLineageSourceModel", + "type": "object" + }, + "LineageClaimModel": { + "additionalProperties": false, + "properties": { + "aces_boundaries": { + "items": { + "$ref": "#/$defs/ArtifactBoundaryModel" + }, + "minItems": 1, + "title": "Aces Boundaries", + "type": "array" + }, + "citation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Citation Refs", + "type": "array" + }, + "classification": { + "$ref": "#/$defs/LineageClassification" + }, + "compatibility": { + "$ref": "#/$defs/CompatibilityStatus" + }, + "compatibility_direction": { + "$ref": "#/$defs/CompatibilityDirection" + }, + "divergence": { + "minLength": 1, + "title": "Divergence", + "type": "string" + }, + "internal_authority_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Internal Authority Refs", + "type": "array" + }, + "plane": { + "$ref": "#/$defs/LineagePlane" + }, + "source_boundaries": { + "items": { + "$ref": "#/$defs/ArtifactBoundaryModel" + }, + "title": "Source Boundaries", + "type": "array" + }, + "source_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Source Refs", + "type": "array" + } + }, + "required": [ + "plane", + "classification", + "aces_boundaries", + "divergence", + "compatibility", + "compatibility_direction" + ], + "title": "LineageClaimModel", + "type": "object" + }, + "LineageClassification": { + "enum": [ + "adopted_syntax", + "adopted_semantics", + "adapted", + "aces_native" + ], + "title": "LineageClassification", + "type": "string" + }, + "LineageDisposition": { + "enum": [ + "current", + "removed", + "planned" + ], + "title": "LineageDisposition", + "type": "string" + }, + "LineagePlane": { + "enum": [ + "syntax", + "semantics", + "artifact_code", + "example" + ], + "title": "LineagePlane", + "type": "string" + }, + "LineageSubjectModel": { + "additionalProperties": false, + "properties": { + "authority": { + "$ref": "#/$defs/AuthorityCoordinateModel" + }, + "claims": { + "items": { + "$ref": "#/$defs/LineageClaimModel" + }, + "minItems": 1, + "title": "Claims", + "type": "array" + }, + "disposition": { + "$ref": "#/$defs/LineageDisposition" + }, + "subject_id": { + "pattern": "^[a-z][a-z0-9-]*:[a-z][a-z0-9_-]*$", + "title": "Subject Id", + "type": "string" + }, + "subject_kind": { + "enum": [ + "top_level_field", + "runtime_family", + "concept_family", + "reference_model" + ], + "title": "Subject Kind", + "type": "string" + } + }, + "required": [ + "subject_id", + "subject_kind", + "disposition", + "authority", + "claims" + ], + "title": "LineageSubjectModel", + "type": "object" + }, + "NoticeDecision": { + "enum": [ + "required_included", + "not_required", + "unknown" + ], + "title": "NoticeDecision", + "type": "string" + }, + "PublicationLineageSourceModel": { + "additionalProperties": false, + "properties": { + "canonical_url": { + "pattern": "^https://[^\\s]+$", + "title": "Canonical Url", + "type": "string" + }, + "citation_ref": { + "minLength": 1, + "title": "Citation Ref", + "type": "string" + }, + "kind": { + "const": "publication", + "title": "Kind", + "type": "string" + }, + "source_id": { + "minLength": 1, + "title": "Source Id", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + }, + "version_or_edition": { + "minLength": 1, + "title": "Version Or Edition", + "type": "string" + } + }, + "required": [ + "source_id", + "title", + "version_or_edition", + "canonical_url", + "citation_ref", + "kind" + ], + "title": "PublicationLineageSourceModel", + "type": "object" + }, + "StandardLineageSourceModel": { + "additionalProperties": false, + "properties": { + "canonical_url": { + "pattern": "^https://[^\\s]+$", + "title": "Canonical Url", + "type": "string" + }, + "citation_ref": { + "minLength": 1, + "title": "Citation Ref", + "type": "string" + }, + "kind": { + "const": "standard", + "title": "Kind", + "type": "string" + }, + "maintaining_body": { + "minLength": 1, + "title": "Maintaining Body", + "type": "string" + }, + "source_id": { + "minLength": 1, + "title": "Source Id", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + }, + "version_or_edition": { + "minLength": 1, + "title": "Version Or Edition", + "type": "string" + } + }, + "required": [ + "source_id", + "title", + "version_or_edition", + "canonical_url", + "citation_ref", + "kind", + "maintaining_body" + ], + "title": "StandardLineageSourceModel", + "type": "object" + }, + "ThirdPartyDispositionModel": { + "additionalProperties": false, + "properties": { + "derivation_extent": { + "minLength": 1, + "title": "Derivation Extent", + "type": "string" + }, + "derivation_scope": { + "items": { + "$ref": "#/$defs/ArtifactBoundaryModel" + }, + "minItems": 1, + "title": "Derivation Scope", + "type": "array" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "notice_artifact": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Notice Artifact" + }, + "notice_decision": { + "$ref": "#/$defs/NoticeDecision" + }, + "rationale": { + "minLength": 1, + "title": "Rationale", + "type": "string" + }, + "reviewed_license_ref": { + "minLength": 1, + "title": "Reviewed License Ref", + "type": "string" + }, + "reviewed_on": { + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "title": "Reviewed On", + "type": "string" + }, + "source_ref": { + "minLength": 1, + "title": "Source Ref", + "type": "string" + } + }, + "required": [ + "source_ref", + "derivation_scope", + "derivation_extent", + "reviewed_license_ref", + "notice_decision", + "rationale", + "reviewed_on", + "evidence_refs" + ], + "title": "ThirdPartyDispositionModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/sdl-lineage-ledger-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "citations": { + "items": { + "$ref": "#/$defs/BibliographicIdentityModel" + }, + "minItems": 1, + "title": "Citations", + "type": "array" + }, + "reviewed_on": { + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "title": "Reviewed On", + "type": "string" + }, + "schema_version": { + "const": "sdl-lineage-ledger/v1", + "default": "sdl-lineage-ledger/v1", + "title": "Schema Version", + "type": "string" + }, + "sources": { + "items": { + "discriminator": { + "mapping": { + "git": "#/$defs/GitLineageSourceModel", + "publication": "#/$defs/PublicationLineageSourceModel", + "standard": "#/$defs/StandardLineageSourceModel" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/GitLineageSourceModel" + }, + { + "$ref": "#/$defs/StandardLineageSourceModel" + }, + { + "$ref": "#/$defs/PublicationLineageSourceModel" + } + ] + }, + "minItems": 1, + "title": "Sources", + "type": "array" + }, + "subjects": { + "items": { + "$ref": "#/$defs/LineageSubjectModel" + }, + "minItems": 1, + "title": "Subjects", + "type": "array" + }, + "third_party_dispositions": { + "items": { + "$ref": "#/$defs/ThirdPartyDispositionModel" + }, + "title": "Third Party Dispositions", + "type": "array" + } + }, + "required": [ + "reviewed_on", + "citations", + "sources", + "subjects" + ], + "title": "SDLLineageLedgerModel", + "type": "object" +} diff --git a/contracts/schemas/realization-envelope/realization-envelope-v1.json b/contracts/schemas/realization-envelope/realization-envelope-v1.json new file mode 100644 index 000000000..85db5f1c6 --- /dev/null +++ b/contracts/schemas/realization-envelope/realization-envelope-v1.json @@ -0,0 +1,982 @@ +{ + "$defs": { + "BooleanDomain": { + "additionalProperties": false, + "description": "Booleans: both ``true``/``false``, or an exact boolean when ``value`` set.", + "properties": { + "kind": { + "const": "boolean", + "default": "boolean", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Value" + } + }, + "title": "BooleanDomain", + "type": "object" + }, + "Closure": { + "description": "Whether unspecified realizable dimensions under a scope are admitted.", + "enum": [ + "open-world", + "closed-world" + ], + "title": "Closure", + "type": "string" + }, + "ClosureOverlay": { + "additionalProperties": false, + "description": "Declares open-world or closed-world closure at a scope path.", + "properties": { + "closure": { + "$ref": "#/$defs/Closure" + }, + "path": { + "default": "", + "title": "Path", + "type": "string" + }, + "scope": { + "$ref": "#/$defs/EnvelopeScope" + } + }, + "required": [ + "scope", + "closure" + ], + "title": "ClosureOverlay", + "type": "object" + }, + "ConcernDisposition": { + "description": "How the selected realizer treats a governed concern.", + "enum": [ + "realized", + "transformed", + "descriptor-only", + "unsupported" + ], + "title": "ConcernDisposition", + "type": "string" + }, + "EnumDomain": { + "additionalProperties": false, + "description": "A finite value set: values equal to one listed member.", + "properties": { + "kind": { + "const": "enum", + "default": "enum", + "title": "Kind", + "type": "string" + }, + "values": { + "items": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "minItems": 1, + "title": "Values", + "type": "array" + } + }, + "required": [ + "values" + ], + "title": "EnumDomain", + "type": "object" + }, + "EnvelopeBinding": { + "additionalProperties": false, + "description": "Binds an SDL path (or governed scope ref) to a domain at a scope.\n\n``domain`` names a descriptor in the envelope's ``domains`` map and is required\nfor ``constrained`` / ``exact`` posture and forbidden for ``open`` posture (an\nopen value is left to a downstream realizer). ``overrideable`` allows a\nmore-specific binding to widen a value an enclosing closed scope fixed\n(``envelope-semantics.md`` R2).", + "properties": { + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Domain" + }, + "overrideable": { + "default": false, + "title": "Overrideable", + "type": "boolean" + }, + "path": { + "minLength": 1, + "title": "Path", + "type": "string" + }, + "posture": { + "$ref": "#/$defs/Posture" + }, + "scope": { + "$ref": "#/$defs/EnvelopeScope" + } + }, + "required": [ + "path", + "scope", + "posture" + ], + "title": "EnvelopeBinding", + "type": "object" + }, + "EnvelopeScope": { + "description": "Semantic extent where a posture or closure applies (most local first).", + "enum": [ + "field", + "node", + "topology", + "app", + "scenario" + ], + "title": "EnvelopeScope", + "type": "string" + }, + "ExactDomain": { + "additionalProperties": false, + "description": "A singleton value set: values equal to ``value``.", + "properties": { + "kind": { + "const": "exact", + "default": "exact", + "title": "Kind", + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "title": "Value" + } + }, + "required": [ + "value" + ], + "title": "ExactDomain", + "type": "object" + }, + "GovernedReferenceDomain": { + "additionalProperties": false, + "description": "References in a finite governed set under a named authority.", + "properties": { + "allowed_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Allowed Refs", + "type": "array" + }, + "authority": { + "minLength": 1, + "title": "Authority", + "type": "string" + }, + "kind": { + "const": "governed-reference", + "default": "governed-reference", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "authority", + "allowed_refs" + ], + "title": "GovernedReferenceDomain", + "type": "object" + }, + "IntegerBoundsModel": { + "additionalProperties": false, + "description": "Closed positive integer interval used by realizer resource claims.", + "properties": { + "maximum": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Maximum" + }, + "minimum": { + "minimum": 1, + "title": "Minimum", + "type": "integer" + } + }, + "required": [ + "minimum" + ], + "title": "IntegerBoundsModel", + "type": "object" + }, + "NumericIntervalDomain": { + "additionalProperties": false, + "description": "Numbers of ``numeric_type`` inside a bounded interval.\n\nBoth endpoints are required (the fragment admits only *bounded* intervals,\n``envelope-semantics.md`` R3). An integer interval requires integral\nendpoints. Empty intervals are rejected at construction.", + "properties": { + "kind": { + "const": "numeric-interval", + "default": "numeric-interval", + "title": "Kind", + "type": "string" + }, + "lower": { + "title": "Lower", + "type": "number" + }, + "lower_closed": { + "default": true, + "title": "Lower Closed", + "type": "boolean" + }, + "numeric_type": { + "$ref": "#/$defs/NumericType" + }, + "upper": { + "title": "Upper", + "type": "number" + }, + "upper_closed": { + "default": true, + "title": "Upper Closed", + "type": "boolean" + } + }, + "required": [ + "numeric_type", + "lower", + "upper" + ], + "title": "NumericIntervalDomain", + "type": "object" + }, + "NumericType": { + "description": "Declared numeric type for a numeric-interval domain.", + "enum": [ + "integer", + "number" + ], + "title": "NumericType", + "type": "string" + }, + "ObservationStrength": { + "description": "Strongest evidence a backend configuration emits for one concern.", + "enum": [ + "none", + "driver-reported", + "daemon-observed", + "guest-observed" + ], + "title": "ObservationStrength", + "type": "string" + }, + "Posture": { + "description": "Author/backend intent for a bound value or child scope.", + "enum": [ + "open", + "constrained", + "exact" + ], + "title": "Posture", + "type": "string" + }, + "RealizationConcern": { + "description": "Closed concern taxonomy shared by backend envelope artifacts.", + "enum": [ + "topology", + "architecture", + "image", + "resource-allocation", + "network", + "content-placement", + "account-placement", + "feature-binding", + "service", + "acl" + ], + "title": "RealizationConcern", + "type": "string" + }, + "RealizationConcernDisclosureModel": { + "additionalProperties": false, + "allOf": [ + { + "else": { + "properties": { + "transformations": { + "maxItems": 0 + } + } + }, + "if": { + "properties": { + "disposition": { + "const": "transformed" + } + } + }, + "then": { + "properties": { + "transformations": { + "minItems": 1 + } + } + } + }, + { + "else": { + "properties": { + "mechanism": { + "minLength": 1, + "type": "string" + }, + "observation_strength": { + "not": { + "const": "none" + } + } + } + }, + "if": { + "properties": { + "disposition": { + "const": "unsupported" + } + } + }, + "then": { + "properties": { + "mechanism": { + "type": "null" + }, + "observation_strength": { + "const": "none" + } + } + } + } + ], + "description": "Typed support, transformation, and observation claim for one concern.", + "properties": { + "concern": { + "$ref": "#/$defs/RealizationConcern" + }, + "disposition": { + "$ref": "#/$defs/ConcernDisposition" + }, + "mechanism": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mechanism" + }, + "observation_strength": { + "$ref": "#/$defs/ObservationStrength" + }, + "transformations": { + "items": { + "$ref": "#/$defs/TransformationKind" + }, + "title": "Transformations", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "concern", + "disposition", + "observation_strength" + ], + "title": "RealizationConcernDisclosureModel", + "type": "object" + }, + "RealizationEnvelopeModel": { + "additionalProperties": false, + "description": "A versioned expression denoting a set of SDL scenario instances.", + "properties": { + "bindings": { + "items": { + "$ref": "#/$defs/EnvelopeBinding" + }, + "title": "Bindings", + "type": "array" + }, + "closure": { + "items": { + "$ref": "#/$defs/ClosureOverlay" + }, + "title": "Closure", + "type": "array" + }, + "contract_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Contract Id" + }, + "digest": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Digest" + }, + "domains": { + "additionalProperties": { + "discriminator": { + "mapping": { + "boolean": "#/$defs/BooleanDomain", + "enum": "#/$defs/EnumDomain", + "exact": "#/$defs/ExactDomain", + "governed-reference": "#/$defs/GovernedReferenceDomain", + "numeric-interval": "#/$defs/NumericIntervalDomain", + "record": "#/$defs/RecordDomain" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/ExactDomain" + }, + { + "$ref": "#/$defs/EnumDomain" + }, + { + "$ref": "#/$defs/BooleanDomain" + }, + { + "$ref": "#/$defs/NumericIntervalDomain" + }, + { + "$ref": "#/$defs/GovernedReferenceDomain" + }, + { + "$ref": "#/$defs/RecordDomain" + } + ] + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Domains", + "type": "object" + }, + "id": { + "minLength": 1, + "title": "Id", + "type": "string" + }, + "schema_version": { + "const": "realization-envelope/v1", + "default": "realization-envelope/v1", + "title": "Schema Version", + "type": "string" + }, + "scope": { + "$ref": "#/$defs/EnvelopeScope" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "witness_policy": { + "anyOf": [ + { + "$ref": "#/$defs/WitnessPolicy" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "id", + "scope" + ], + "title": "RealizationEnvelopeModel", + "type": "object" + }, + "RealizerConfigurationModel": { + "additionalProperties": false, + "description": "Secret-free material configuration identity for one realizer mode.", + "properties": { + "architecture": { + "minLength": 1, + "title": "Architecture", + "type": "string" + }, + "configuration_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Configuration Digest", + "type": "string" + }, + "image_policy": { + "minLength": 1, + "title": "Image Policy", + "type": "string" + }, + "memory_mib": { + "$ref": "#/$defs/IntegerBoundsModel" + }, + "mode": { + "minLength": 1, + "title": "Mode", + "type": "string" + }, + "network_policy": { + "minLength": 1, + "title": "Network Policy", + "type": "string" + }, + "supported_account_features": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Supported Account Features", + "type": "array", + "uniqueItems": true + }, + "supported_content_types": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Supported Content Types", + "type": "array", + "uniqueItems": true + }, + "supported_node_types": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Node Types", + "type": "array", + "uniqueItems": true + }, + "supported_os_families": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Os Families", + "type": "array", + "uniqueItems": true + }, + "supports_acls": { + "default": false, + "title": "Supports Acls", + "type": "boolean" + }, + "vcpus": { + "$ref": "#/$defs/IntegerBoundsModel" + } + }, + "required": [ + "mode", + "configuration_digest", + "architecture", + "image_policy", + "network_policy", + "supported_node_types", + "supported_os_families", + "memory_mib", + "vcpus" + ], + "title": "RealizerConfigurationModel", + "type": "object" + }, + "RecordDomain": { + "additionalProperties": false, + "description": "Product structure: each declared field references another named domain.\n\n``extra`` controls undeclared fields: ``False`` (closed) rejects any field not\nnamed in ``fields``; ``True`` (open) admits them. Field values reference domain\nnames resolved against the envelope's ``domains`` map, keeping the structure\nacyclic and free of inline recursion.", + "properties": { + "extra": { + "default": false, + "title": "Extra", + "type": "boolean" + }, + "fields": { + "additionalProperties": { + "minLength": 1, + "type": "string" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Fields", + "type": "object" + }, + "kind": { + "const": "record", + "default": "record", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "fields" + ], + "title": "RecordDomain", + "type": "object" + }, + "TransformationKind": { + "description": "Portable disclosure of a material realization transformation.", + "enum": [ + "bounded-normalization", + "default-substitution", + "descriptor-substitution", + "image-substitution", + "service-synthesis" + ], + "title": "TransformationKind", + "type": "string" + }, + "WitnessPolicy": { + "additionalProperties": false, + "description": "Deterministic default-selection policy for witness generation.\n\n``selections`` overrides the default choice for named domains (each value must\nbe a member of the referenced domain); ``seed`` records the selection basis for\nreproducibility. Neither introduces randomness: witness generation stays a pure\nfunction of ``(envelope, policy)``.", + "properties": { + "seed": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Seed" + }, + "selections": { + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Selections", + "type": "object" + } + }, + "title": "WitnessPolicy", + "type": "object" + } + }, + "$id": "https://aces.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.", + "properties": { + "concerns": { + "allOf": [ + { + "contains": { + "properties": { + "concern": { + "const": "topology" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "architecture" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "image" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "resource-allocation" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "network" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "content-placement" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "account-placement" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "feature-binding" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "service" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "acl" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 + } + ], + "items": { + "$ref": "#/$defs/RealizationConcernDisclosureModel" + }, + "maxItems": 10, + "minItems": 10, + "title": "Concerns", + "type": "array" + }, + "configuration": { + "$ref": "#/$defs/RealizerConfigurationModel" + }, + "contract_id": { + "const": "realization-envelope-v1", + "default": "realization-envelope-v1", + "title": "Contract Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "expression": { + "$ref": "#/$defs/RealizationEnvelopeModel" + }, + "id": { + "minLength": 1, + "title": "Id", + "type": "string" + }, + "schema_version": { + "const": "realization-envelope/v1", + "default": "realization-envelope/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "id", + "expression", + "configuration", + "concerns", + "digest" + ], + "title": "BackendRealizationEnvelopeModel", + "type": "object", + "x-aces-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", + "inputs": [ + { + "contract_id": "realization-envelope-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.realization_envelope.validate_backend_realization_envelope" + } + ], + "x-aces-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", + "required": true, + "uri": "https://aces.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 new file mode 100644 index 000000000..6a6aa962b --- /dev/null +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -0,0 +1,23193 @@ +{ + "$defs": { + "ACLAction": { + "description": "Firewall rule action.", + "enum": [ + "allow", + "deny" + ], + "title": "ACLAction", + "type": "string" + }, + "ACLRule": { + "additionalProperties": false, + "description": "A network access control rule on an infrastructure node.\n\nAdapted from CybORG's subnet NACL model. Specifies directional\ntraffic rules between network segments.", + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/$defs/ACLAction" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "allow", + "title": "Action" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "direction": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Direction", + "type": "string" + }, + "from_net": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "From Net", + "type": "string" + }, + "name": { + "anyOf": [ + { + "const": "" + }, + { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + ], + "default": "", + "title": "Name" + }, + "ports": { + "items": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Ports", + "type": "array" + }, + "protocol": { + "default": "any", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + }, + "to_net": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "To Net", + "type": "string" + } + }, + "title": "ACLRule", + "type": "object" + }, + "Account": { + "additionalProperties": false, + "description": "A user account on a scenario node.\n\nDistinct from OCR's ``Role`` model: roles map exercise participants\nto VM logins for exercise access. Accounts describe the environment\nstate \u2014 what accounts attackers will encounter or exploit.", + "properties": { + "auth_method": { + "default": "password", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Auth Method", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Disabled" + }, + "groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Groups", + "type": "array" + }, + "home": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Home", + "type": "string" + }, + "mail": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mail", + "type": "string" + }, + "node": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Node", + "type": "string" + }, + "password_strength": { + "anyOf": [ + { + "$ref": "#/$defs/PasswordStrength" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "medium", + "title": "Password Strength" + }, + "shell": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Shell", + "type": "string" + }, + "spn": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Spn", + "type": "string" + }, + "username": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Username", + "type": "string" + } + }, + "required": [ + "username" + ], + "title": "Account", + "type": "object" + }, + "Agent": { + "additionalProperties": false, + "description": "An autonomous participant in the scenario.\n\nAgents reference existing scenario elements:\n\n- ``entity`` links to the entities section (team/role) and supplies\n identity and role per ADR-020\n- ``starting_accounts`` links to the accounts section\n- ``allowed_subnets`` links to infrastructure entries\n- ``initial_knowledge`` references nodes and infrastructure\n- ``starting_assertions`` links to precondition assertions, giving the\n authoring surface a declarative hook for participant-relevant starting\n state without equating a probe implementation with truth (ACT-601)\n- ``authority_anchors`` links to declared SDL elements (entities,\n relationships, content, etc.) that anchor what the participant is\n allowed or expected to do in scenario meaning (ACT-601, ADR-020)\n- ``operating_scope`` links to targetable named scenario elements\n (subnets, hosts, services, content) defining where the participant\n may act or observe (ACT-601, ADR-020)\n- ``observation_boundaries`` links to declared participant observation\n boundaries that define participant-specific projections of world and\n evidence state (SEM-208)\n\nPer ADR-073 the CybORG-inherited ``reward_calculator`` label was removed;\nit was an unbound, unvalidated string and graded reward lives in the\nexperiment/evaluator plane (ADR-055/064/069).", + "properties": { + "actions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Actions", + "type": "array" + }, + "allowed_subnets": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allowed Subnets", + "type": "array" + }, + "authority_anchors": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Authority Anchors", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "entity": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Entity", + "type": "string" + }, + "initial_knowledge": { + "anyOf": [ + { + "$ref": "#/$defs/InitialKnowledge" + }, + { + "type": "null" + } + ], + "default": null + }, + "observation_boundaries": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Observation Boundaries", + "type": "array" + }, + "operating_scope": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Operating Scope", + "type": "array" + }, + "starting_accounts": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Starting Accounts", + "type": "array" + }, + "starting_assertions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Starting Assertions", + "type": "array" + } + }, + "title": "Agent", + "type": "object" + }, + "Assertion": { + "additionalProperties": false, + "description": "A typed use of one proposition at a governed semantic boundary.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "polarity": { + "$ref": "#/$defs/AssertionPolarity", + "default": "positive" + }, + "proposition": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Proposition", + "type": "string" + }, + "role": { + "$ref": "#/$defs/AssertionRole" + } + }, + "required": [ + "proposition", + "role" + ], + "title": "Assertion", + "type": "object" + }, + "AssertionPolarity": { + "description": "Expected polarity of the referenced proposition.", + "enum": [ + "positive", + "negative" + ], + "title": "AssertionPolarity", + "type": "string" + }, + "AssertionRole": { + "description": "Boundary at which an assertion constrains its owning construct.", + "enum": [ + "precondition", + "invariant", + "postcondition" + ], + "title": "AssertionRole", + "type": "string" + }, + "AssetValue": { + "additionalProperties": false, + "description": "CIA triad asset valuation for scoring and risk assessment.", + "properties": { + "availability": { + "anyOf": [ + { + "$ref": "#/$defs/AssetValueLevel" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "medium", + "title": "Availability" + }, + "confidentiality": { + "anyOf": [ + { + "$ref": "#/$defs/AssetValueLevel" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "medium", + "title": "Confidentiality" + }, + "integrity": { + "anyOf": [ + { + "$ref": "#/$defs/AssetValueLevel" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "medium", + "title": "Integrity" + } + }, + "title": "AssetValue", + "type": "object" + }, + "AssetValueLevel": { + "description": "CIA triad value level. Adapted from CybORG ConfidentialityValue.", + "enum": [ + "none", + "low", + "medium", + "high", + "critical" + ], + "title": "AssetValueLevel", + "type": "string" + }, + "BehaviorSpecificationExtensionValue": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "items": { + "$ref": "#/$defs/BehaviorSpecificationExtensionValue" + }, + "type": "array" + }, + { + "additionalProperties": { + "$ref": "#/$defs/BehaviorSpecificationExtensionValue" + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "BindingOrigin": { + "description": "Why a concrete parameter value was selected.", + "enum": [ + "provided", + "default" + ], + "title": "BindingOrigin", + "type": "string" + }, + "BooleanPredicate": { + "additionalProperties": false, + "description": "Compare a governed Boolean property with an expected Boolean.", + "properties": { + "expected": { + "title": "Expected", + "type": "boolean" + }, + "kind": { + "const": "boolean", + "default": "boolean", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected" + ], + "title": "BooleanPredicate", + "type": "object" + }, + "CapabilityConstraint": { + "additionalProperties": false, + "description": "Pre-instantiation constraint retained for one concrete field.", + "properties": { + "allowed_values": { + "items": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "minItems": 1, + "title": "Allowed Values", + "type": "array", + "uniqueItems": true + }, + "field_pointer": { + "maxLength": 4096, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Field Pointer", + "type": "string" + }, + "kind": { + "const": "allowed-values", + "default": "allowed-values", + "title": "Kind", + "type": "string" + }, + "parameter": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 32, + "minItems": 1, + "title": "Parameter", + "type": "array" + } + }, + "required": [ + "field_pointer", + "parameter", + "allowed_values" + ], + "title": "CapabilityConstraint", + "type": "object" + }, + "Condition": { + "additionalProperties": false, + "description": "A probe implementation deployed to a VM.\n\nEither ``command`` + ``interval`` or ``source`` must be set, not both.", + "properties": { + "command": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "environment": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Environment", + "type": "array" + }, + "interval": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Interval" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "proposition": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Proposition", + "type": "string" + }, + "retries": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retries" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/Source" + }, + { + "type": "null" + } + ], + "default": null + }, + "start_period": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Period" + }, + "timeout": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout" + } + }, + "title": "Condition", + "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": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Base Image", + "type": "string" + }, + "base_image_digest": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "dockerfile_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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" + }, + "Content": { + "additionalProperties": false, + "description": "Data or files placed into a scenario node.\n\nSupports three forms:\n\n- ``file``: A single file at a specific path, optionally with inline text.\n- ``dataset``: A collection of related items (emails, records, pcaps)\n delivered via a source package or listed as items.\n- ``directory``: A directory structure placed at a destination path.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "destination": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Destination", + "type": "string" + }, + "format": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Format", + "type": "string" + }, + "items": { + "items": { + "$ref": "#/$defs/ContentItem" + }, + "title": "Items", + "type": "array" + }, + "path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "sensitive": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Sensitive" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/Source" + }, + { + "type": "null" + } + ], + "default": null + }, + "tags": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Tags", + "type": "array" + }, + "target": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target", + "type": "string" + }, + "text": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + }, + "type": { + "$ref": "#/$defs/ContentType" + } + }, + "required": [ + "type" + ], + "title": "Content", + "type": "object" + }, + "ContentItem": { + "additionalProperties": false, + "description": "A single item within a dataset (e.g., one email, one record).", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "display_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Display Name", + "type": "string" + }, + "name": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Tags", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "ContentItem", + "type": "object" + }, + "ContentType": { + "description": "Kind of content placed into a system.", + "enum": [ + "file", + "dataset", + "directory" + ], + "title": "ContentType", + "type": "string" + }, + "Database": { + "additionalProperties": false, + "description": "An observed logical database within a database service.", + "properties": { + "database_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseObjectOrigin" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Origin" + }, + "schemas": { + "items": { + "$ref": "#/$defs/DatabaseSchema" + }, + "title": "Schemas", + "type": "array" + } + }, + "required": [ + "database_id", + "name" + ], + "title": "Database", + "type": "object" + }, + "DatabaseAuthMethod": { + "description": "How a client authenticates to a database on an access relationship.", + "enum": [ + "password", + "md5", + "scram_sha_256", + "cert", + "trust", + "peer", + "ident", + "gssapi", + "sspi", + "ldap", + "radius", + "other", + "unknown" + ], + "title": "DatabaseAuthMethod", + "type": "string" + }, + "DatabaseEngine": { + "description": "The database engine of an observed database service.", + "enum": [ + "postgresql", + "mysql", + "mariadb", + "sqlite", + "mongodb", + "mssql", + "oracle", + "redis", + "other", + "unknown" + ], + "title": "DatabaseEngine", + "type": "string" + }, + "DatabaseGrant": { + "additionalProperties": false, + "description": "An observed privilege grant to a database role.\n\nThe grant is structured by grantee, target object, and privileges; raw\n``GRANT`` statements are not the portable model (ADR-029 \u00a74). ``object_ref``\nis the stable ``*_id`` of a database/schema/table in the same service.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "grantee_role_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Grantee Role Ref", + "type": "string" + }, + "object_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Object Ref", + "type": "string" + }, + "object_type": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseObjectType" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Object Type" + }, + "privileges": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "with_grant_option": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "With Grant Option" + } + }, + "required": [ + "grantee_role_ref", + "object_type", + "object_ref" + ], + "title": "DatabaseGrant", + "type": "object" + }, + "DatabaseListener": { + "additionalProperties": false, + "description": "An observed listener of a database service.\n\nThis records what the database process itself listens on. It is not host\npublication \u2014 ``runtime.network.published_ports`` remains the host-exposure\nfact (ADR-029 \u00a73).", + "properties": { + "address": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Port" + } + }, + "required": [ + "address" + ], + "title": "DatabaseListener", + "type": "object" + }, + "DatabaseObjectOrigin": { + "description": "Whether a database object was scenario-authored or engine-supplied.\n\nKeeps engine built-ins (``postgres``, ``template0``, ``template1``,\n``information_schema``) and system roles from looking scenario-authored.", + "enum": [ + "scenario", + "built_in", + "system", + "unknown", + "other" + ], + "title": "DatabaseObjectOrigin", + "type": "string" + }, + "DatabaseObjectType": { + "description": "The kind of database object a grant applies to.", + "enum": [ + "database", + "schema", + "table" + ], + "title": "DatabaseObjectType", + "type": "string" + }, + "DatabaseProtocol": { + "description": "The wire protocol an observed database service speaks.\n\nKept distinct from :class:`DatabaseEngine`: a MariaDB engine speaks the\n``mysql`` protocol, and PostgreSQL must never be ``other`` (ADR-029 \u00a73).", + "enum": [ + "postgresql", + "mysql", + "tds", + "mongodb", + "redis", + "other", + "unknown" + ], + "title": "DatabaseProtocol", + "type": "string" + }, + "DatabaseRole": { + "additionalProperties": false, + "description": "An observed database-local role \u2014 an authorization principal.\n\nDatabase roles are not OS accounts, ``runtime.local_identity`` users, or\ntop-level scenario ``accounts`` (ADR-029 \u00a74).", + "properties": { + "can_login": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Can Login" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseObjectOrigin" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Origin" + }, + "role_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Role Id", + "type": "string" + }, + "role_type": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseRoleType" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Role Type" + } + }, + "required": [ + "role_id", + "name" + ], + "title": "DatabaseRole", + "type": "object" + }, + "DatabaseRoleType": { + "description": "Portable classification of a database-local role.", + "enum": [ + "login", + "group", + "application", + "service", + "admin", + "system", + "other", + "unknown" + ], + "title": "DatabaseRoleType", + "type": "string" + }, + "DatabaseSchema": { + "additionalProperties": false, + "description": "An observed schema (namespace) within a database.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseObjectOrigin" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Origin" + }, + "schema_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Schema Id", + "type": "string" + }, + "tables": { + "items": { + "$ref": "#/$defs/DatabaseTable" + }, + "title": "Tables", + "type": "array" + } + }, + "required": [ + "schema_id", + "name" + ], + "title": "DatabaseSchema", + "type": "object" + }, + "DatabaseSetting": { + "additionalProperties": false, + "description": "An observed database runtime setting with provenance and sensitivity.\n\nExplicit ``redacted``/``operator_secret`` classifications omit raw values;\ncredential-shaped names remain scenario content unless the author marks the\nvalue withheld.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseSettingProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + } + }, + "required": [ + "name" + ], + "title": "DatabaseSetting", + "type": "object" + }, + "DatabaseSettingProvenance": { + "description": "Where an observed database runtime setting value came from.", + "enum": [ + "introspection", + "configuration_file", + "image_default", + "operator_override", + "runtime_default", + "unknown", + "other" + ], + "title": "DatabaseSettingProvenance", + "type": "string" + }, + "DatabaseTable": { + "additionalProperties": false, + "description": "An observed table in a database schema.\n\n``table_id`` is the stable identity; ``name`` is the observed table name\nand is data, never a mapping key or reference segment.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "table_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Table Id", + "type": "string" + } + }, + "required": [ + "table_id", + "name" + ], + "title": "DatabaseTable", + "type": "object" + }, + "DnsDynamicUpdatePolicy": { + "additionalProperties": false, + "description": "Observed dynamic-update posture without raw update credentials.", + "properties": { + "allowed_clients": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allowed Clients", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "key_names": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Key Names", + "type": "array" + }, + "policy": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Policy", + "type": "string" + } + }, + "title": "DnsDynamicUpdatePolicy", + "type": "object" + }, + "DnsForwarder": { + "additionalProperties": false, + "description": "Recursive upstream resolver endpoint.", + "properties": { + "address": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": 53, + "title": "Port" + }, + "tls_server_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Tls Server Name", + "type": "string" + }, + "transport": { + "anyOf": [ + { + "$ref": "#/$defs/DnsForwarderTransport" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "udp", + "title": "Transport" + } + }, + "required": [ + "address" + ], + "title": "DnsForwarder", + "type": "object" + }, + "DnsForwarderTransport": { + "description": "Transport used to contact a recursive upstream.", + "enum": [ + "udp", + "tcp", + "tls", + "https", + "other", + "unknown" + ], + "title": "DnsForwarderTransport", + "type": "string" + }, + "DnsForwardingPolicy": { + "description": "How upstream forwarders are used.", + "enum": [ + "first", + "only", + "random", + "round_robin", + "sequential", + "none", + "other", + "unknown" + ], + "title": "DnsForwardingPolicy", + "type": "string" + }, + "DnsMxRdata": { + "additionalProperties": false, + "description": "Typed MX RDATA fields.", + "properties": { + "exchange": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Exchange", + "type": "string" + }, + "preference": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Preference" + } + }, + "required": [ + "preference", + "exchange" + ], + "title": "DnsMxRdata", + "type": "object" + }, + "DnsRecordClass": { + "description": "DNS RR class.", + "enum": [ + "in", + "ch", + "hs", + "none", + "any", + "other", + "unknown" + ], + "title": "DnsRecordClass", + "type": "string" + }, + "DnsRecordProvenance": { + "description": "Where an observed DNS record or zone fact came from.", + "enum": [ + "axfr", + "ixfr", + "query", + "configuration_file", + "zone_file", + "provider_api", + "runtime_default", + "operator_override", + "unknown", + "other" + ], + "title": "DnsRecordProvenance", + "type": "string" + }, + "DnsRecordType": { + "description": "Common DNS RR types plus an escape hatch for IANA extensions.", + "enum": [ + "a", + "aaaa", + "cname", + "ns", + "mx", + "ptr", + "soa", + "srv", + "txt", + "caa", + "ds", + "dnskey", + "rrsig", + "nsec", + "nsec3", + "tlsa", + "svcb", + "https", + "other", + "unknown" + ], + "title": "DnsRecordType", + "type": "string" + }, + "DnsResolverPolicy": { + "additionalProperties": false, + "description": "Recursive/forwarding resolver policy for a DNS service.", + "properties": { + "allow_recursion": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allow Recursion", + "type": "array" + }, + "default_logging": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default Logging" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "dnssec_validation": { + "anyOf": [ + { + "$ref": "#/$defs/DnssecValidationMode" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Dnssec Validation" + }, + "forwarders": { + "items": { + "$ref": "#/$defs/DnsForwarder" + }, + "title": "Forwarders", + "type": "array" + }, + "forwarding_policy": { + "anyOf": [ + { + "$ref": "#/$defs/DnsForwardingPolicy" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Forwarding Policy" + }, + "query_logging": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Query Logging" + }, + "recursion_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Recursion Enabled" + } + }, + "title": "DnsResolverPolicy", + "type": "object" + }, + "DnsResourceRecord": { + "additionalProperties": false, + "description": "One observed record within an RRset.", + "properties": { + "address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "mx": { + "anyOf": [ + { + "$ref": "#/$defs/DnsMxRdata" + }, + { + "type": "null" + } + ], + "default": null + }, + "rdata": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Rdata", + "type": "string" + }, + "soa": { + "anyOf": [ + { + "$ref": "#/$defs/DnsSoaRdata" + }, + { + "type": "null" + } + ], + "default": null + }, + "srv": { + "anyOf": [ + { + "$ref": "#/$defs/DnsSrvRdata" + }, + { + "type": "null" + } + ], + "default": null + }, + "target": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target", + "type": "string" + }, + "text": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Text", + "type": "array" + } + }, + "title": "DnsResourceRecord", + "type": "object" + }, + "DnsResourceRecordSet": { + "additionalProperties": false, + "description": "Records grouped by owner name, class, type, and TTL.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "owner": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Owner", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/DnsRecordProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "record_type": { + "anyOf": [ + { + "$ref": "#/$defs/DnsRecordType" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Record Type" + }, + "records": { + "items": { + "$ref": "#/$defs/DnsResourceRecord" + }, + "title": "Records", + "type": "array" + }, + "rrset_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ttl" + }, + "type_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Type Code" + }, + "zone_class": { + "anyOf": [ + { + "$ref": "#/$defs/DnsRecordClass" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "in", + "title": "Zone Class" + } + }, + "required": [ + "rrset_id", + "owner", + "record_type" + ], + "title": "DnsResourceRecordSet", + "type": "object" + }, + "DnsRuntimeSetting": { + "additionalProperties": false, + "description": "Bounded DNS runtime setting with provenance and redaction.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/DnsSettingProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + } + }, + "required": [ + "name" + ], + "title": "DnsRuntimeSetting", + "type": "object" + }, + "DnsServerImplementation": { + "description": "Portable implementation family for the observed DNS service.", + "enum": [ + "bind", + "coredns", + "powerdns", + "knot_dns", + "nsd", + "windows_dns", + "dnsmasq", + "unbound", + "other", + "unknown" + ], + "title": "DnsServerImplementation", + "type": "string" + }, + "DnsServiceRole": { + "description": "Role the DNS service performs for participants or peers.", + "enum": [ + "authoritative", + "recursive_resolver", + "forwarding_resolver", + "caching_resolver", + "stub_resolver", + "other", + "unknown" + ], + "title": "DnsServiceRole", + "type": "string" + }, + "DnsSettingProvenance": { + "description": "Where an observed DNS setting value came from.", + "enum": [ + "configuration_file", + "introspection", + "image_default", + "operator_override", + "runtime_default", + "unknown", + "other" + ], + "title": "DnsSettingProvenance", + "type": "string" + }, + "DnsSoaRdata": { + "additionalProperties": false, + "description": "Typed SOA RDATA fields.", + "properties": { + "expire": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Expire" + }, + "minimum": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Minimum" + }, + "mname": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mname", + "type": "string" + }, + "refresh": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Refresh" + }, + "retry": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Retry" + }, + "rname": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Rname", + "type": "string" + }, + "serial": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Serial" + } + }, + "required": [ + "mname", + "rname", + "serial", + "refresh", + "retry", + "expire", + "minimum" + ], + "title": "DnsSoaRdata", + "type": "object" + }, + "DnsSrvRdata": { + "additionalProperties": false, + "description": "Typed SRV RDATA fields.", + "properties": { + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Port" + }, + "priority": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Priority" + }, + "target": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target", + "type": "string" + }, + "weight": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Weight" + } + }, + "required": [ + "priority", + "weight", + "port", + "target" + ], + "title": "DnsSrvRdata", + "type": "object" + }, + "DnsZone": { + "additionalProperties": false, + "description": "An observed authoritative or forwarding DNS zone.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/DnsZoneKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/DnsRecordProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "purpose": { + "anyOf": [ + { + "$ref": "#/$defs/DnsZonePurpose" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Purpose" + }, + "rrsets": { + "items": { + "$ref": "#/$defs/DnsResourceRecordSet" + }, + "title": "Rrsets", + "type": "array" + }, + "transfer": { + "anyOf": [ + { + "$ref": "#/$defs/DnsZoneTransferPolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "zone_class": { + "anyOf": [ + { + "$ref": "#/$defs/DnsRecordClass" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "in", + "title": "Zone Class" + }, + "zone_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Zone File Refs", + "type": "array" + }, + "zone_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "zone_id", + "name" + ], + "title": "DnsZone", + "type": "object" + }, + "DnsZoneKind": { + "description": "Portable zone role independent of one server's configuration grammar.", + "enum": [ + "primary", + "secondary", + "stub", + "forward", + "hint", + "catalog", + "other", + "unknown" + ], + "title": "DnsZoneKind", + "type": "string" + }, + "DnsZonePurpose": { + "description": "Semantic purpose of a zone name.", + "enum": [ + "forward", + "reverse", + "service_discovery", + "infrastructure", + "other", + "unknown" + ], + "title": "DnsZonePurpose", + "type": "string" + }, + "DnsZoneTransferPolicy": { + "additionalProperties": false, + "description": "Observed zone-transfer posture for a DNS zone.", + "properties": { + "allowed_clients": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allowed Clients", + "type": "array" + }, + "axfr_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Axfr Enabled" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "ixfr_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ixfr Enabled" + }, + "primary_servers": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Primary Servers", + "type": "array" + }, + "secondary_servers": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Secondary Servers", + "type": "array" + } + }, + "title": "DnsZoneTransferPolicy", + "type": "object" + }, + "DnssecValidationMode": { + "description": "Recursive DNSSEC validation posture.", + "enum": [ + "enabled", + "disabled", + "auto", + "permissive", + "unknown", + "other" + ], + "title": "DnssecValidationMode", + "type": "string" + }, + "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 ACES variable substitution.", + "properties": { + "arguments": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Arguments", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "instruction": { + "anyOf": [ + { + "$ref": "#/$defs/DockerfileInstructionKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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" + }, + "Entity": { + "additionalProperties": false, + "description": "An organizational unit, team, or person in the exercise.\n\nEntities can nest recursively. Flattened names use dot-notation:\n``blue-team.bob`` refers to the ``bob`` entity nested inside\n``blue-team``.", + "properties": { + "categories": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Categories", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "entities": { + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/Entity" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Entities", + "type": "object" + }, + "events": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Events", + "type": "array" + }, + "facts": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Facts", + "type": "object" + }, + "mission": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mission", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/ExerciseRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Role" + }, + "vulnerabilities": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Vulnerabilities", + "type": "array" + } + }, + "title": "Entity", + "type": "object" + }, + "Event": { + "additionalProperties": false, + "description": "A triggered action combining assertion preconditions and injects.", + "properties": { + "assertions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Assertions", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "injects": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Injects", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/Source" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "Event", + "type": "object" + }, + "EvidenceIntegrityExpectation": { + "description": "Expected integrity or chain-of-custody treatment.", + "enum": [ + "none", + "checksum", + "signature", + "chain_of_custody", + "timestamped", + "other" + ], + "title": "EvidenceIntegrityExpectation", + "type": "string" + }, + "EvidenceLossDisclosureExpectation": { + "description": "Expected disclosure when capture is incomplete or lossy.", + "enum": [ + "not_expected", + "best_effort", + "required", + "other" + ], + "title": "EvidenceLossDisclosureExpectation", + "type": "string" + }, + "EvidenceRedactionExpectation": { + "description": "Expected redaction treatment for captured output.", + "enum": [ + "none", + "redact_sensitive", + "redact_secrets", + "aggregate_only", + "derived_only", + "other" + ], + "title": "EvidenceRedactionExpectation", + "type": "string" + }, + "EvidenceRequirement": { + "additionalProperties": false, + "description": "One authored data, evidence, or output capture requirement.\n\nThe model records capture intent only. Executable capture contracts may map\nthis to ``experiment-capture-spec-v1`` later, but captured payloads and\nproof of capture belong to experiment evidence records.", + "properties": { + "artifact_role": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Artifact Role", + "type": "string" + }, + "boundary_kind": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Boundary Kind", + "type": "string" + }, + "boundary_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Boundary Ref", + "type": "string" + }, + "capture_requirement_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Capture Requirement Ref", + "type": "string" + }, + "capture_spec_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Capture Spec Ref", + "type": "string" + }, + "channel": { + "anyOf": [ + { + "$ref": "#/$defs/EvidenceRequirementChannel" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Channel" + }, + "channel_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Channel Refs", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "integrity": { + "anyOf": [ + { + "$ref": "#/$defs/EvidenceIntegrityExpectation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Integrity" + }, + "loss_disclosure": { + "anyOf": [ + { + "$ref": "#/$defs/EvidenceLossDisclosureExpectation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Loss Disclosure" + }, + "media_types": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Media Types", + "type": "array" + }, + "notes": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "redaction": { + "anyOf": [ + { + "$ref": "#/$defs/EvidenceRedactionExpectation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Redaction" + }, + "retention": { + "anyOf": [ + { + "$ref": "#/$defs/EvidenceRetentionExpectation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Retention" + }, + "scope": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Scope", + "type": "string" + }, + "scope_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Scope Refs", + "type": "array" + }, + "sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Sensitivity" + }, + "source_class": { + "anyOf": [ + { + "$ref": "#/$defs/EvidenceRequirementSourceClass" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Class" + }, + "source_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Source Refs", + "type": "array" + }, + "trigger_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Trigger Ref", + "type": "string" + }, + "window": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Window", + "type": "string" + } + }, + "required": [ + "sensitivity", + "redaction", + "integrity", + "retention", + "loss_disclosure" + ], + "title": "EvidenceRequirement", + "type": "object" + }, + "EvidenceRequirementChannel": { + "description": "Capture channel or modality for an authored requirement.", + "enum": [ + "packet_capture", + "log", + "trace", + "metric", + "file_artifact", + "screen_capture", + "api_response", + "database_record", + "participant_output", + "other" + ], + "title": "EvidenceRequirementChannel", + "type": "string" + }, + "EvidenceRequirementSourceClass": { + "description": "Closed source classes for requirements without a concrete source ref.", + "enum": [ + "scenario_native_observability", + "participant_action", + "participant_observation", + "scenario_state", + "processor_backend", + "apparatus", + "external", + "other" + ], + "title": "EvidenceRequirementSourceClass", + "type": "string" + }, + "EvidenceRetentionExpectation": { + "description": "Expected retention boundary for captured output.", + "enum": [ + "not_retained", + "run_lifetime", + "study_lifetime", + "archival", + "policy_defined", + "other" + ], + "title": "EvidenceRetentionExpectation", + "type": "string" + }, + "ExerciseRole": { + "description": "Role in the exercise.", + "enum": [ + "white", + "green", + "red", + "blue" + ], + "title": "ExerciseRole", + "type": "string" + }, + "ExplicitnessClass": { + "description": "SEM-218 author-intent class for a declaration.", + "enum": [ + "exact", + "constrained", + "open" + ], + "title": "ExplicitnessClass", + "type": "string" + }, + "ExplicitnessProvenance": { + "description": "Where the classified value came from in the SDL lifecycle.", + "enum": [ + "author-declared", + "processor-derived", + "backend-realized" + ], + "title": "ExplicitnessProvenance", + "type": "string" + }, + "ExplicitnessProvenanceRecord": { + "additionalProperties": false, + "description": "Serializable SEM-218 classification for one instantiated model path.", + "properties": { + "classification": { + "$ref": "#/$defs/ExplicitnessClass" + }, + "model_path": { + "maxLength": 4096, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Model Path", + "type": "string" + }, + "parameters": { + "default": [], + "items": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "type": "array" + }, + "title": "Parameters", + "type": "array" + }, + "provenance": { + "$ref": "#/$defs/ExplicitnessProvenance", + "default": "author-declared" + }, + "reason": { + "default": "", + "maxLength": 1024, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Reason", + "type": "string" + } + }, + "required": [ + "model_path", + "classification" + ], + "title": "ExplicitnessProvenanceRecord", + "type": "object" + }, + "ExternalMappingLoss": { + "additionalProperties": false, + "description": "Loss-labeled mapping from an external vocabulary to ACES semantics.", + "properties": { + "identifier": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Identifier", + "type": "string" + }, + "loss_label": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Loss Label", + "type": "string" + }, + "rationale": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Rationale", + "type": "string" + }, + "system": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "System", + "type": "string" + } + }, + "required": [ + "system", + "identifier", + "loss_label", + "rationale" + ], + "title": "ExternalMappingLoss", + "type": "object" + }, + "Feature": { + "additionalProperties": false, + "description": "A software artifact, service, or configuration deployed to a VM.", + "properties": { + "dependencies": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Dependencies", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "destination": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Destination", + "type": "string" + }, + "environment": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Environment", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/Source" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "$ref": "#/$defs/FeatureType" + }, + "vulnerabilities": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type" + ], + "title": "Feature", + "type": "object" + }, + "FeatureType": { + "description": "Kind of feature deployed to a VM.", + "enum": [ + "service", + "configuration", + "artifact" + ], + "title": "FeatureType", + "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" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Attestation Type" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_reference": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Evidence Reference", + "type": "string" + }, + "predicate_type": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Predicate Type", + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/ImageAttestationStatus" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Status" + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/ImageVerificationStatus" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeEnvironmentValueClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Command", + "type": "array" + }, + "default_environment": { + "items": { + "$ref": "#/$defs/ImageEnvironmentDefault" + }, + "title": "Default Environment", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "entrypoint": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Entrypoint", + "type": "array" + }, + "exposed_ports": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Exposed Ports", + "type": "array" + }, + "labels": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Labels", + "type": "object" + }, + "working_directory": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "destination_path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Destination Path", + "type": "string" + }, + "from_stage": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "From Stage", + "type": "string" + }, + "source_path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeEnvironmentValueClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Created By", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "digest": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Digest", + "type": "string" + }, + "empty": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Empty" + }, + "size": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Checksum", + "type": "string" + }, + "checksum_algorithm": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Checksum Algorithm", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "destination_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Destination Path", + "type": "string" + }, + "identifier": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Identifier", + "type": "string" + }, + "source_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "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" + }, + "InfraNode": { + "additionalProperties": false, + "description": "Deployment parameters for a node.\n\nShorthand: ``node-name: 3`` (just the count).\nLonghand: full dict with count, links, dependencies, properties, acls.", + "properties": { + "acls": { + "items": { + "$ref": "#/$defs/ACLRule" + }, + "title": "Acls", + "type": "array" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": 1, + "title": "Count" + }, + "dependencies": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Dependencies", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "links": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Links", + "type": "array" + }, + "properties": { + "anyOf": [ + { + "$ref": "#/$defs/SimpleProperties" + }, + { + "items": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Properties" + } + }, + "title": "InfraNode", + "type": "object" + }, + "InitialKnowledge": { + "additionalProperties": false, + "description": "What an agent knows about the scenario at start time.\n\nAdapted from CybORG's INT (Initial Network Topology).\nSpecifies which hosts, subnets, services, and accounts\nthe agent has knowledge of before the scenario begins.", + "properties": { + "accounts": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Accounts", + "type": "array" + }, + "hosts": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Hosts", + "type": "array" + }, + "services": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Services", + "type": "array" + }, + "subnets": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Subnets", + "type": "array" + } + }, + "title": "InitialKnowledge", + "type": "object" + }, + "Inject": { + "additionalProperties": false, + "description": "An action injected between entities during an exercise.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "environment": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Environment", + "type": "array" + }, + "from_entity": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "From Entity", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/Source" + }, + { + "type": "null" + } + ], + "default": null + }, + "to_entities": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "To Entities", + "type": "array" + } + }, + "title": "Inject", + "type": "object" + }, + "InstantiatedScenario": { + "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.", + "properties": { + "accounts": { + "additionalProperties": { + "$ref": "#/$defs/Account" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Accounts", + "type": "object" + }, + "action_contracts": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantActionContract" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Action Contracts", + "type": "object" + }, + "agents": { + "additionalProperties": { + "$ref": "#/$defs/Agent" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Agents", + "type": "object" + }, + "assertions": { + "additionalProperties": { + "$ref": "#/$defs/Assertion" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Assertions", + "type": "object" + }, + "behavior_specifications": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantBehaviorSpecification" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Behavior Specifications", + "type": "object" + }, + "conditions": { + "additionalProperties": { + "$ref": "#/$defs/Condition" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Conditions", + "type": "object" + }, + "content": { + "additionalProperties": { + "$ref": "#/$defs/Content" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Content", + "type": "object" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "entities": { + "additionalProperties": { + "$ref": "#/$defs/Entity" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Entities", + "type": "object" + }, + "events": { + "additionalProperties": { + "$ref": "#/$defs/Event" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Events", + "type": "object" + }, + "evidence_requirements": { + "additionalProperties": { + "$ref": "#/$defs/EvidenceRequirement" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Evidence Requirements", + "type": "object" + }, + "features": { + "additionalProperties": { + "$ref": "#/$defs/Feature" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Features", + "type": "object" + }, + "forwarding_agents": { + "items": { + "allOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgent" + }, + { + "properties": { + "forwarding_agent_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + } + }, + "type": "object" + } + ] + }, + "title": "Forwarding Agents", + "type": "array" + }, + "infrastructure": { + "additionalProperties": { + "$ref": "#/$defs/InfraNode" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Infrastructure", + "type": "object" + }, + "injects": { + "additionalProperties": { + "$ref": "#/$defs/Inject" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Injects", + "type": "object" + }, + "instantiation_provenance": { + "$ref": "#/$defs/InstantiationProvenance", + "x-aces-realization-dimension": false + }, + "name": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "title": "Name", + "type": "string" + }, + "nodes": { + "additionalProperties": { + "$ref": "#/$defs/Node" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,34}$", + "type": "string" + }, + "title": "Nodes", + "type": "object" + }, + "objectives": { + "additionalProperties": { + "$ref": "#/$defs/Objective" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Objectives", + "type": "object" + }, + "observation_boundaries": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantObservationBoundary" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Observation Boundaries", + "type": "object" + }, + "outcome_interpretation_rules": { + "additionalProperties": { + "$ref": "#/$defs/OutcomeInterpretationRule" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Outcome Interpretation Rules", + "type": "object" + }, + "propositions": { + "additionalProperties": { + "$ref": "#/$defs/Proposition" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Propositions", + "type": "object" + }, + "relationships": { + "additionalProperties": { + "$ref": "#/$defs/Relationship" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Relationships", + "type": "object" + }, + "scripts": { + "additionalProperties": { + "$ref": "#/$defs/Script" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Scripts", + "type": "object" + }, + "stories": { + "additionalProperties": { + "$ref": "#/$defs/Story" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Stories", + "type": "object" + }, + "version": { + "default": "*", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + }, + "vulnerabilities": { + "additionalProperties": { + "$ref": "#/$defs/Vulnerability" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Vulnerabilities", + "type": "object" + }, + "workflows": { + "additionalProperties": { + "$ref": "#/$defs/Workflow" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Workflows", + "type": "object" + } + }, + "required": [ + "name", + "instantiation_provenance" + ], + "title": "SDL Instantiated Scenario v1", + "type": "object", + "x-aces-authored-identity-profile": "aces-sdl-semantic/v1", + "x-aces-document-phase": "instantiated-scenario" + }, + "InstantiationProvenance": { + "additionalProperties": false, + "description": "Complete portable derivation context for an instantiated scenario.", + "properties": { + "authored_digest": { + "$ref": "#/$defs/SemanticDigest" + }, + "bindings": { + "default": [], + "items": { + "$ref": "#/$defs/ParameterBinding" + }, + "title": "Bindings", + "type": "array" + }, + "capability_constraints": { + "default": [], + "items": { + "$ref": "#/$defs/CapabilityConstraint" + }, + "title": "Capability Constraints", + "type": "array" + }, + "explicitness": { + "default": [], + "items": { + "$ref": "#/$defs/ExplicitnessProvenanceRecord" + }, + "title": "Explicitness", + "type": "array" + }, + "imports": { + "default": [], + "items": { + "$ref": "#/$defs/ResolvedImportProvenance" + }, + "title": "Imports", + "type": "array" + }, + "selected_profile": { + "anyOf": [ + { + "maxLength": 256, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Selected Profile" + } + }, + "required": [ + "authored_digest" + ], + "title": "InstantiationProvenance", + "type": "object" + }, + "Node": { + "additionalProperties": false, + "description": "A scenario node \u2014 either a VM or a Switch.\n\nThe ``type`` field determines which variant is active. VM fields\nare only valid when type is VM; Switch nodes carry no extra data.", + "properties": { + "asset_value": { + "anyOf": [ + { + "$ref": "#/$defs/AssetValue" + }, + { + "type": "null" + } + ], + "default": null + }, + "conditions": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Conditions", + "type": "object" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "features": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Features", + "type": "object" + }, + "injects": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Injects", + "type": "object" + }, + "os": { + "anyOf": [ + { + "$ref": "#/$defs/OSFamily" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Os" + }, + "os_version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Os Version", + "type": "string" + }, + "resources": { + "anyOf": [ + { + "$ref": "#/$defs/Resources" + }, + { + "type": "null" + } + ], + "default": null + }, + "roles": { + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/Role" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Roles", + "type": "object" + }, + "runtime": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeConfiguration" + }, + { + "type": "null" + } + ], + "default": null + }, + "services": { + "items": { + "$ref": "#/$defs/ServicePort" + }, + "title": "Services", + "type": "array" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/Source" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "$ref": "#/$defs/NodeType" + }, + "vulnerabilities": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type" + ], + "title": "Node", + "type": "object" + }, + "NodeType": { + "description": "Whether a node is a virtual machine or network switch.", + "enum": [ + "vm", + "switch" + ], + "title": "NodeType", + "type": "string" + }, + "NumericPredicate": { + "additionalProperties": false, + "description": "Compare a governed numeric property using an explicit unit.", + "properties": { + "expected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "title": "Expected" + }, + "kind": { + "const": "number", + "default": "number", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals", + "less_than", + "less_than_or_equal", + "greater_than", + "greater_than_or_equal" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + }, + "unit": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Unit", + "type": "string" + }, + "unit_semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Unit Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected", + "unit", + "unit_semantic_ref" + ], + "title": "NumericPredicate", + "type": "object" + }, + "OSFamily": { + "description": "Operating system family. Vocabulary from OCSF Device.os.", + "enum": [ + "windows", + "linux", + "macos", + "freebsd", + "other" + ], + "title": "OSFamily", + "type": "string" + }, + "Objective": { + "additionalProperties": false, + "description": "A declarative experiment objective.", + "properties": { + "actions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Actions", + "type": "array" + }, + "agent": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Agent", + "type": "string" + }, + "depends_on": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Depends On", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "entity": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Entity", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "success": { + "$ref": "#/$defs/ObjectiveSuccess" + }, + "targets": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Targets", + "type": "array" + }, + "window": { + "anyOf": [ + { + "$ref": "#/$defs/ObjectiveWindow" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "success" + ], + "title": "Objective", + "type": "object" + }, + "ObjectiveSuccess": { + "additionalProperties": false, + "description": "Declarative success criteria for an objective.\n\nSuccess composes backend-neutral invariant or postcondition assertions.\nProbe implementations, graded scoring, and reward remain outside this\nconstruct.", + "properties": { + "assertions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Assertions", + "type": "array" + }, + "mode": { + "anyOf": [ + { + "$ref": "#/$defs/TruthCompositionMode" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "all_of", + "title": "Mode" + }, + "threshold": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" + } + }, + "title": "ObjectiveSuccess", + "type": "object" + }, + "ObjectiveWindow": { + "additionalProperties": false, + "description": "Optional orchestration window constraining when an objective applies.", + "properties": { + "events": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Events", + "type": "array" + }, + "scripts": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Scripts", + "type": "array" + }, + "steps": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Steps", + "type": "array" + }, + "stories": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Stories", + "type": "array" + }, + "workflows": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Workflows", + "type": "array" + } + }, + "title": "ObjectiveWindow", + "type": "object" + }, + "OutcomeInterpretationParticipantScope": { + "description": "Allowed participant scope for SEM-215 interpretation rules.", + "enum": [ + "participant_local" + ], + "title": "OutcomeInterpretationParticipantScope", + "type": "string" + }, + "OutcomeInterpretationRule": { + "additionalProperties": false, + "description": "Explicit SEM-215 rule relating participant-local outcomes to meaning layers.", + "properties": { + "diagnostics": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Diagnostics", + "type": "array" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "interpretation_basis": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Interpretation Basis", + "type": "string" + }, + "limitations": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "observation_point_basis": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Observation Point Basis", + "type": "string" + }, + "participant_scope": { + "$ref": "#/$defs/OutcomeInterpretationParticipantScope" + }, + "semantic_version": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Semantic Version", + "type": "string" + }, + "source_bindings": { + "items": { + "$ref": "#/$defs/OutcomeInterpretationSourceBinding" + }, + "minItems": 1, + "title": "Source Bindings", + "type": "array" + }, + "target_bindings": { + "items": { + "$ref": "#/$defs/OutcomeInterpretationTargetBinding" + }, + "minItems": 1, + "title": "Target Bindings", + "type": "array" + } + }, + "required": [ + "semantic_version", + "participant_scope", + "observation_point_basis", + "interpretation_basis", + "source_bindings", + "target_bindings", + "evidence_refs", + "limitations" + ], + "title": "OutcomeInterpretationRule", + "type": "object" + }, + "OutcomeInterpretationSourceBinding": { + "additionalProperties": false, + "description": "Declared input to a participant outcome interpretation rule.", + "properties": { + "diagnostics": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Diagnostics", + "type": "array" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "interpretation_role": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Interpretation Role", + "type": "string" + }, + "provenance_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ref", + "type": "string" + }, + "source_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source Id", + "type": "string" + }, + "source_layer": { + "$ref": "#/$defs/OutcomeInterpretationSourceLayer" + } + }, + "required": [ + "source_id", + "source_layer", + "ref", + "interpretation_role" + ], + "title": "OutcomeInterpretationSourceBinding", + "type": "object" + }, + "OutcomeInterpretationSourceLayer": { + "description": "Semantic layers that may feed a SEM-215 interpretation rule.", + "enum": [ + "participant_action_outcome", + "participant_episode_status", + "objective_result", + "workflow_result", + "evaluation_result", + "evidence_claim", + "reward_signal", + "benchmark_milestone", + "subtask", + "gold_step", + "human_assistance", + "scaffold_variant", + "cost_resource_telemetry", + "privacy_redaction_result" + ], + "title": "OutcomeInterpretationSourceLayer", + "type": "string" + }, + "OutcomeInterpretationTargetBinding": { + "additionalProperties": false, + "description": "Declared output relation from a participant outcome interpretation rule.", + "properties": { + "diagnostics": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Diagnostics", + "type": "array" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "governance_ref": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Governance Ref" + }, + "limitations": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Limitations", + "type": "array" + }, + "ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ref", + "type": "string" + }, + "relation": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Relation", + "type": "string" + }, + "target_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Id", + "type": "string" + }, + "target_layer": { + "$ref": "#/$defs/OutcomeInterpretationTargetLayer" + } + }, + "required": [ + "target_id", + "target_layer", + "ref", + "relation" + ], + "title": "OutcomeInterpretationTargetBinding", + "type": "object" + }, + "OutcomeInterpretationTargetLayer": { + "description": "Semantic layers a SEM-215 interpretation rule may explicitly produce.", + "enum": [ + "scenario_meaning", + "objective_result", + "workflow_result", + "evaluation_result", + "evidence_claim", + "reward_signal", + "benchmark_progress", + "privacy_redaction_result" + ], + "title": "OutcomeInterpretationTargetLayer", + "type": "string" + }, + "ParameterBinding": { + "additionalProperties": false, + "description": "One selected parameter value with an unambiguous qualified identity.", + "properties": { + "origin": { + "$ref": "#/$defs/BindingOrigin" + }, + "parameter": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 32, + "minItems": 1, + "title": "Parameter", + "type": "array" + }, + "value": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "title": "Value" + } + }, + "required": [ + "parameter", + "origin", + "value" + ], + "title": "ParameterBinding", + "type": "object" + }, + "ParticipantActionContract": { + "additionalProperties": false, + "description": "Governed semantic contract for one participant action name.", + "properties": { + "backend_failure_mappings": { + "items": { + "$ref": "#/$defs/ParticipantBackendFailureMapping" + }, + "title": "Backend Failure Mappings", + "type": "array" + }, + "backend_timing_disclosures": { + "items": { + "$ref": "#/$defs/ParticipantBackendTimingDisclosure" + }, + "title": "Backend Timing Disclosures", + "type": "array" + }, + "behavioral_granularity": { + "$ref": "#/$defs/ParticipantActionGranularity" + }, + "effects": { + "items": { + "$ref": "#/$defs/ParticipantActionEffect" + }, + "title": "Effects", + "type": "array" + }, + "evidence_expectations": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Expectations", + "type": "array" + }, + "external_mappings": { + "items": { + "$ref": "#/$defs/ExternalMappingLoss" + }, + "title": "External Mappings", + "type": "array" + }, + "failure_classes": { + "items": { + "$ref": "#/$defs/ParticipantFailureClass" + }, + "title": "Failure Classes", + "type": "array" + }, + "fidelity_claim": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Fidelity Claim", + "type": "string" + }, + "interactions": { + "items": { + "$ref": "#/$defs/ParticipantInteractionDeclaration" + }, + "title": "Interactions", + "type": "array" + }, + "lifecycle_state": { + "$ref": "#/$defs/ParticipantActionLifecycle", + "default": "active" + }, + "observation_expectations": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Observation Expectations", + "type": "array" + }, + "preconditions": { + "items": { + "$ref": "#/$defs/ParticipantActionPrecondition" + }, + "title": "Preconditions", + "type": "array" + }, + "procedure_basis": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Procedure Basis", + "type": "string" + }, + "realization_profile": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Realization Profile", + "type": "string" + }, + "semantic_version": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Semantic Version", + "type": "string" + }, + "state_transition_effects": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "State Transition Effects", + "type": "array" + }, + "temporal_contracts": { + "items": { + "$ref": "#/$defs/ParticipantTemporalContract" + }, + "title": "Temporal Contracts", + "type": "array" + } + }, + "required": [ + "semantic_version", + "behavioral_granularity", + "procedure_basis", + "realization_profile", + "fidelity_claim" + ], + "title": "ParticipantActionContract", + "type": "object" + }, + "ParticipantActionEffect": { + "additionalProperties": false, + "description": "Typed SEM-211 effect declaration on an action contract.", + "properties": { + "description": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "effect_class": { + "$ref": "#/$defs/ParticipantEffectClass" + }, + "effect_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Effect Id", + "type": "string" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "target_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Target Refs", + "type": "array" + } + }, + "required": [ + "effect_id", + "effect_class", + "description" + ], + "title": "ParticipantActionEffect", + "type": "object" + }, + "ParticipantActionGranularity": { + "description": "Behavioral granularity of a participant action contract.", + "enum": [ + "atomic", + "procedure", + "aggregate" + ], + "title": "ParticipantActionGranularity", + "type": "string" + }, + "ParticipantActionLifecycle": { + "description": "Governance lifecycle for participant action contracts.", + "enum": [ + "draft", + "active", + "deprecated" + ], + "title": "ParticipantActionLifecycle", + "type": "string" + }, + "ParticipantActionPrecondition": { + "additionalProperties": false, + "description": "Typed SEM-211 applicability precondition on an action contract.", + "properties": { + "description": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "precondition_class": { + "$ref": "#/$defs/ParticipantPreconditionClass" + }, + "precondition_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Precondition Id", + "type": "string" + }, + "support_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Support Refs", + "type": "array" + } + }, + "required": [ + "precondition_id", + "precondition_class", + "description" + ], + "title": "ParticipantActionPrecondition", + "type": "object" + }, + "ParticipantBackendFailureMapping": { + "additionalProperties": false, + "description": "Mapping from a backend diagnostic code to a portable SEM-211 failure.", + "properties": { + "backend_error_code": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Backend Error Code", + "type": "string" + }, + "diagnostic": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Diagnostic", + "type": "string" + }, + "failure_class": { + "$ref": "#/$defs/ParticipantFailureClass" + } + }, + "required": [ + "backend_error_code", + "failure_class", + "diagnostic" + ], + "title": "ParticipantBackendFailureMapping", + "type": "object" + }, + "ParticipantBackendTimingDisclosure": { + "additionalProperties": false, + "description": "Backend timing realization disclosure for SEM-213 contracts.", + "properties": { + "affected_temporal_ids": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Affected Temporal Ids", + "type": "array" + }, + "description": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "disclosure_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Disclosure Id", + "type": "string" + }, + "disclosure_kind": { + "$ref": "#/$defs/ParticipantBackendTimingDisclosureKind" + }, + "limitations": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Limitations", + "type": "array" + }, + "support_mode": { + "$ref": "#/$defs/ParticipantTemporalSupportMode" + } + }, + "required": [ + "disclosure_id", + "disclosure_kind", + "support_mode", + "description", + "affected_temporal_ids" + ], + "title": "ParticipantBackendTimingDisclosure", + "type": "object" + }, + "ParticipantBackendTimingDisclosureKind": { + "description": "Backend timing realization disclosures.", + "enum": [ + "pacing", + "dilation", + "synchronization", + "serialization", + "unsupported_guarantee" + ], + "title": "ParticipantBackendTimingDisclosureKind", + "type": "string" + }, + "ParticipantBehaviorSpecification": { + "additionalProperties": false, + "description": "First-class authored aggregate over participant behavior surfaces.", + "properties": { + "action_contract_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Action Contract Refs", + "type": "array" + }, + "ai_offensive_behavior_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Ai Offensive Behavior Refs", + "type": "array" + }, + "authority_scope_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Authority Scope Refs", + "type": "array" + }, + "backend_feature_support_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Backend Feature Support Refs", + "type": "array" + }, + "behavior_mode": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Behavior Mode" + }, + "evidence_contract_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Contract Refs", + "type": "array" + }, + "extension_policy": { + "default": "governed-extension", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Extension Policy", + "type": "string" + }, + "extensions": { + "additionalProperties": { + "$ref": "#/$defs/BehaviorSpecificationExtensionValue" + }, + "title": "Extensions", + "type": "object" + }, + "lifecycle_state": { + "$ref": "#/$defs/ParticipantBehaviorSpecificationLifecycle", + "default": "active" + }, + "observation_boundary_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Observation Boundary Refs", + "type": "array" + }, + "offensive_behavior_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Offensive Behavior Refs", + "type": "array" + }, + "outcome_interpretation_rule_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Outcome Interpretation Rule Refs", + "type": "array" + }, + "participant_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Participant Refs", + "type": "array" + }, + "participant_role_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Participant Role Refs", + "type": "array" + }, + "realization_profile_ref": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Realization Profile Ref" + }, + "semantic_version": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Semantic Version", + "type": "string" + } + }, + "required": [ + "semantic_version" + ], + "title": "ParticipantBehaviorSpecification", + "type": "object" + }, + "ParticipantBehaviorSpecificationLifecycle": { + "description": "Governance lifecycle for participant behavior specifications.", + "enum": [ + "draft", + "active", + "deprecated" + ], + "title": "ParticipantBehaviorSpecificationLifecycle", + "type": "string" + }, + "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" + }, + "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" + }, + "ParticipantInformationBoundaryClass": { + "description": "SEM-210 classes for participant information-boundary objects.", + "enum": [ + "observable_resource", + "telemetry_stream", + "tool_output", + "instruction", + "public_task_statement", + "starter_file", + "scaffold_instruction", + "subtask_guidance", + "hidden_truth", + "private_answer_key", + "canary", + "holdout_variant", + "adjudication_material", + "archival_evidence" + ], + "title": "ParticipantInformationBoundaryClass", + "type": "string" + }, + "ParticipantInteractionClass": { + "description": "SEM-209 interaction classes for multi-participant behavior.", + "enum": [ + "coordination", + "contention", + "interference", + "shared_state_change" + ], + "title": "ParticipantInteractionClass", + "type": "string" + }, + "ParticipantInteractionDeclaration": { + "additionalProperties": false, + "description": "Declared interaction semantics for a participant action contract.", + "properties": { + "interaction_class": { + "$ref": "#/$defs/ParticipantInteractionClass" + }, + "rationale": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Rationale", + "type": "string" + }, + "related_actions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Related Actions", + "type": "array" + }, + "shared_state_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Shared State Refs", + "type": "array" + }, + "target": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target", + "type": "string" + } + }, + "required": [ + "interaction_class", + "target", + "rationale" + ], + "title": "ParticipantInteractionDeclaration", + "type": "object" + }, + "ParticipantObservationBoundary": { + "additionalProperties": false, + "description": "Participant-specific observation projection boundary.", + "properties": { + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "hidden_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Hidden Refs", + "type": "array" + }, + "latency_profile": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Latency Profile", + "type": "string" + }, + "observable_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Observable Refs", + "type": "array" + }, + "observer_effects": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Observer Effects", + "type": "array" + }, + "projection_basis": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Projection Basis", + "type": "string" + }, + "realized_view_disclosure": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Realized View Disclosure" + }, + "redaction_policy": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Redaction Policy", + "type": "string" + }, + "view_rules": { + "items": { + "$ref": "#/$defs/ParticipantViewRule" + }, + "title": "View Rules", + "type": "array" + }, + "view_transitions": { + "items": { + "$ref": "#/$defs/ParticipantViewTransition" + }, + "title": "View Transitions", + "type": "array" + } + }, + "required": [ + "projection_basis", + "redaction_policy", + "latency_profile" + ], + "title": "ParticipantObservationBoundary", + "type": "object" + }, + "ParticipantPreconditionClass": { + "description": "SEM-211 precondition classes for participant action applicability.", + "enum": [ + "authority", + "capability", + "target", + "knowledge", + "resource", + "temporal", + "interaction", + "realization" + ], + "title": "ParticipantPreconditionClass", + "type": "string" + }, + "ParticipantTemporalContract": { + "additionalProperties": false, + "description": "Typed SEM-213 temporal contract for a participant action.", + "properties": { + "backend_disclosure_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Backend Disclosure Refs", + "type": "array" + }, + "clock_authority": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Clock Authority", + "type": "string" + }, + "description": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "duration_ref": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Duration Ref" + }, + "event_points": { + "items": { + "$ref": "#/$defs/ParticipantTemporalEventPoint" + }, + "minItems": 1, + "title": "Event Points", + "type": "array" + }, + "ordering_basis": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ordering Basis", + "type": "string" + }, + "randomization_basis": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Randomization Basis" + }, + "replay_boundary": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Replay Boundary" + }, + "reset_boundary": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reset Boundary" + }, + "temporal_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Temporal Id", + "type": "string" + }, + "temporal_kind": { + "$ref": "#/$defs/ParticipantTemporalContractKind" + }, + "time_domain": { + "$ref": "#/$defs/ParticipantTimeDomain" + }, + "window_ref": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Window Ref" + } + }, + "required": [ + "temporal_id", + "temporal_kind", + "time_domain", + "clock_authority", + "event_points", + "description", + "ordering_basis" + ], + "title": "ParticipantTemporalContract", + "type": "object" + }, + "ParticipantTemporalContractKind": { + "description": "SEM-213 temporal contract kinds.", + "enum": [ + "schedule", + "cadence", + "deadline", + "dwell", + "latency", + "time_window", + "timeout", + "cooldown" + ], + "title": "ParticipantTemporalContractKind", + "type": "string" + }, + "ParticipantTemporalEventPoint": { + "description": "Named participant event points used by temporal contracts.", + "enum": [ + "submit", + "start", + "end", + "observed", + "effective", + "deadline", + "window_open", + "window_close", + "reset", + "replay" + ], + "title": "ParticipantTemporalEventPoint", + "type": "string" + }, + "ParticipantTemporalSupportMode": { + "description": "How a backend supports a temporal guarantee.", + "enum": [ + "exact", + "bounded", + "disclosed_limitation", + "unsupported" + ], + "title": "ParticipantTemporalSupportMode", + "type": "string" + }, + "ParticipantTimeDomain": { + "description": "Distinct SEM-213 time domains.", + "enum": [ + "episode_step", + "scenario_time", + "simulation_time", + "backend_time", + "wall_clock_time" + ], + "title": "ParticipantTimeDomain", + "type": "string" + }, + "ParticipantViewDisposition": { + "description": "SEM-210 visibility disposition for one information object.", + "enum": [ + "observable", + "hidden", + "evidence_only", + "discovered", + "inferred", + "disclosed", + "concealed", + "deceptive" + ], + "title": "ParticipantViewDisposition", + "type": "string" + }, + "ParticipantViewHistoryEventType": { + "description": "Behavior-history event that realizes a SEM-210 visibility transition.", + "enum": [ + "action_attempted", + "state_transition_recorded", + "observation_emitted", + "episode_close" + ], + "title": "ParticipantViewHistoryEventType", + "type": "string" + }, + "ParticipantViewRule": { + "additionalProperties": false, + "description": "Visibility rule for one participant information-boundary object.", + "properties": { + "boundary_class": { + "$ref": "#/$defs/ParticipantInformationBoundaryClass" + }, + "certainty": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certainty" + }, + "disclosure_rule": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Disclosure Rule" + }, + "disposition": { + "$ref": "#/$defs/ParticipantViewDisposition" + }, + "effective_from": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Effective From" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "information_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Information Ref", + "type": "string" + }, + "latency_profile": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Latency Profile" + }, + "realized_backend_disclosure": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Realized Backend Disclosure" + }, + "visibility_basis": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Visibility Basis", + "type": "string" + } + }, + "required": [ + "information_ref", + "boundary_class", + "disposition", + "visibility_basis" + ], + "title": "ParticipantViewRule", + "type": "object" + }, + "ParticipantViewTransition": { + "additionalProperties": false, + "description": "Time-indexed SEM-210 transition over a participant view relation.", + "properties": { + "action_instance_id": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action Instance Id" + }, + "certainty": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Certainty", + "type": "string" + }, + "disclosure_rule": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Disclosure Rule" + }, + "effective_from": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Effective From", + "type": "string" + }, + "effective_order": { + "title": "Effective Order", + "type": "integer" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "from_disposition": { + "$ref": "#/$defs/ParticipantViewDisposition" + }, + "history_event_type": { + "$ref": "#/$defs/ParticipantViewHistoryEventType" + }, + "information_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Information Ref", + "type": "string" + }, + "latency_profile": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Latency Profile", + "type": "string" + }, + "realized_backend_disclosure": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Realized Backend Disclosure" + }, + "to_disposition": { + "$ref": "#/$defs/ParticipantViewDisposition" + }, + "transition_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Transition Id", + "type": "string" + }, + "transition_kind": { + "$ref": "#/$defs/ParticipantViewTransitionKind" + }, + "trigger": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Trigger", + "type": "string" + } + }, + "required": [ + "transition_id", + "transition_kind", + "information_ref", + "trigger", + "effective_from", + "effective_order", + "history_event_type", + "from_disposition", + "to_disposition", + "certainty", + "latency_profile" + ], + "title": "ParticipantViewTransition", + "type": "object" + }, + "ParticipantViewTransitionKind": { + "description": "SEM-210 state transitions that alter participant visibility over time.", + "enum": [ + "discovery", + "inference", + "concealment", + "disclosure", + "revocation", + "deception" + ], + "title": "ParticipantViewTransitionKind", + "type": "string" + }, + "PasswordStrength": { + "description": "How resistant the account password is to cracking.", + "enum": [ + "weak", + "medium", + "strong", + "none" + ], + "title": "PasswordStrength", + "type": "string" + }, + "PresencePredicate": { + "additionalProperties": false, + "description": "Test whether a governed property is present on a subject.", + "properties": { + "kind": { + "const": "presence", + "default": "presence", + "title": "Kind", + "type": "string" + }, + "operator": { + "const": "exists", + "default": "exists", + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref" + ], + "title": "PresencePredicate", + "type": "object" + }, + "Proposition": { + "additionalProperties": false, + "description": "A side-effect-free claim over a finite set of stable SDL subjects.", + "properties": { + "basis": { + "$ref": "#/$defs/PropositionBasis" + }, + "description": { + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_requirements": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Requirements", + "type": "array" + }, + "predicate": { + "discriminator": { + "mapping": { + "boolean": "#/$defs/BooleanPredicate", + "number": "#/$defs/NumericPredicate", + "presence": "#/$defs/PresencePredicate", + "string": "#/$defs/StringPredicate" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/PresencePredicate" + }, + { + "$ref": "#/$defs/BooleanPredicate" + }, + { + "$ref": "#/$defs/StringPredicate" + }, + { + "$ref": "#/$defs/NumericPredicate" + } + ], + "title": "Predicate" + }, + "quantifier": { + "$ref": "#/$defs/SubjectQuantifier", + "default": "all" + }, + "subjects": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Subjects", + "type": "array" + }, + "threshold": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" + } + }, + "required": [ + "description", + "subjects", + "basis", + "predicate" + ], + "title": "Proposition", + "type": "object" + }, + "PropositionBasis": { + "description": "Source of facts that may decide a proposition.", + "enum": [ + "declared_state", + "observed_state" + ], + "title": "PropositionBasis", + "type": "string" + }, + "Relationship": { + "additionalProperties": false, + "description": "A typed directed edge between two named scenario elements.\n\nSource and target can reference any named element in the scenario:\nnodes, features, accounts, entities, or infrastructure entries.\nThe validator checks that both endpoints resolve.\n\nThe ``properties`` dict carries type-specific metadata (e.g.,\n``trust_type: parent-child`` for AD trusts, ``protocol: SAML``\nfor federation). It's a flat dict rather than typed sub-models\nbecause relationship properties vary widely and we don't want\nto gate expressiveness on pre-modeling every variant.\n\n``database_access`` and ``mail_access`` are typed exceptions where\nprotocol/auth details need structural validation rather than prose.\n``forwarding_edge``, ``service_integration``, and ``proxy_upstream`` are\nthe same kind of typed exception for, respectively, a forwarding /\nintel-sync agent's inter-node trust edge (SCN-010 \u00a75.7), a platform\nconsumer-to-engine service integration, and a reverse-proxy/gateway\nroute-to-origin upstream hop. Each keeps protocol/auth/topology facts\nstructurally validated and cross-referable rather than buried in prose.", + "properties": { + "database_access": { + "anyOf": [ + { + "$ref": "#/$defs/RelationshipDatabaseAccess" + }, + { + "type": "null" + } + ], + "default": null + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "forwarding_edge": { + "anyOf": [ + { + "$ref": "#/$defs/RelationshipForwardingEdge" + }, + { + "type": "null" + } + ], + "default": null + }, + "mail_access": { + "anyOf": [ + { + "$ref": "#/$defs/RelationshipMailAccess" + }, + { + "type": "null" + } + ], + "default": null + }, + "properties": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Properties", + "type": "object" + }, + "proxy_upstream": { + "anyOf": [ + { + "$ref": "#/$defs/RelationshipProxyUpstream" + }, + { + "type": "null" + } + ], + "default": null + }, + "service_integration": { + "anyOf": [ + { + "$ref": "#/$defs/RelationshipServiceIntegration" + }, + { + "type": "null" + } + ], + "default": null + }, + "source": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source", + "type": "string" + }, + "target": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target", + "type": "string" + }, + "type": { + "$ref": "#/$defs/RelationshipType" + } + }, + "required": [ + "type", + "source", + "target" + ], + "title": "Relationship", + "type": "object" + }, + "RelationshipDatabaseAccess": { + "additionalProperties": false, + "description": "Typed database-access detail carried by a top-level relationship edge.\n\nWhen an application connects to a database, the relationship's ``target``\nresolves to the database (service or logical database) and this block keeps\nthe ``role_ref`` (a ``role_id`` in that service) and ``auth_method``\nstructurally validated rather than recorded as prose (ADR-029 \u00a74).", + "properties": { + "auth_method": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseAuthMethod" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Auth Method" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "role_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Role Ref", + "type": "string" + } + }, + "title": "RelationshipDatabaseAccess", + "type": "object" + }, + "RelationshipForwardingEdge": { + "additionalProperties": false, + "description": "Typed forwarding-trust detail carried by a top-level relationship edge.\n\nThe inter-node trust edge between a forwarding / intel-sync agent\n(``forwarder_ref``, a ``forwarding_agent_id``) and the downstream consumer\nit ships to. The consumer's transport listener is named by\n``target_listener_role`` \u2014 REUSING the manager-side\n``RuntimeSecurityMonitoringListenerRole`` lattice rather than forking a\nparallel enum (SCN-010 \u00a75.7).\n\nThe agent enrollment identity itself is never recorded: only the closed\n``RuntimeForwardingEnrollmentClassification`` lattice (``none`` /\n``redacted`` / ``operator_secret``). A present enrollment-identity ref must\nclassify ``redacted`` / ``operator_secret`` \u2014 a raw identity is never the\nportable model.", + "properties": { + "crypto_method": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Crypto Method", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enrollment_identity_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingEnrollmentClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "none", + "title": "Enrollment Identity Classification" + }, + "enrollment_identity_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Enrollment Identity Ref", + "type": "string" + }, + "forwarder_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Forwarder Ref", + "type": "string" + }, + "parse_format": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingParseFormat" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Parse Format" + }, + "protocol": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + }, + "target_listener_role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringListenerRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Target Listener Role" + } + }, + "required": [ + "forwarder_ref" + ], + "title": "RelationshipForwardingEdge", + "type": "object" + }, + "RelationshipMailAccess": { + "additionalProperties": false, + "description": "Typed mail-access detail carried by a top-level relationship edge.", + "properties": { + "auth_mechanism": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailAuthMechanism" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Auth Mechanism" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "domain_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Domain Ref", + "type": "string" + }, + "listener_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Listener Ref", + "type": "string" + }, + "mailbox_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mailbox Ref", + "type": "string" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Protocol" + }, + "tls_mode": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailTlsMode" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Tls Mode" + } + }, + "title": "RelationshipMailAccess", + "type": "object" + }, + "RelationshipProxyUpstream": { + "additionalProperties": false, + "description": "Typed proxy-upstream detail carried by a top-level relationship edge.\n\nWhen a reverse proxy / gateway forwards a route to an upstream origin, the\nrelationship's ``target`` resolves to that origin and this block keeps the\nforwarding facts structurally validated rather than recorded as prose:\n``route_ref`` (a ``route_id`` on the proxy's application surface), the\nresolved upstream node/service, whether the client-facing TLS is terminated\nat the proxy, whether the proxy-to-origin hop is plaintext, and any request\n``body_limit`` the proxy enforces.", + "properties": { + "body_limit": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Body Limit", + "type": "string" + }, + "client_tls_terminated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Client Tls Terminated" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "origin_plaintext": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Origin Plaintext" + }, + "route_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Route Ref", + "type": "string" + }, + "upstream_node_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Upstream Node Ref", + "type": "string" + }, + "upstream_service_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Upstream Service Ref", + "type": "string" + } + }, + "required": [ + "route_ref" + ], + "title": "RelationshipProxyUpstream", + "type": "object" + }, + "RelationshipServiceIntegration": { + "additionalProperties": false, + "description": "Typed service-integration detail carried by a top-level relationship edge.\n\nWhen a consumer application integrates with a platform engine (analyzer,\nresponder, webhook, notification, or enrichment), the relationship's\nendpoints resolve to the two platform applications and this block keeps the\n``integration_kind``, ``auth_principal_ref`` (an ``app_authorization``\n``principal_id`` in the target), and ``direction`` structurally validated\nrather than recorded as prose. ``consumer_ref``/``engine_ref`` are the\n``platform_application_id`` symbols of the two endpoints; a ``${var}``\nplaceholder is permitted in those refs.", + "properties": { + "auth_principal_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Auth Principal Ref", + "type": "string" + }, + "consumer_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Consumer Ref", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "direction": { + "anyOf": [ + { + "$ref": "#/$defs/RelationshipServiceIntegrationDirection" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "bidirectional", + "title": "Direction" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "engine_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Engine Ref", + "type": "string" + }, + "integration_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RelationshipServiceIntegrationKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Integration Kind" + } + }, + "title": "RelationshipServiceIntegration", + "type": "object" + }, + "RelationshipServiceIntegrationDirection": { + "description": "Closed structural vocabulary for an integration edge's data direction.\n\nClosed structural vocabulary: carries neither ``unknown`` nor ``other``.", + "enum": [ + "inbound", + "outbound", + "bidirectional" + ], + "title": "RelationshipServiceIntegrationDirection", + "type": "string" + }, + "RelationshipServiceIntegrationKind": { + "description": "Open taxonomy of how a consumer integrates with a platform engine.\n\nOpen taxonomy: carries both ``unknown`` and ``other``.", + "enum": [ + "analyzer", + "responder", + "webhook", + "notification", + "enrichment", + "unknown", + "other" + ], + "title": "RelationshipServiceIntegrationKind", + "type": "string" + }, + "RelationshipType": { + "description": "How two scenario elements relate to each other.", + "enum": [ + "authenticates_with", + "trusts", + "federates_with", + "connects_to", + "depends_on", + "manages", + "replicates_to" + ], + "title": "RelationshipType", + "type": "string" + }, + "ResolvedImportProvenance": { + "additionalProperties": false, + "description": "Portable evidence emitted after one trusted module resolution.", + "properties": { + "bindings": { + "default": [], + "items": { + "$ref": "#/$defs/ParameterBinding" + }, + "title": "Bindings", + "type": "array" + }, + "content_digest": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Content Digest", + "type": "string" + }, + "export_hash": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Export Hash", + "type": "string" + }, + "manifest_digest": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Manifest Digest", + "type": "string" + }, + "module_id": { + "maxLength": 129, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Module Id", + "type": "string" + }, + "module_version": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Module Version", + "type": "string" + }, + "namespace": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 31, + "minItems": 1, + "title": "Namespace", + "type": "array" + }, + "requested_digest": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Requested Digest", + "type": "string" + }, + "requested_source": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Requested Source", + "type": "string" + }, + "requested_version": { + "default": "*", + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Requested Version", + "type": "string" + }, + "resolved_source": { + "maxLength": 4096, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Resolved Source", + "type": "string" + }, + "signer_id": { + "default": "", + "maxLength": 256, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Signer Id", + "type": "string" + } + }, + "required": [ + "namespace", + "requested_source", + "module_id", + "module_version", + "resolved_source" + ], + "title": "ResolvedImportProvenance", + "type": "object" + }, + "Resources": { + "additionalProperties": false, + "description": "Compute resources for a VM node.", + "properties": { + "cpu": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "description": "Number of CPU cores", + "title": "Cpu" + }, + "ram": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "description": "RAM in bytes (parsed from human-readable)", + "title": "Ram" + } + }, + "required": [ + "ram", + "cpu" + ], + "title": "Resources", + "type": "object" + }, + "Role": { + "additionalProperties": false, + "description": "A named role on a VM with optional entity assignments.\n\nShorthand: ``admin: \"username\"`` (just the username string).\nLonghand: ``admin: {username: \"admin\", entities: [\"blue-team.bob\"]}``.", + "properties": { + "entities": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Entities", + "type": "array" + }, + "username": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Username", + "type": "string" + } + }, + "required": [ + "username" + ], + "title": "Role", + "type": "object" + }, + "RuntimeAppAuthorization": { + "additionalProperties": false, + "description": "Application-internal RBAC store inventory for a single owning spine.", + "properties": { + "app_authorization_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "auth_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Auth Enabled" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "permission_grants": { + "items": { + "$ref": "#/$defs/RuntimeAppAuthorizationGrant" + }, + "title": "Permission Grants", + "type": "array" + }, + "principals": { + "items": { + "$ref": "#/$defs/RuntimeAppAuthorizationPrincipal" + }, + "title": "Principals", + "type": "array" + }, + "resource_vocabulary": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeAppAuthorizationResourceVocabulary" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Resource Vocabulary" + }, + "role_mappings": { + "items": { + "$ref": "#/$defs/RuntimeAppAuthorizationRoleMapping" + }, + "title": "Role Mappings", + "type": "array" + }, + "roles": { + "items": { + "$ref": "#/$defs/RuntimeAppAuthorizationRole" + }, + "title": "Roles", + "type": "array" + }, + "tenants": { + "items": { + "$ref": "#/$defs/RuntimeAppAuthorizationTenant" + }, + "title": "Tenants", + "type": "array" + } + }, + "required": [ + "app_authorization_id" + ], + "title": "RuntimeAppAuthorization", + "type": "object" + }, + "RuntimeAppAuthorizationCredentialClassification": { + "description": "Closed posture vocabulary for a principal credential.\n\nThe raw credential value MUST NOT be recorded; the principal model has no\nfield that can hold one. This vocabulary is the only credential\nrepresentation in the surface. Closed structural vocab: no ``unknown`` /\n``other``.", + "enum": [ + "none", + "redacted", + "operator_secret" + ], + "title": "RuntimeAppAuthorizationCredentialClassification", + "type": "string" + }, + "RuntimeAppAuthorizationGrant": { + "additionalProperties": false, + "description": "A resource-scoped permission grant (role -> actions -> resource pattern).\n\n``resource_kind`` is the single author-settable source of truth for the\nresource vocabulary; the owning authorization's ``resource_vocabulary`` is\nthe declared set validated against these grants.", + "properties": { + "actions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Actions", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "effect": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeAppAuthorizationGrantEffect" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "allow", + "title": "Effect" + }, + "grant_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "resource_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeAppAuthorizationResourceVocabulary" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Resource Kind" + }, + "resource_patterns": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Resource Patterns", + "type": "array" + }, + "role_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Role Ref", + "type": "string" + } + }, + "required": [ + "grant_id" + ], + "title": "RuntimeAppAuthorizationGrant", + "type": "object" + }, + "RuntimeAppAuthorizationGrantEffect": { + "description": "Closed allow/deny effect for a permission grant.\n\nClosed structural vocab: no ``unknown`` / ``other``.", + "enum": [ + "allow", + "deny" + ], + "title": "RuntimeAppAuthorizationGrantEffect", + "type": "string" + }, + "RuntimeAppAuthorizationPrincipal": { + "additionalProperties": false, + "description": "A user, service account, API key, or backend role known to the store.\n\nNo raw secret is ever carried: the credential posture is recorded purely\nvia :attr:`credential_classification`.", + "properties": { + "backend_roles": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Backend Roles", + "type": "array" + }, + "credential_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeAppAuthorizationCredentialClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "none", + "title": "Credential Classification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hidden" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeAppAuthorizationPrincipalKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "principal_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "reserved": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reserved" + } + }, + "required": [ + "principal_id" + ], + "title": "RuntimeAppAuthorizationPrincipal", + "type": "object" + }, + "RuntimeAppAuthorizationPrincipalKind": { + "description": "Open taxonomy of authorization principal kinds.\n\nCarries both ``unknown`` and ``other`` (extensible real-world vocab).", + "enum": [ + "user", + "service_account", + "api_key", + "backend_role", + "unknown", + "other" + ], + "title": "RuntimeAppAuthorizationPrincipalKind", + "type": "string" + }, + "RuntimeAppAuthorizationResourceVocabulary": { + "description": "Open spine discriminator for the resource space an RBAC store governs.\n\nTier placement (storage RBAC vs presentation RBAC) is derived from which\nspine references the authorization, not declared here. Open taxonomy:\ncarries both ``unknown`` and ``other``.", + "enum": [ + "index_pattern", + "cql_resource", + "redis_acl", + "app_resource", + "unknown", + "other" + ], + "title": "RuntimeAppAuthorizationResourceVocabulary", + "type": "string" + }, + "RuntimeAppAuthorizationRole": { + "additionalProperties": false, + "description": "A named role defined within the application authorization store.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "role_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "role_id" + ], + "title": "RuntimeAppAuthorizationRole", + "type": "object" + }, + "RuntimeAppAuthorizationRoleMapping": { + "additionalProperties": false, + "description": "A mapping that binds backend roles, users, or hosts onto a local role.", + "properties": { + "backend_roles": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Backend Roles", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "hosts": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Hosts", + "type": "array" + }, + "mapping_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "role_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Role Ref", + "type": "string" + }, + "users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Users", + "type": "array" + } + }, + "required": [ + "mapping_id" + ], + "title": "RuntimeAppAuthorizationRoleMapping", + "type": "object" + }, + "RuntimeAppAuthorizationTenant": { + "additionalProperties": false, + "description": "A tenant/namespace scope within the application authorization store.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "tenant_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "tenant_id" + ], + "title": "RuntimeAppAuthorizationTenant", + "type": "object" + }, + "RuntimeApplicationDisclosure": { + "additionalProperties": false, + "description": "Observable error or information-disclosure behavior of a route.\n\n``disclosure`` is a classified description of what the application exposes\n(a stack trace, an internal path, a backend version), never the raw payload,\ntoken, password, or cookie itself.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "disclosure": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Disclosure", + "type": "string" + }, + "sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Sensitivity" + }, + "status_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status Code" + }, + "trigger": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Trigger", + "type": "string" + } + }, + "title": "RuntimeApplicationDisclosure", + "type": "object" + }, + "RuntimeApplicationExposedField": { + "additionalProperties": false, + "description": "A route-visible fixture secret or intentionally exposed diagnostic field.\n\nThe sensitivity vocabulary is shared with the rest of the runtime surface.\nA ``redacted`` or ``operator_secret`` field must omit its raw ``value``.\nOther values, including credential-shaped fixture facts, are scenario\ncontent needed for range realization and participant observation.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Sensitivity" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "RuntimeApplicationExposedField", + "type": "object" + }, + "RuntimeApplicationParameter": { + "additionalProperties": false, + "description": "A typed request input observed on an application route.\n\n``location`` distinguishes path variables, query string, headers, cookies,\nform fields, JSON body fields, and uploaded-file fields.", + "properties": { + "data_type": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Data Type", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "location": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeApplicationParameterLocation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Location" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Required" + } + }, + "required": [ + "name" + ], + "title": "RuntimeApplicationParameter", + "type": "object" + }, + "RuntimeApplicationParameterLocation": { + "description": "Where a request input is carried on an observed route.", + "enum": [ + "path", + "query", + "header", + "cookie", + "form", + "json_body", + "uploaded_file", + "other", + "unknown" + ], + "title": "RuntimeApplicationParameterLocation", + "type": "string" + }, + "RuntimeApplicationProtocol": { + "description": "Application-layer protocol class for an observed application surface.", + "enum": [ + "http", + "https", + "ws", + "wss", + "other", + "unknown" + ], + "title": "RuntimeApplicationProtocol", + "type": "string" + }, + "RuntimeApplicationRedirect": { + "additionalProperties": false, + "description": "An observed redirect a route can issue.\n\nTarget path/URL, status code, and the triggering condition are kept\ndistinct; the target is recorded verbatim, never resolved.", + "properties": { + "condition": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Condition", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "status_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status Code" + }, + "target": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target", + "type": "string" + } + }, + "required": [ + "target" + ], + "title": "RuntimeApplicationRedirect", + "type": "object" + }, + "RuntimeApplicationResponse": { + "additionalProperties": false, + "description": "An observed response a route can produce.", + "properties": { + "content_type": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Content Type", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "status_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Status Code" + } + }, + "required": [ + "status_code" + ], + "title": "RuntimeApplicationResponse", + "type": "object" + }, + "RuntimeApplicationRoute": { + "additionalProperties": false, + "description": "An observed application route \u2014 a participant-visible endpoint.\n\n``route_id`` is the stable identity; ``path`` is data and may carry path\nvariables, may be shared across HTTP methods, and is never a mapping key.", + "properties": { + "auth_required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Auth Required" + }, + "auth_scheme": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Auth Scheme", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "disclosures": { + "items": { + "$ref": "#/$defs/RuntimeApplicationDisclosure" + }, + "title": "Disclosures", + "type": "array" + }, + "exposed_fields": { + "items": { + "$ref": "#/$defs/RuntimeApplicationExposedField" + }, + "title": "Exposed Fields", + "type": "array" + }, + "methods": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Methods", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "parameters": { + "items": { + "$ref": "#/$defs/RuntimeApplicationParameter" + }, + "title": "Parameters", + "type": "array" + }, + "path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/$defs/RuntimeApplicationRedirect" + }, + "title": "Redirects", + "type": "array" + }, + "responses": { + "items": { + "$ref": "#/$defs/RuntimeApplicationResponse" + }, + "title": "Responses", + "type": "array" + }, + "route_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Route Id", + "type": "string" + }, + "session_required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Session Required" + }, + "static_assets": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Static Assets", + "type": "array" + }, + "templates": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Templates", + "type": "array" + }, + "upstream_target": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeApplicationRouteUpstreamTarget" + }, + { + "type": "null" + } + ], + "default": null + }, + "vulnerability_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Vulnerability Refs", + "type": "array" + } + }, + "required": [ + "route_id", + "path" + ], + "title": "RuntimeApplicationRoute", + "type": "object" + }, + "RuntimeApplicationRouteUpstreamScheme": { + "description": "Wire scheme a reverse proxy uses to reach a route's upstream origin.\n\nThis is a CLOSED taxonomy: an upstream is reached over either cleartext\nHTTP or TLS-wrapped HTTPS. Unlike the open ``RuntimeApplicationProtocol``,\nit carries neither ``other`` nor ``unknown`` \u2014 a proxy-to-origin hop that is\nneither HTTP nor HTTPS is not an application route upstream.", + "enum": [ + "http", + "https" + ], + "title": "RuntimeApplicationRouteUpstreamScheme", + "type": "string" + }, + "RuntimeApplicationRouteUpstreamTarget": { + "additionalProperties": false, + "description": "The origin a reverse-proxied route forwards to.\n\nA proxy/gateway route does not itself serve content; it relays to an\nupstream origin on this or another node. ``target_node_ref`` /\n``target_service`` name that origin (the service-name forms mirror\n``RuntimeApplicationSurface.service``), ``scheme`` is the CLOSED\nproxy-to-origin wire scheme, and ``tls_terminated_here`` records whether\nTLS is terminated at the proxy (re-encrypted or plaintext to the origin).\nAll fields are observation metadata; nothing is resolved.", + "properties": { + "scheme": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeApplicationRouteUpstreamScheme" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "http", + "title": "Scheme" + }, + "target_node_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Node Ref", + "type": "string" + }, + "target_service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Service", + "type": "string" + }, + "tls_terminated_here": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tls Terminated Here" + } + }, + "title": "RuntimeApplicationRouteUpstreamTarget", + "type": "object" + }, + "RuntimeApplicationSurface": { + "additionalProperties": false, + "description": "An observed application surface hosted by a transport service on a node.\n\n``service`` references the owning same-node ``Node.services[].name`` (bare\nname or the qualified ``nodes..services.`` form). The surface is\nobservation metadata; it never mutates ``Node.services``.", + "properties": { + "application_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "base_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Base Path", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "framework": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Framework", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeApplicationProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "http", + "title": "Protocol" + }, + "routes": { + "items": { + "$ref": "#/$defs/RuntimeApplicationRoute" + }, + "title": "Routes", + "type": "array" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + } + }, + "required": [ + "application_id" + ], + "title": "RuntimeApplicationSurface", + "type": "object" + }, + "RuntimeCapabilityOverrideScope": { + "description": "Scope of a ``RuntimeProcessCapabilityOverride``.\n\n``PROCESS`` records a capability delta that applies only to the subject\nprocess itself; ``SUBTREE`` records a delta that applies to the subject\nprocess and every descendant it spawns (the motivating case is\n``capsh --drop=cap_audit_control`` exec'ing ``sshd``, after which the\ninteractive shell subtree runs without ``CAP_AUDIT_CONTROL``).", + "enum": [ + "process", + "subtree" + ], + "title": "RuntimeCapabilityOverrideScope", + "type": "string" + }, + "RuntimeCapabilityPolicy": { + "additionalProperties": false, + "description": "Linux/container capability policy observed for a runtime node.", + "properties": { + "add": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Add", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "drop": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Drop", + "type": "array" + }, + "effective": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Effective", + "type": "array" + }, + "process_overrides": { + "items": { + "$ref": "#/$defs/RuntimeProcessCapabilityOverride" + }, + "title": "Process Overrides", + "type": "array" + }, + "required": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Required", + "type": "array" + } + }, + "title": "RuntimeCapabilityPolicy", + "type": "object" + }, + "RuntimeConfiguration": { + "additionalProperties": false, + "description": "Observed runtime configuration facts attached to a VM node.", + "properties": { + "app_authorizations": { + "items": { + "$ref": "#/$defs/RuntimeAppAuthorization" + }, + "title": "App Authorizations", + "type": "array" + }, + "applications": { + "items": { + "$ref": "#/$defs/RuntimeApplicationSurface" + }, + "title": "Applications", + "type": "array" + }, + "container": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeContainerConfiguration" + }, + { + "type": "null" + } + ], + "default": null + }, + "database_services": { + "items": { + "$ref": "#/$defs/RuntimeDatabaseService" + }, + "title": "Database Services", + "type": "array" + }, + "datastore_services": { + "items": { + "$ref": "#/$defs/RuntimeDatastoreService" + }, + "title": "Datastore Services", + "type": "array" + }, + "dependency_manifests": { + "items": { + "$ref": "#/$defs/RuntimeDependencyManifest" + }, + "title": "Dependency Manifests", + "type": "array" + }, + "dns_services": { + "items": { + "$ref": "#/$defs/RuntimeDnsService" + }, + "title": "Dns Services", + "type": "array" + }, + "environment": { + "items": { + "$ref": "#/$defs/RuntimeEnvironmentVariable" + }, + "title": "Environment", + "type": "array" + }, + "file_services": { + "items": { + "$ref": "#/$defs/RuntimeFileService" + }, + "title": "File Services", + "type": "array" + }, + "filesystem_inventory": { + "items": { + "$ref": "#/$defs/RuntimeFilesystemEntry" + }, + "title": "Filesystem Inventory", + "type": "array" + }, + "forwarding_agents": { + "items": { + "allOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgent" + }, + { + "properties": { + "forwarding_agent_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "type": "object" + } + ] + }, + "title": "Forwarding Agents", + "type": "array" + }, + "health": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeHealthObservation" + }, + { + "type": "null" + } + ], + "default": null + }, + "identity_authorities": { + "items": { + "$ref": "#/$defs/RuntimeIdentityAuthority" + }, + "title": "Identity Authorities", + "type": "array" + }, + "linux_capabilities": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeCapabilityPolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "local_control_interfaces": { + "items": { + "$ref": "#/$defs/RuntimeControlInterface" + }, + "title": "Local Control Interfaces", + "type": "array" + }, + "local_identity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeLocalIdentityInventory" + }, + { + "type": "null" + } + ], + "default": null + }, + "mail_services": { + "items": { + "$ref": "#/$defs/RuntimeMailService" + }, + "title": "Mail Services", + "type": "array" + }, + "mounts": { + "items": { + "$ref": "#/$defs/RuntimeMount" + }, + "title": "Mounts", + "type": "array" + }, + "network": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkRealization" + }, + { + "type": "null" + } + ], + "default": null + }, + "network_detection_engines": { + "items": { + "$ref": "#/$defs/RuntimeNetworkDetectionEngine" + }, + "title": "Network Detection Engines", + "type": "array" + }, + "network_sensors": { + "items": { + "$ref": "#/$defs/RuntimeNetworkSensor" + }, + "title": "Network Sensors", + "type": "array" + }, + "operational_policy": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeOperationalPolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "orchestration_authorities": { + "items": { + "$ref": "#/$defs/RuntimeOrchestrationAuthority" + }, + "title": "Orchestration Authorities", + "type": "array" + }, + "package_vulnerabilities": { + "items": { + "$ref": "#/$defs/RuntimePackageVulnerabilityFinding" + }, + "title": "Package Vulnerabilities", + "type": "array" + }, + "packages": { + "items": { + "$ref": "#/$defs/RuntimePackage" + }, + "title": "Packages", + "type": "array" + }, + "platform_applications": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplication" + }, + "title": "Platform Applications", + "type": "array" + }, + "processes": { + "items": { + "$ref": "#/$defs/RuntimeProcessIdentity" + }, + "title": "Processes", + "type": "array" + }, + "scheduled_jobs": { + "items": { + "$ref": "#/$defs/RuntimeScheduledJob" + }, + "title": "Scheduled Jobs", + "type": "array" + }, + "security_monitoring_managers": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringManager" + }, + "title": "Security Monitoring Managers", + "type": "array" + }, + "service_listeners": { + "items": { + "$ref": "#/$defs/RuntimeServiceListener" + }, + "title": "Service Listeners", + "type": "array" + }, + "service_manager_units": { + "items": { + "$ref": "#/$defs/ServiceManagerUnit" + }, + "title": "Service Manager Units", + "type": "array" + }, + "software_components": { + "items": { + "$ref": "#/$defs/RuntimeSoftwareComponent" + }, + "title": "Software Components", + "type": "array" + }, + "ssh_servers": { + "items": { + "$ref": "#/$defs/RuntimeSshServer" + }, + "title": "Ssh Servers", + "type": "array" + } + }, + "title": "RuntimeConfiguration", + "type": "object" + }, + "RuntimeContainerConfiguration": { + "additionalProperties": false, + "description": "Observed container runtime host and security configuration facts.", + "properties": { + "autoremove": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Autoremove" + }, + "cgroup_parent": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Cgroup Parent", + "type": "string" + }, + "command": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Command", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "device_cgroup_rules": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Device Cgroup Rules", + "type": "array" + }, + "devices": { + "items": { + "$ref": "#/$defs/RuntimeDeviceMapping" + }, + "title": "Devices", + "type": "array" + }, + "dns": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Dns", + "type": "array" + }, + "dns_options": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Dns Options", + "type": "array" + }, + "dns_search": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Dns Search", + "type": "array" + }, + "entrypoint": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Entrypoint", + "type": "array" + }, + "extra_hosts": { + "items": { + "$ref": "#/$defs/RuntimeExtraHost" + }, + "title": "Extra Hosts", + "type": "array" + }, + "group_add": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Group Add", + "type": "array" + }, + "init_process": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeInitProcess" + }, + { + "type": "null" + } + ], + "default": null + }, + "log_driver": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Log Driver", + "type": "string" + }, + "log_options": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Log Options", + "type": "object" + }, + "masked_paths": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Masked Paths", + "type": "array" + }, + "namespaces": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNamespaceConfiguration" + }, + { + "type": "null" + } + ], + "default": null + }, + "privileged": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Privileged" + }, + "publish_all_ports": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Publish All Ports" + }, + "read_only_paths": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Read Only Paths", + "type": "array" + }, + "read_only_rootfs": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Read Only Rootfs" + }, + "runtime_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Runtime Name", + "type": "string" + }, + "seccomp_profile": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Seccomp Profile", + "type": "string" + }, + "security_opt": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Security Opt", + "type": "array" + }, + "shm_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Shm Size" + } + }, + "title": "RuntimeContainerConfiguration", + "type": "object" + }, + "RuntimeControlInterface": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "bind_source_sensitivity": { + "anyOf": [ + { + "pattern": "^[Rr][Ee][Dd][Aa][Cc][Tt][Ee][Dd]$", + "type": "string" + }, + { + "pattern": "^[Oo][Pp][Ee][Rr][Aa][Tt][Oo][Rr][_-][Ss][Ee][Cc][Rr][Ee][Tt]$", + "type": "string" + } + ] + } + }, + "required": [ + "bind_source_sensitivity" + ] + }, + "then": { + "not": { + "properties": { + "bind_source": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "bind_source" + ] + } + } + } + ], + "description": "A non-network local control API exposed inside a runtime node.", + "properties": { + "access": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeControlInterfaceAccess" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Access" + }, + "bind_source": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Bind Source", + "type": "string" + }, + "bind_source_sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Bind Source Sensitivity" + }, + "control_interface_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Control Interface Id", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeControlInterfaceKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unix_socket", + "title": "Kind" + }, + "path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "protocol": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + } + }, + "required": [ + "control_interface_id", + "path" + ], + "title": "RuntimeControlInterface", + "type": "object" + }, + "RuntimeControlInterfaceAccess": { + "description": "Observed local-control access mode.", + "enum": [ + "read_only", + "read_write", + "unknown", + "other" + ], + "title": "RuntimeControlInterfaceAccess", + "type": "string" + }, + "RuntimeControlInterfaceKind": { + "description": "Path-local control interface shape observed at runtime.", + "enum": [ + "unix_socket", + "named_pipe", + "file", + "other", + "unknown" + ], + "title": "RuntimeControlInterfaceKind", + "type": "string" + }, + "RuntimeDatabaseService": { + "additionalProperties": false, + "description": "An observed database service hosted by a transport service on a node.\n\n``service`` references the owning same-node ``Node.services[].name`` (bare\nname or the qualified ``nodes..services.`` form). The inventory\nis observation metadata; it never mutates ``Node.services``.", + "properties": { + "database_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "databases": { + "items": { + "$ref": "#/$defs/Database" + }, + "title": "Databases", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "engine": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseEngine" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Engine" + }, + "grants": { + "items": { + "$ref": "#/$defs/DatabaseGrant" + }, + "title": "Grants", + "type": "array" + }, + "listeners": { + "items": { + "$ref": "#/$defs/DatabaseListener" + }, + "title": "Listeners", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/DatabaseProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Protocol" + }, + "roles": { + "items": { + "$ref": "#/$defs/DatabaseRole" + }, + "title": "Roles", + "type": "array" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "settings": { + "items": { + "$ref": "#/$defs/DatabaseSetting" + }, + "title": "Settings", + "type": "array" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "database_service_id" + ], + "title": "RuntimeDatabaseService", + "type": "object" + }, + "RuntimeDatastoreCluster": { + "additionalProperties": false, + "description": "The single observed cluster posture of a datastore service.\n\nCaptures the cluster-identity facts a search/wide-column cluster exposes:\nnative UUID, aggregate cardinality and size, shard totals, health,\ndiscovery mode, partitioner, and native protocol version.", + "properties": { + "cluster_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Cluster Id", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "discovery_mode": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Discovery Mode", + "type": "string" + }, + "doc_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doc Count" + }, + "health": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Health", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "native_protocol_version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Native Protocol Version", + "type": "string" + }, + "node_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Count" + }, + "partitioner": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Partitioner", + "type": "string" + }, + "shard_primaries": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Shard Primaries" + }, + "shard_total": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Shard Total" + }, + "store_size_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Store Size Bytes" + }, + "uuid": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Uuid", + "type": "string" + } + }, + "required": [ + "cluster_id" + ], + "title": "RuntimeDatastoreCluster", + "type": "object" + }, + "RuntimeDatastoreDataModel": { + "description": "The logical data model of a datastore \u2014 the spine discriminator.\n\nOPEN taxonomy (per the SCN-010 adversarial verdict): carries both\n``unknown`` and ``other`` so an unrecognized model is permissive rather than\nforcing a shallow relational encoding. ``document`` is intentionally absent \u2014\nno observed container realizes it (forbidden-completion discipline).", + "enum": [ + "search_index", + "wide_column", + "key_value", + "relational", + "unknown", + "other" + ], + "title": "RuntimeDatastoreDataModel", + "type": "string" + }, + "RuntimeDatastoreEngine": { + "description": "Observed product family for a datastore service.\n\nOPEN taxonomy: real-world datastore engines are unbounded, so both\n``unknown`` and ``other`` are carried.", + "enum": [ + "opensearch", + "elasticsearch", + "cassandra", + "scylladb", + "redis", + "valkey", + "memcached", + "unknown", + "other" + ], + "title": "RuntimeDatastoreEngine", + "type": "string" + }, + "RuntimeDatastoreEnginePlugin": { + "additionalProperties": false, + "description": "An engine extension/plugin/module installed on a datastore node.\n\nPer-node installed-capability inventory (OpenSearch plugins, Redis modules,\n\u2026). Carries the per-plugin ``version`` the name-only service-level list could\nnot. ``plugin_id`` is a stable symbol; ``name`` is the observed engine name.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "plugin_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "title": "RuntimeDatastoreEnginePlugin", + "type": "object" + }, + "RuntimeDatastoreEvictionPolicy": { + "description": "A key-value eviction policy.\n\nOPEN taxonomy: carries both ``unknown`` and ``other``. Covers Redis\n``maxmemory-policy`` values.", + "enum": [ + "noeviction", + "allkeys_lru", + "allkeys_lfu", + "allkeys_random", + "volatile_lru", + "volatile_lfu", + "volatile_random", + "volatile_ttl", + "unknown", + "other" + ], + "title": "RuntimeDatastoreEvictionPolicy", + "type": "string" + }, + "RuntimeDatastoreMapping": { + "additionalProperties": false, + "description": "A bounded manifest of an observed search-index mapping/schema.\n\nCarries schema geometry and digest/evidence facts, never the raw\nOpenSearch/Elasticsearch ``_mapping`` response body.", + "properties": { + "date_detection": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Date Detection" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "dynamic_policy": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Dynamic Policy", + "type": "string" + }, + "dynamic_template_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dynamic Template Count" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "field_type_census": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Field Type Census", + "type": "object" + }, + "leaf_field_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Leaf Field Count" + }, + "mapping_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "partition_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Partition Ref", + "type": "string" + }, + "schema_digest": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Schema Digest", + "type": "string" + }, + "top_level_field_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Top Level Field Count" + } + }, + "required": [ + "mapping_id" + ], + "title": "RuntimeDatastoreMapping", + "type": "object" + }, + "RuntimeDatastoreNode": { + "additionalProperties": false, + "description": "An observed node participating in a datastore cluster.\n\nBeyond cluster membership and roles, a node carries product-neutral engine\nprovenance (version, build hash/type), JVM/process memory posture (initial\nand maximum heap byte bounds, memory-lock state), a typed per-node engine\nplugin inventory, and typed published endpoints (client vs peer listeners).\nAll are observed runtime facts \u2014 never host policy or software-component\nidentity (ADR-058 amending ADR-048).", + "properties": { + "build_hash": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Build Hash", + "type": "string" + }, + "build_type": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Build Type", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "endpoints": { + "items": { + "$ref": "#/$defs/RuntimeDatastoreNodeEndpoint" + }, + "title": "Endpoints", + "type": "array" + }, + "engine_version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Engine Version", + "type": "string" + }, + "heap_init_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Heap Init Bytes" + }, + "heap_max_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Heap Max Bytes" + }, + "is_coordinator": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Is Coordinator" + }, + "memory_locked": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Memory Locked" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "node_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "plugins": { + "items": { + "$ref": "#/$defs/RuntimeDatastoreEnginePlugin" + }, + "title": "Plugins", + "type": "array" + }, + "roles": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreNodeRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Roles", + "type": "array" + } + }, + "required": [ + "node_id" + ], + "title": "RuntimeDatastoreNode", + "type": "object" + }, + "RuntimeDatastoreNodeEndpoint": { + "additionalProperties": false, + "description": "An observed published listener on a datastore node.\n\nProduct-neutral node listener topology: ``role`` distinguishes the\nparticipant-facing ``client`` listener from the inter-node ``peer`` listener\nwithout encoding engine-native names. ``address`` and ``port`` stay split,\nmatching every other runtime listener surface. A node endpoint records\npublished topology, not proof of an OS bind or host publication (ADR-058).", + "properties": { + "address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "endpoint_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Port" + }, + "protocol": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreNodeEndpointRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Role" + } + }, + "required": [ + "endpoint_id" + ], + "title": "RuntimeDatastoreNodeEndpoint", + "type": "object" + }, + "RuntimeDatastoreNodeEndpointRole": { + "description": "The role a datastore node's published listener fulfils.\n\nOPEN taxonomy: carries both ``unknown`` and ``other``. ``client`` is the\nparticipant/application-facing listener (OpenSearch ``http``, Cassandra\nnative/CQL, Redis client); ``peer`` is the inter-node/cluster listener\n(OpenSearch ``transport``, Cassandra internode, Redis cluster-bus).\nEngine-native listener names are intentionally not modelled \u2014 the datastore\nspine stays product-neutral (ADR-048, ADR-058).", + "enum": [ + "client", + "peer", + "unknown", + "other" + ], + "title": "RuntimeDatastoreNodeEndpointRole", + "type": "string" + }, + "RuntimeDatastoreNodeRole": { + "description": "A role a cluster node fulfils.\n\nOPEN taxonomy: carries both ``unknown`` and ``other``. Covers OpenSearch\n``cluster_manager``/``data``/``ingest``/``ml`` roles and Cassandra\n``coordinator``/``seed`` roles uniformly.", + "enum": [ + "cluster_manager", + "data", + "ingest", + "coordinating", + "ml", + "seed", + "coordinator", + "unknown", + "other" + ], + "title": "RuntimeDatastoreNodeRole", + "type": "string" + }, + "RuntimeDatastorePartition": { + "additionalProperties": false, + "description": "An observed partition primitive (index / keyspace / logical_db / family).\n\nCarries the geometry that differentiates one datastore data model from\nanother: native partition/index UUID, shard/replica and document counts for\na search index, replication strategy + factor + per-DC factor map for a\nwide-column keyspace, and a datatype census for a key-value logical\ndatabase.", + "properties": { + "creation_timestamp": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Creation Timestamp", + "type": "string" + }, + "datatype_census": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Datatype Census", + "type": "object" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "doc_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doc Count" + }, + "doc_count_deleted": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doc Count Deleted" + }, + "durable_writes": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Durable Writes" + }, + "health": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Health", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastorePartitionKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "open_closed_status": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Open Closed Status", + "type": "string" + }, + "partition_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "per_dc_factor_map": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Per Dc Factor Map", + "type": "object" + }, + "replica_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Replica Count" + }, + "replication_factor": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Replication Factor" + }, + "replication_strategy": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreReplicationStrategy" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Replication Strategy" + }, + "shard_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Shard Count" + }, + "store_size_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Store Size Bytes" + }, + "uuid": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Uuid", + "type": "string" + } + }, + "required": [ + "partition_id" + ], + "title": "RuntimeDatastorePartition", + "type": "object" + }, + "RuntimeDatastorePartitionKind": { + "description": "The kind of a datastore partition/namespace primitive.\n\nOPEN taxonomy: carries both ``unknown`` and ``other``. An ``index`` is the\nsearch-cluster primary object, a ``keyspace`` the wide-column one, a\n``logical_db`` a numbered key-value database, and a ``column_family`` a\nwide-column table.", + "enum": [ + "index", + "keyspace", + "logical_db", + "column_family", + "unknown", + "other" + ], + "title": "RuntimeDatastorePartitionKind", + "type": "string" + }, + "RuntimeDatastorePersistence": { + "additionalProperties": false, + "description": "The single observed persistence posture of a key-value datastore.\n\nCaptures Redis-style persistence: RDB save points, AOF on/off, the eviction\npolicy, and the maxmemory ceiling. Its presence is the defining profile a\n``key_value`` data model must carry.", + "properties": { + "aof": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Aof" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "eviction": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreEvictionPolicy" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Eviction" + }, + "maxmemory": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Maxmemory", + "type": "string" + }, + "persistence_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Persistence Id", + "type": "string" + }, + "rdb_save_points": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Rdb Save Points", + "type": "array" + } + }, + "required": [ + "persistence_id" + ], + "title": "RuntimeDatastorePersistence", + "type": "object" + }, + "RuntimeDatastoreReplicationStrategy": { + "description": "A wide-column replication strategy \u2014 the spine discriminator's geometry.\n\nOPEN taxonomy (per the SCN-010 verdict): carries both ``unknown`` and\n``other``. Covers Cassandra ``SimpleStrategy``/``NetworkTopologyStrategy``.", + "enum": [ + "simple_strategy", + "network_topology_strategy", + "unknown", + "other" + ], + "title": "RuntimeDatastoreReplicationStrategy", + "type": "string" + }, + "RuntimeDatastoreService": { + "additionalProperties": false, + "description": "An observed datastore service hosted by a transport service on a node.\n\nThe single non-relational datastore spine. ``service`` references the owning\nsame-node ``Node.services[].name`` (bare name or the qualified\n``nodes..services.`` form). The ``data_model`` discriminator\nselects the required profile the ``require_profile_for_data_model`` guard\nenforces.", + "properties": { + "aliases": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Aliases", + "type": "array" + }, + "authorization_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Authorization Ref", + "type": "string" + }, + "backup_targets": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Backup Targets", + "type": "array" + }, + "cluster": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreCluster" + }, + { + "type": "null" + } + ], + "default": null + }, + "data_model": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreDataModel" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Data Model" + }, + "datastore_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "engine": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreEngine" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Engine" + }, + "ingest_pipelines": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Ingest Pipelines", + "type": "array" + }, + "lifecycle_policies": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Lifecycle Policies", + "type": "array" + }, + "mappings": { + "items": { + "$ref": "#/$defs/RuntimeDatastoreMapping" + }, + "title": "Mappings", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/RuntimeDatastoreNode" + }, + "title": "Nodes", + "type": "array" + }, + "partitions": { + "items": { + "$ref": "#/$defs/RuntimeDatastorePartition" + }, + "title": "Partitions", + "type": "array" + }, + "persistence": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastorePersistence" + }, + { + "type": "null" + } + ], + "default": null + }, + "protocol": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + }, + "pubsub_channels": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Pubsub Channels", + "type": "array" + }, + "queues_streams": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Queues Streams", + "type": "array" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "settings": { + "items": { + "$ref": "#/$defs/RuntimeDatastoreSetting" + }, + "title": "Settings", + "type": "array" + }, + "templates": { + "items": { + "$ref": "#/$defs/RuntimeDatastoreTemplate" + }, + "title": "Templates", + "type": "array" + }, + "transport_security": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreTransportSecurity" + }, + { + "type": "null" + } + ], + "default": null + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "datastore_service_id" + ], + "title": "RuntimeDatastoreService", + "type": "object" + }, + "RuntimeDatastoreSetting": { + "additionalProperties": false, + "description": "An observed datastore runtime setting with scope, provenance, and class.\n\nExplicit ``redacted`` / ``operator_secret`` classifications omit raw values;\nnames that look credential-bearing remain scenario content unless the\nauthor marks the value withheld.", + "properties": { + "classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Classification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreSettingProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "scope": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreSettingScope" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "engine", + "title": "Scope" + }, + "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + } + }, + "required": [ + "setting_id" + ], + "title": "RuntimeDatastoreSetting", + "type": "object" + }, + "RuntimeDatastoreSettingProvenance": { + "description": "Where an observed datastore runtime setting value came from.\n\nOPEN taxonomy: provenance sources are extensible, so both ``unknown`` and\n``other`` are carried (mirrors ``DatabaseSettingProvenance``).", + "enum": [ + "introspection", + "configuration_file", + "image_default", + "operator_override", + "runtime_default", + "unknown", + "other" + ], + "title": "RuntimeDatastoreSettingProvenance", + "type": "string" + }, + "RuntimeDatastoreSettingScope": { + "description": "The scope a datastore runtime setting applies at.\n\nCLOSED structural vocab: the scope lattice (cluster / node / partition /\nengine) is fixed by the spine's own topology, so no ``unknown`` / ``other``.", + "enum": [ + "cluster", + "node", + "partition", + "engine" + ], + "title": "RuntimeDatastoreSettingScope", + "type": "string" + }, + "RuntimeDatastoreTemplate": { + "additionalProperties": false, + "description": "A bounded manifest of an observed index template body.\n\nThe template captures patterns, selected settings, optional mapping linkage,\ndigest, and evidence refs without embedding the backend's raw template JSON.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "index_patterns": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Index Patterns", + "type": "array" + }, + "mapping_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mapping Ref", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "settings_summary": { + "additionalProperties": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "boolean" + } + ] + }, + "title": "Settings Summary", + "type": "object" + }, + "template_digest": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Template Digest", + "type": "string" + }, + "template_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "template_id" + ], + "title": "RuntimeDatastoreTemplate", + "type": "object" + }, + "RuntimeDatastoreTransportSecurity": { + "additionalProperties": false, + "description": "The single observed transport-security posture of a datastore service.\n\nRecords the intra-cluster transport TLS mode and whether client/node\ncertificate verification is enforced \u2014 the observable ``xpack.security`` /\n``SKIPSSL_VERIFY`` posture, never the certificate material itself.", + "properties": { + "client_verification": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Client Verification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "mode": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeDatastoreTransportSecurityMode" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "none", + "title": "Mode" + }, + "node_verification": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Verification" + }, + "transport_security_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Transport Security Id", + "type": "string" + } + }, + "required": [ + "transport_security_id" + ], + "title": "RuntimeDatastoreTransportSecurity", + "type": "object" + }, + "RuntimeDatastoreTransportSecurityMode": { + "description": "The transport-security posture of a datastore service.\n\nCLOSED structural vocab: the observable TLS posture is a fixed three-way\ndistinction (none / one-way TLS / mutual TLS), so no ``unknown`` / ``other``.\nA ``${var}`` placeholder remains expressible at the field level.", + "enum": [ + "none", + "tls", + "mutual_tls" + ], + "title": "RuntimeDatastoreTransportSecurityMode", + "type": "string" + }, + "RuntimeDependencyManifest": { + "additionalProperties": false, + "description": "A dependency manifest visible in the realized runtime artifact.", + "properties": { + "ecosystem": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ecosystem", + "type": "string" + }, + "format": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Format", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "ecosystem", + "path" + ], + "title": "RuntimeDependencyManifest", + "type": "object" + }, + "RuntimeDeviceMapping": { + "additionalProperties": false, + "description": "A host device mapping observed in runtime configuration.", + "properties": { + "container_path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Container Path", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "host_path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Host Path", + "type": "string" + }, + "permissions": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Permissions", + "type": "string" + } + }, + "required": [ + "host_path", + "container_path" + ], + "title": "RuntimeDeviceMapping", + "type": "object" + }, + "RuntimeDnsService": { + "additionalProperties": false, + "description": "An observed DNS service hosted by a node.", + "properties": { + "configuration_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Configuration File Refs", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "dns_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "dynamic_update": { + "anyOf": [ + { + "$ref": "#/$defs/DnsDynamicUpdatePolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "implementation": { + "anyOf": [ + { + "$ref": "#/$defs/DnsServerImplementation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Implementation" + }, + "log_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Log File Refs", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "resolver_policy": { + "anyOf": [ + { + "$ref": "#/$defs/DnsResolverPolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "roles": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/DnsServiceRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Roles", + "type": "array" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "settings": { + "items": { + "$ref": "#/$defs/DnsRuntimeSetting" + }, + "title": "Settings", + "type": "array" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + }, + "zones": { + "items": { + "$ref": "#/$defs/DnsZone" + }, + "title": "Zones", + "type": "array" + } + }, + "required": [ + "dns_service_id" + ], + "title": "RuntimeDnsService", + "type": "object" + }, + "RuntimeEnvironmentValueClassification": { + "description": "Sensitivity classification for an observed runtime environment value.", + "enum": [ + "plain", + "redacted", + "secret_fixture", + "operator_secret", + "unknown", + "other" + ], + "title": "RuntimeEnvironmentValueClassification", + "type": "string" + }, + "RuntimeEnvironmentVariable": { + "additionalProperties": false, + "description": "Observed runtime environment variable with provenance and sensitivity.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeEnvironmentVariableProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "source": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeEnvironmentValueClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + } + }, + "required": [ + "name" + ], + "title": "RuntimeEnvironmentVariable", + "type": "object" + }, + "RuntimeEnvironmentVariableProvenance": { + "description": "Origin class for an observed runtime environment variable.", + "enum": [ + "compose", + "image", + "operator", + "container", + "runtime", + "other", + "unknown" + ], + "title": "RuntimeEnvironmentVariableProvenance", + "type": "string" + }, + "RuntimeExtraHost": { + "additionalProperties": false, + "description": "An observed extra host mapping in runtime configuration.", + "properties": { + "address": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "hostname": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Hostname", + "type": "string" + } + }, + "required": [ + "hostname", + "address" + ], + "title": "RuntimeExtraHost", + "type": "object" + }, + "RuntimeFileService": { + "additionalProperties": false, + "description": "A node-scoped runtime file-service inventory.\n\n``service`` references the owning same-node ``Node.services[].name``\n(bare name or qualified ``nodes..services.``).", + "properties": { + "access_observations": { + "items": { + "$ref": "#/$defs/RuntimeFileServiceAccessObservation" + }, + "title": "Access Observations", + "type": "array" + }, + "access_rules": { + "items": { + "$ref": "#/$defs/RuntimeFileServiceAccessRule" + }, + "title": "Access Rules", + "type": "array" + }, + "backend": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Backend", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "file_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "principals": { + "items": { + "$ref": "#/$defs/RuntimeFileServicePrincipal" + }, + "title": "Principals", + "type": "array" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Protocol" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "shares": { + "items": { + "$ref": "#/$defs/RuntimeFileServiceShare" + }, + "title": "Shares", + "type": "array" + } + }, + "required": [ + "file_service_id" + ], + "title": "RuntimeFileService", + "type": "object" + }, + "RuntimeFileServiceAccessAction": { + "description": "Portable action vocabulary for file-service access policy/observations.", + "enum": [ + "browse", + "list", + "read", + "write", + "create", + "delete", + "execute", + "administer", + "other", + "unknown" + ], + "title": "RuntimeFileServiceAccessAction", + "type": "string" + }, + "RuntimeFileServiceAccessBasis": { + "description": "Basis/provenance for an access claim or observation.", + "enum": [ + "share_config", + "passdb", + "filesystem_acl", + "directory_policy", + "observed_probe", + "computed", + "unknown", + "other" + ], + "title": "RuntimeFileServiceAccessBasis", + "type": "string" + }, + "RuntimeFileServiceAccessEffect": { + "description": "Portable effect for an authored or computed file-service access rule.", + "enum": [ + "allow", + "deny", + "not_applicable", + "unknown", + "other" + ], + "title": "RuntimeFileServiceAccessEffect", + "type": "string" + }, + "RuntimeFileServiceAccessObservation": { + "additionalProperties": false, + "description": "An observed probe outcome against a file-service resource.\n\nObservations are evidence; they support but do not overwrite authored\nor configured policy.", + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceAccessAction" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Action" + }, + "basis": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceAccessBasis" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "observed_probe", + "title": "Basis" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "observation_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "outcome": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceAccessOutcome" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Outcome" + }, + "resource_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Resource Ref", + "type": "string" + }, + "sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Sensitivity" + }, + "subject_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Subject Ref", + "type": "string" + } + }, + "required": [ + "observation_id", + "subject_ref", + "resource_ref" + ], + "title": "RuntimeFileServiceAccessObservation", + "type": "object" + }, + "RuntimeFileServiceAccessOutcome": { + "description": "Observed outcome class for a per-share access probe.", + "enum": [ + "allowed", + "denied", + "error", + "not_observed", + "unknown", + "other" + ], + "title": "RuntimeFileServiceAccessOutcome", + "type": "string" + }, + "RuntimeFileServiceAccessRule": { + "additionalProperties": false, + "description": "An authored or computed access rule for a file-service resource.", + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceAccessAction" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Action" + }, + "basis": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceAccessBasis" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Basis" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "effect": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceAccessEffect" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Effect" + }, + "resource_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Resource Ref", + "type": "string" + }, + "rule_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "subject_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Subject Ref", + "type": "string" + } + }, + "required": [ + "rule_id", + "subject_ref", + "resource_ref" + ], + "title": "RuntimeFileServiceAccessRule", + "type": "object" + }, + "RuntimeFileServiceCredentialClassification": { + "description": "Semantic classification of a service-local credential.\n\nThe raw credential value MUST NOT be recorded; the principal model\nhas no field that can hold one. This vocabulary is the only credential\nrepresentation in the file-service surface \u2014 it describes the\nstrength/posture of the credential as observed (or its absence) so\ndownstream consumers can reason about exposure without leaking secret\nmaterial into fixtures, schemas, diagnostics, or logs.", + "enum": [ + "no_credential", + "weak", + "default_or_trivial", + "strong", + "redacted", + "unknown", + "other" + ], + "title": "RuntimeFileServiceCredentialClassification", + "type": "string" + }, + "RuntimeFileServicePrincipal": { + "additionalProperties": false, + "description": "A service-local principal (Samba passdb-style account or group).\n\nService-local principals are NOT promoted to top-level ``accounts``,\n``runtime.local_identity``, or ``runtime.identity_authorities``.\nOptional ``local_user_ref`` and ``directory_subject_ref`` carry refs to\nthose surfaces when a mapping is observed.\n\nRaw credential material (passwords, NT/LM hashes, Kerberos keys, private\nkeys, bearer tokens, captured credentials) is unrepresentable on this\nrecord per ADR-037 \u00a74 and the secret-handling gate; only\n``credential_classification`` is carried, so the field surface itself\ncannot smuggle a secret into models, schemas, fixtures, diagnostics, or\nsnapshots.", + "properties": { + "credential_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServiceCredentialClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Credential Classification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "directory_subject_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Directory Subject Ref", + "type": "string" + }, + "external_id": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "External Id", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServicePrincipalKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "local_user_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Local User Ref", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServicePrincipalOrigin" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Origin" + }, + "principal_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileServicePrincipalStatus" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Status" + } + }, + "required": [ + "principal_id", + "name" + ], + "title": "RuntimeFileServicePrincipal", + "type": "object" + }, + "RuntimeFileServicePrincipalKind": { + "description": "Portable kind of a service-local principal in a file service.", + "enum": [ + "user", + "group", + "machine", + "alias", + "guest", + "anonymous", + "service_account", + "other", + "unknown" + ], + "title": "RuntimeFileServicePrincipalKind", + "type": "string" + }, + "RuntimeFileServicePrincipalOrigin": { + "description": "Origin/provenance class for a service-local principal record.", + "enum": [ + "built_in", + "provisioned", + "synchronized", + "runtime_created", + "operator", + "unknown", + "other" + ], + "title": "RuntimeFileServicePrincipalOrigin", + "type": "string" + }, + "RuntimeFileServicePrincipalStatus": { + "description": "Observed account status for a service-local principal.", + "enum": [ + "enabled", + "disabled", + "locked", + "no_login", + "unknown", + "other" + ], + "title": "RuntimeFileServicePrincipalStatus", + "type": "string" + }, + "RuntimeFileServiceProtocol": { + "description": "Portable protocol family for an observed runtime file service.", + "enum": [ + "smb", + "cifs", + "nfs", + "afp", + "ftp", + "sftp", + "webdav", + "object_store", + "other", + "unknown" + ], + "title": "RuntimeFileServiceProtocol", + "type": "string" + }, + "RuntimeFileServiceShare": { + "additionalProperties": false, + "description": "A published share/resource on a runtime file service.", + "properties": { + "backing_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Backing Path", + "type": "string" + }, + "browseable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Browseable" + }, + "comment": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Comment", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "guest_ok": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Guest Ok" + }, + "invalid_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Invalid Users", + "type": "array" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFileShareKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "disk", + "title": "Kind" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "read_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Read Only" + }, + "share_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "valid_groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Valid Groups", + "type": "array" + }, + "valid_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Valid Users", + "type": "array" + }, + "write_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Write Users", + "type": "array" + } + }, + "required": [ + "share_id", + "name" + ], + "title": "RuntimeFileServiceShare", + "type": "object" + }, + "RuntimeFileShareKind": { + "description": "Observed kind of a published file-service share/resource.", + "enum": [ + "disk", + "ipc", + "printer", + "other", + "unknown" + ], + "title": "RuntimeFileShareKind", + "type": "string" + }, + "RuntimeFilesystemEntry": { + "additionalProperties": false, + "description": "A filesystem entry observed inside a runtime node or container asset.", + "properties": { + "content_digest": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Content Digest", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "digest_algorithm": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Digest Algorithm", + "type": "string" + }, + "entry_type": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFilesystemEntryType" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Entry Type" + }, + "gid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Gid" + }, + "mode": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mode", + "type": "string" + }, + "owner_group": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Owner Group", + "type": "string" + }, + "owner_user": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Owner User", + "type": "string" + }, + "path": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "presence": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFilesystemPresence" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "present", + "title": "Presence" + }, + "provenance": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Provenance", + "type": "string" + }, + "sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Sensitivity" + }, + "size": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Size" + }, + "source_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source Path", + "type": "string" + }, + "stability": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFilesystemStability" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Stability" + }, + "uid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Uid" + } + }, + "required": [ + "path" + ], + "title": "RuntimeFilesystemEntry", + "type": "object" + }, + "RuntimeFilesystemEntryType": { + "description": "Observed type of a runtime filesystem inventory entry.", + "enum": [ + "file", + "directory", + "symlink", + "device", + "socket", + "fifo", + "other", + "unknown" + ], + "title": "RuntimeFilesystemEntryType", + "type": "string" + }, + "RuntimeFilesystemPresence": { + "description": "Observed presence state for a runtime filesystem inventory entry.\n\n``present`` is the default and preserves prior behavior. ``expected_absent``\nrecords an authored or expected path that was not observed at capture time \u2014\nfor example a deploy-key path attempted by setup code but missing in the\ncaptured steady state. Absent entries retain their expected ``entry_type``\ninstead of collapsing to ``other``; present-only facts (size, content\ndigest, owner ids, mode) must be omitted, per ADR-037.", + "enum": [ + "present", + "expected_absent", + "unknown", + "other" + ], + "title": "RuntimeFilesystemPresence", + "type": "string" + }, + "RuntimeFilesystemStability": { + "description": "Stability class for an observed runtime filesystem or mount fact.", + "enum": [ + "stable", + "generated", + "log", + "cache", + "runtime_created", + "volume_backed", + "transient", + "unknown", + "other" + ], + "title": "RuntimeFilesystemStability", + "type": "string" + }, + "RuntimeForwardingAgent": { + "additionalProperties": false, + "description": "Node-scoped runtime inventory for a forwarding / intel-sync agent.\n\nThe single forwarder spine. The ``agent_kind`` discriminator selects the\nrequired profile the ``require_profile_for_agent_kind`` guard enforces.\nCadence composes a ``runtime.scheduled_jobs`` entry; the inter-node trust\nedge composes a ``RelationshipForwardingEdge`` \u2014 neither is re-typed here.", + "properties": { + "agent_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgentKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Agent Kind" + }, + "buffer_policy": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingBufferPolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "forwarding_agent_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Forwarding Agent Id", + "type": "string" + }, + "implementation": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgentImplementation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Implementation" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "reload_channels": { + "items": { + "$ref": "#/$defs/RuntimeForwardingReloadChannel" + }, + "title": "Reload Channels", + "type": "array" + }, + "settings": { + "items": { + "$ref": "#/$defs/RuntimeForwardingSetting" + }, + "title": "Settings", + "type": "array" + }, + "ship_targets": { + "items": { + "$ref": "#/$defs/RuntimeForwardingShipTarget" + }, + "title": "Ship Targets", + "type": "array" + }, + "sources": { + "items": { + "$ref": "#/$defs/RuntimeForwardingSource" + }, + "title": "Sources", + "type": "array" + }, + "transforms": { + "items": { + "$ref": "#/$defs/RuntimeForwardingTransform" + }, + "title": "Transforms", + "type": "array" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "forwarding_agent_id" + ], + "title": "RuntimeForwardingAgent", + "type": "object" + }, + "RuntimeForwardingAgentImplementation": { + "description": "Observed product family for a forwarding / intel-sync agent (OPEN).", + "enum": [ + "filebeat", + "wazuh_agent", + "fluent_bit", + "fluentd", + "logstash", + "vector", + "rsyslog", + "nxlog", + "otel_collector", + "misp_suricata_sync", + "unknown", + "other" + ], + "title": "RuntimeForwardingAgentImplementation", + "type": "string" + }, + "RuntimeForwardingAgentKind": { + "description": "Portable forwarding-agent role discriminator (OPEN per the verdict).", + "enum": [ + "log_forwarder", + "content_sync", + "unknown", + "other" + ], + "title": "RuntimeForwardingAgentKind", + "type": "string" + }, + "RuntimeForwardingBufferCrypto": { + "description": "Portable at-rest / in-transit crypto of a buffer policy (OPEN).", + "enum": [ + "none", + "aes", + "tls", + "unknown", + "other" + ], + "title": "RuntimeForwardingBufferCrypto", + "type": "string" + }, + "RuntimeForwardingBufferPolicy": { + "additionalProperties": false, + "description": "The single observed buffer / back-pressure posture of a forwarder.\n\nCaptures the ``client_buffer`` shape: queue capacity, events-per-second\nceiling, at-rest/in-transit crypto, and reconnect interval. Its presence is\nthe defining profile a ``log_forwarder`` must carry.", + "properties": { + "buffer_policy_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Buffer Policy Id", + "type": "string" + }, + "crypto": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingBufferCrypto" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Crypto" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "eps": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Eps" + }, + "queue_capacity": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Queue Capacity" + }, + "reconnect_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconnect Seconds" + } + }, + "required": [ + "buffer_policy_id" + ], + "title": "RuntimeForwardingBufferPolicy", + "type": "object" + }, + "RuntimeForwardingEnrollmentClassification": { + "description": "Redaction class for a ship-target enrollment identity (CLOSED).\n\nA fixed structural lattice \u2014 an enrollment credential is either absent\n(``none``) or present-but-not-recorded (``redacted`` / ``operator_secret``).\nRaw enrollment-key material is never carried; per the enum-sentinel\ndiscipline this closed set carries no ``unknown`` / ``other`` tail.", + "enum": [ + "none", + "redacted", + "operator_secret" + ], + "title": "RuntimeForwardingEnrollmentClassification", + "type": "string" + }, + "RuntimeForwardingParseFormat": { + "description": "Portable on-the-wire / on-disk parse format of a source (OPEN).", + "enum": [ + "json", + "syslog", + "eve_json", + "cef", + "leef", + "csv", + "text", + "stix", + "misp_json", + "unknown", + "other" + ], + "title": "RuntimeForwardingParseFormat", + "type": "string" + }, + "RuntimeForwardingProtocol": { + "description": "Portable wire protocol of a ship target (OPEN).", + "enum": [ + "syslog", + "syslog_tls", + "syslog_tcp", + "beats", + "http", + "https", + "tcp", + "udp", + "kafka", + "unknown", + "other" + ], + "title": "RuntimeForwardingProtocol", + "type": "string" + }, + "RuntimeForwardingReloadChannel": { + "additionalProperties": false, + "description": "An observed downstream reload channel a content-sync agent drives.\n\nReferences the downstream consumer's control channel (e.g. the Suricata\nrule-reload socket) rather than inventing a parallel socket model; the\ninter-node trust edge is carried by ``RelationshipForwardingEdge``.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingReloadChannelKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "reload_channel_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "target_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Ref", + "type": "string" + } + }, + "required": [ + "reload_channel_id" + ], + "title": "RuntimeForwardingReloadChannel", + "type": "object" + }, + "RuntimeForwardingReloadChannelKind": { + "description": "Portable control / reload channel family for a downstream consumer (OPEN).", + "enum": [ + "unix_socket", + "signal", + "http_api", + "cli", + "file_drop", + "unknown", + "other" + ], + "title": "RuntimeForwardingReloadChannelKind", + "type": "string" + }, + "RuntimeForwardingSetting": { + "additionalProperties": false, + "description": "An observed forwarding-agent runtime setting with provenance and class.\n\nExplicit ``redacted`` / ``operator_secret`` classifications omit raw values;\ncredential-shaped names remain scenario content unless the author marks the\nvalue withheld.", + "properties": { + "classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingSettingClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "plain", + "title": "Classification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingSettingProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + } + }, + "required": [ + "setting_id" + ], + "title": "RuntimeForwardingSetting", + "type": "object" + }, + "RuntimeForwardingSettingClassification": { + "description": "Redaction class for an observed forwarding setting value (CLOSED).\n\nThe fixed structural redaction lattice for a setting: a plain value, a\npresent-but-not-recorded secret, or an operator-only secret. Carries no\nopen tail by the enum-sentinel discipline.", + "enum": [ + "plain", + "redacted", + "operator_secret" + ], + "title": "RuntimeForwardingSettingClassification", + "type": "string" + }, + "RuntimeForwardingSettingProvenance": { + "description": "Portable provenance of an observed forwarding setting (OPEN).", + "enum": [ + "configuration_file", + "environment", + "command_line", + "default", + "unknown", + "other" + ], + "title": "RuntimeForwardingSettingProvenance", + "type": "string" + }, + "RuntimeForwardingShipTarget": { + "additionalProperties": false, + "description": "An observed downstream ship target (event ingest and/or enrollment).\n\n``ingestion_port`` is the event-ingest endpoint (e.g. ``wazuh.manager:1514``);\n``enrollment_port`` is the enrollment endpoint (e.g. ``:1515``). The\nenrollment identity itself is never recorded \u2014 only the closed\n``enrollment_identity_classification`` lattice (``none`` / ``redacted`` /\n``operator_secret``).", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enrollment_identity_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingEnrollmentClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "none", + "title": "Enrollment Identity Classification" + }, + "enrollment_port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enrollment Port" + }, + "ingestion_port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ingestion Port" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Protocol" + }, + "target_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "target_node_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Node Ref", + "type": "string" + }, + "target_service_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Service Ref", + "type": "string" + } + }, + "required": [ + "target_id" + ], + "title": "RuntimeForwardingShipTarget", + "type": "object" + }, + "RuntimeForwardingSource": { + "additionalProperties": false, + "description": "An observed forwarder input source (tailed path, API pull, or queue).", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingSourceKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "location": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Location", + "type": "string" + }, + "parse_format": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingParseFormat" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Parse Format" + }, + "selector": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Selector", + "type": "string" + }, + "source_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "source_id" + ], + "title": "RuntimeForwardingSource", + "type": "object" + }, + "RuntimeForwardingSourceKind": { + "description": "Portable provenance family for a forwarder input source (OPEN).", + "enum": [ + "tailed_path", + "api_pull", + "queue", + "unknown", + "other" + ], + "title": "RuntimeForwardingSourceKind", + "type": "string" + }, + "RuntimeForwardingTransform": { + "additionalProperties": false, + "description": "An observed transform applied between a source and a ship target.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingTransformKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "sid_namespace": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Sid Namespace", + "type": "string" + }, + "transform_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "transform_id" + ], + "title": "RuntimeForwardingTransform", + "type": "object" + }, + "RuntimeForwardingTransformKind": { + "description": "Portable transform family applied between source and ship target (OPEN).", + "enum": [ + "passthrough", + "parse", + "ioc_to_rule", + "unknown", + "other" + ], + "title": "RuntimeForwardingTransformKind", + "type": "string" + }, + "RuntimeHealthObservation": { + "additionalProperties": false, + "description": "Observed runtime health status and healthcheck log facts.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "failing_streak": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Failing Streak" + }, + "log": { + "items": { + "$ref": "#/$defs/RuntimeHealthcheckLog" + }, + "title": "Log", + "type": "array" + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeHealthStatus" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Status" + } + }, + "title": "RuntimeHealthObservation", + "type": "object" + }, + "RuntimeHealthStatus": { + "description": "Observed health status for a runtime node or container.", + "enum": [ + "none", + "starting", + "healthy", + "unhealthy", + "unknown", + "other" + ], + "title": "RuntimeHealthStatus", + "type": "string" + }, + "RuntimeHealthcheckLog": { + "additionalProperties": false, + "description": "An observed runtime healthcheck log entry.", + "properties": { + "end": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "End", + "type": "string" + }, + "exit_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Exit Code" + }, + "output": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Output", + "type": "string" + }, + "output_redacted": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Output Redacted" + }, + "start": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Start", + "type": "string" + } + }, + "title": "RuntimeHealthcheckLog", + "type": "object" + }, + "RuntimeIdentityAttribute": { + "additionalProperties": false, + "description": "Observed identity attribute or bounded setting.\n\nSecret-bearing names must not carry raw values. This keeps directory\npasswords, Kerberos keys, keytabs, tokens, and client secrets out of\nfixtures, diagnostics, schemas, and generated runtime artifacts.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityRecordOrigin" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Origin" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityRecordOrigin" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + }, + "values": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "RuntimeIdentityAttribute", + "type": "object" + }, + "RuntimeIdentityAuthority": { + "additionalProperties": false, + "description": "A node-scoped identity authority inventory.", + "properties": { + "base_dn": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Base Dn", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "domain_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Domain Name", + "type": "string" + }, + "identity_authority_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "issuer": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Issuer", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityAuthorityKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "namespace": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Namespace", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/RuntimeIdentityPolicy" + }, + "title": "Policies", + "type": "array" + }, + "realm": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Realm", + "type": "string" + }, + "relationships": { + "items": { + "$ref": "#/$defs/RuntimeIdentityRelationship" + }, + "title": "Relationships", + "type": "array" + }, + "services": { + "items": { + "$ref": "#/$defs/RuntimeIdentityAuthorityService" + }, + "title": "Services", + "type": "array" + }, + "subjects": { + "items": { + "$ref": "#/$defs/RuntimeIdentitySubject" + }, + "title": "Subjects", + "type": "array" + }, + "tenant_id": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Tenant Id", + "type": "string" + } + }, + "required": [ + "identity_authority_id" + ], + "title": "RuntimeIdentityAuthority", + "type": "object" + }, + "RuntimeIdentityAuthorityKind": { + "description": "Portable kind of identity authority represented by the inventory.", + "enum": [ + "directory", + "domain", + "kerberos_realm", + "identity_provider", + "cloud_iam", + "authorization_system", + "other", + "unknown" + ], + "title": "RuntimeIdentityAuthorityKind", + "type": "string" + }, + "RuntimeIdentityAuthorityProtocol": { + "description": "Protocol or API family exposed by an identity authority service.", + "enum": [ + "ldap", + "ldaps", + "kerberos", + "saml", + "oidc", + "oauth2", + "scim", + "ad_ds_rpc", + "graph_api", + "other", + "unknown" + ], + "title": "RuntimeIdentityAuthorityProtocol", + "type": "string" + }, + "RuntimeIdentityAuthorityService": { + "additionalProperties": false, + "description": "A protocol/API endpoint associated with an identity authority.", + "properties": { + "address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Port" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityAuthorityProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Protocol" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "service_id" + ], + "title": "RuntimeIdentityAuthorityService", + "type": "object" + }, + "RuntimeIdentityPolicy": { + "additionalProperties": false, + "description": "Observed identity authority policy or bounded policy setting group.", + "properties": { + "applies_to_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Applies To Refs", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "policy_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "policy_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityPolicyKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Policy Kind" + }, + "settings": { + "items": { + "$ref": "#/$defs/RuntimeIdentityAttribute" + }, + "title": "Settings", + "type": "array" + } + }, + "required": [ + "policy_id" + ], + "title": "RuntimeIdentityPolicy", + "type": "object" + }, + "RuntimeIdentityPolicyKind": { + "description": "Portable policy family attached to an identity authority.", + "enum": [ + "password", + "lockout", + "kerberos", + "access", + "conditional_access", + "mfa", + "group_policy", + "trust", + "other", + "unknown" + ], + "title": "RuntimeIdentityPolicyKind", + "type": "string" + }, + "RuntimeIdentityProvenance": { + "description": "Origin class for an observed local identity record.", + "enum": [ + "image", + "package", + "runtime_created", + "operator", + "unknown", + "other" + ], + "title": "RuntimeIdentityProvenance", + "type": "string" + }, + "RuntimeIdentityRecordOrigin": { + "description": "Origin class for an observed identity authority record.", + "enum": [ + "built_in", + "directory", + "synchronized", + "federated", + "provisioned", + "runtime_created", + "operator", + "unknown", + "other" + ], + "title": "RuntimeIdentityRecordOrigin", + "type": "string" + }, + "RuntimeIdentityRelationship": { + "additionalProperties": false, + "description": "Observed membership, trust, federation, or delegation relationship.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "external_target": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "External Target", + "type": "string" + }, + "relationship_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "relationship_type": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityRelationshipKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Relationship Type" + }, + "source_ref": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source Ref", + "type": "string" + }, + "target_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Ref", + "type": "string" + } + }, + "required": [ + "relationship_id", + "source_ref" + ], + "title": "RuntimeIdentityRelationship", + "type": "object" + }, + "RuntimeIdentityRelationshipKind": { + "description": "Portable relationship kind between authority-local identity objects.", + "enum": [ + "member_of", + "has_member", + "trusts", + "federates_with", + "manages", + "owns", + "delegated_admin", + "syncs_from", + "associated", + "other", + "unknown" + ], + "title": "RuntimeIdentityRelationshipKind", + "type": "string" + }, + "RuntimeIdentitySubject": { + "additionalProperties": false, + "description": "An authority-local user, group, device, role, or service principal.", + "properties": { + "attributes": { + "items": { + "$ref": "#/$defs/RuntimeIdentityAttribute" + }, + "title": "Attributes", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "display_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Display Name", + "type": "string" + }, + "distinguished_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Distinguished Name", + "type": "string" + }, + "domain": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Domain", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentitySubjectKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityRecordOrigin" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Origin" + }, + "principal_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Principal Name", + "type": "string" + }, + "service_principal_names": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Service Principal Names", + "type": "array" + }, + "subject_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "subject_id", + "name" + ], + "title": "RuntimeIdentitySubject", + "type": "object" + }, + "RuntimeIdentitySubjectKind": { + "description": "Portable subject/principal kind within an identity authority.", + "enum": [ + "user", + "group", + "computer", + "device", + "service_account", + "service_principal", + "role", + "application", + "organizational_unit", + "other", + "unknown" + ], + "title": "RuntimeIdentitySubjectKind", + "type": "string" + }, + "RuntimeInitProcess": { + "additionalProperties": false, + "description": "Observed backend-injected container init / PID-1 reaper configuration.\n\nModels authored container runtime intent \u2014 for example Docker Compose\n``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is\ndistinct from the observed PID-1 process recorded in\n``runtime.processes``; see ADR-027.", + "properties": { + "argv": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Argv", + "type": "array" + }, + "argv_redacted": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Argv Redacted" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "executable_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Executable Path", + "type": "string" + }, + "implementation": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Implementation", + "type": "string" + }, + "reaps_children": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reaps Children" + } + }, + "title": "RuntimeInitProcess", + "type": "object" + }, + "RuntimeListenerAddressFamily": { + "description": "Address family observed for a runtime listener.", + "enum": [ + "ipv4", + "ipv6", + "unix", + "unspecified", + "other", + "unknown" + ], + "title": "RuntimeListenerAddressFamily", + "type": "string" + }, + "RuntimeListenerProtocol": { + "description": "Transport or IPC family for an observed listener.", + "enum": [ + "tcp", + "udp", + "sctp", + "unix", + "other", + "unknown" + ], + "title": "RuntimeListenerProtocol", + "type": "string" + }, + "RuntimeListenerProvenance": { + "description": "Source class for an observed runtime listener fact.", + "enum": [ + "osquery", + "ss", + "netstat", + "lsof", + "nmap", + "docker_inspect", + "kubernetes", + "systemd", + "operator", + "scanner", + "unknown", + "other" + ], + "title": "RuntimeListenerProvenance", + "type": "string" + }, + "RuntimeListenerReadiness": { + "additionalProperties": false, + "description": "Optional readiness/probe evidence for an observed listener.", + "properties": { + "criteria": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Criteria", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "probe": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Probe", + "type": "string" + } + }, + "title": "RuntimeListenerReadiness", + "type": "object" + }, + "RuntimeListenerScope": { + "description": "Reachability class of the bind endpoint in the node namespace.", + "enum": [ + "wildcard", + "loopback_only", + "network_facing", + "node_local", + "local_socket", + "unknown", + "other" + ], + "title": "RuntimeListenerScope", + "type": "string" + }, + "RuntimeLocalGroup": { + "additionalProperties": false, + "description": "A local group record observed in a node's identity database (/etc/group).", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "gid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Gid" + }, + "members": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Members", + "type": "array" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + } + }, + "required": [ + "name" + ], + "title": "RuntimeLocalGroup", + "type": "object" + }, + "RuntimeLocalIdentityInventory": { + "additionalProperties": false, + "description": "Observed local identity database (users, groups, sudo rules) for a node.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/RuntimeLocalGroup" + }, + "title": "Groups", + "type": "array" + }, + "sudo_rules": { + "items": { + "$ref": "#/$defs/RuntimeSudoRule" + }, + "title": "Sudo Rules", + "type": "array" + }, + "users": { + "items": { + "$ref": "#/$defs/RuntimeLocalUser" + }, + "title": "Users", + "type": "array" + } + }, + "title": "RuntimeLocalIdentityInventory", + "type": "object" + }, + "RuntimeLocalUser": { + "additionalProperties": false, + "description": "A local user record observed in a node's identity database (/etc/passwd).", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Disabled" + }, + "gecos": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Gecos", + "type": "string" + }, + "home": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Home", + "type": "string" + }, + "locked": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Locked" + }, + "no_login": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "No Login" + }, + "primary_gid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Primary Gid" + }, + "primary_group": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Primary Group", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeIdentityProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "shell": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Shell", + "type": "string" + }, + "stability": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFilesystemStability" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Stability" + }, + "supplemental_groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Supplemental Groups", + "type": "array" + }, + "uid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Uid" + }, + "username": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Username", + "type": "string" + } + }, + "required": [ + "username" + ], + "title": "RuntimeLocalUser", + "type": "object" + }, + "RuntimeMailAlias": { + "additionalProperties": false, + "description": "A service-local mailbox alias or forwarding address.", + "properties": { + "address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "alias_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "domain_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Domain Ref", + "type": "string" + }, + "external_targets": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "External Targets", + "type": "array" + }, + "target_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Target Refs", + "type": "array" + } + }, + "required": [ + "alias_id" + ], + "title": "RuntimeMailAlias", + "type": "object" + }, + "RuntimeMailAuthMechanism": { + "description": "Portable mail authentication mechanism vocabulary.", + "enum": [ + "none", + "plain", + "login", + "cram_md5", + "digest_md5", + "scram_sha_1", + "scram_sha_256", + "oauthbearer", + "external", + "anonymous", + "other", + "unknown" + ], + "title": "RuntimeMailAuthMechanism", + "type": "string" + }, + "RuntimeMailComponent": { + "additionalProperties": false, + "description": "A mail-service engine/component such as Postfix, Dovecot, or a filter.", + "properties": { + "component_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailComponentKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "component_id", + "name" + ], + "title": "RuntimeMailComponent", + "type": "object" + }, + "RuntimeMailComponentKind": { + "description": "Portable component kind inside a runtime mail service.", + "enum": [ + "mta", + "mda", + "imap_server", + "pop3_server", + "submission_agent", + "mailbox_store", + "spam_filter", + "antivirus", + "queue_manager", + "relay", + "policy_service", + "other", + "unknown" + ], + "title": "RuntimeMailComponentKind", + "type": "string" + }, + "RuntimeMailCredentialClassification": { + "description": "Semantic classification of a mailbox credential, never its raw value.", + "enum": [ + "no_credential", + "weak", + "default_or_trivial", + "fixture", + "strong", + "redacted", + "unknown", + "other" + ], + "title": "RuntimeMailCredentialClassification", + "type": "string" + }, + "RuntimeMailDomain": { + "additionalProperties": false, + "description": "A domain known to the runtime mail service.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "domain_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailDomainRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Role" + } + }, + "required": [ + "domain_id", + "name" + ], + "title": "RuntimeMailDomain", + "type": "object" + }, + "RuntimeMailDomainRole": { + "description": "Role of a domain known to the mail service.", + "enum": [ + "local_delivery", + "virtual", + "relay", + "alias", + "catch_all", + "outbound", + "other", + "unknown" + ], + "title": "RuntimeMailDomainRole", + "type": "string" + }, + "RuntimeMailListener": { + "additionalProperties": false, + "description": "A protocol listener bound to a same-node transport service.", + "properties": { + "advertised_identity": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Advertised Identity", + "type": "string" + }, + "auth_mechanisms": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailAuthMechanism" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Auth Mechanisms", + "type": "array" + }, + "banner": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Banner", + "type": "string" + }, + "capabilities": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Capabilities", + "type": "array" + }, + "component_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Component Ref", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "listener_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Protocol" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailListenerRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Role" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "tls_mode": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailTlsMode" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Tls Mode" + }, + "tls_versions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Tls Versions", + "type": "array" + } + }, + "required": [ + "listener_id" + ], + "title": "RuntimeMailListener", + "type": "object" + }, + "RuntimeMailListenerRole": { + "description": "Observed role a mail listener plays for the node-scoped mail service.", + "enum": [ + "inbound_mx", + "submission", + "mail_access", + "local_delivery", + "relay", + "lmtp", + "sieve", + "monitoring", + "other", + "unknown" + ], + "title": "RuntimeMailListenerRole", + "type": "string" + }, + "RuntimeMailMailbox": { + "additionalProperties": false, + "description": "A service-local mailbox/account record.\n\nRaw credentials are intentionally unrepresentable. The model records only\nauthentication mechanisms and credential-strength classification.", + "properties": { + "account_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Account Ref", + "type": "string" + }, + "address": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "auth_mechanisms": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailAuthMechanism" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Auth Mechanisms", + "type": "array" + }, + "credential_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailCredentialClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Credential Classification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "domain_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Domain Ref", + "type": "string" + }, + "local_part": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Local Part", + "type": "string" + }, + "local_user_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Local User Ref", + "type": "string" + }, + "mailbox_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailMailboxRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "user", + "title": "Role" + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailMailboxStatus" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Status" + }, + "store_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Store Ref", + "type": "string" + } + }, + "required": [ + "mailbox_id", + "address" + ], + "title": "RuntimeMailMailbox", + "type": "object" + }, + "RuntimeMailMailboxRole": { + "description": "Portable role/classification of a service-local mailbox.", + "enum": [ + "user", + "admin", + "service", + "shared", + "postmaster", + "abuse", + "automated", + "other", + "unknown" + ], + "title": "RuntimeMailMailboxRole", + "type": "string" + }, + "RuntimeMailMailboxStatus": { + "description": "Observed account status for a service-local mailbox.", + "enum": [ + "enabled", + "disabled", + "locked", + "suspended", + "unknown", + "other" + ], + "title": "RuntimeMailMailboxStatus", + "type": "string" + }, + "RuntimeMailMailboxStore": { + "additionalProperties": false, + "description": "Mailbox storage backing for service-local mailboxes.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailMailboxStoreKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "store_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "store_id" + ], + "title": "RuntimeMailMailboxStore", + "type": "object" + }, + "RuntimeMailMailboxStoreKind": { + "description": "Observed mailbox storage backing kind.", + "enum": [ + "maildir", + "mbox", + "database", + "object_store", + "remote", + "other", + "unknown" + ], + "title": "RuntimeMailMailboxStoreKind", + "type": "string" + }, + "RuntimeMailProtocol": { + "description": "Portable protocol family for a runtime mail listener or relationship.", + "enum": [ + "smtp", + "esmtp", + "submission", + "smtps", + "imap", + "imaps", + "pop3", + "pop3s", + "lmtp", + "sieve", + "other", + "unknown" + ], + "title": "RuntimeMailProtocol", + "type": "string" + }, + "RuntimeMailQueue": { + "additionalProperties": false, + "description": "Shape of a mail queue; dynamic content is explicitly classified.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailQueueKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "message_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Message Count" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "queue_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "stability": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailQueueStability" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "dynamic", + "title": "Stability" + } + }, + "required": [ + "queue_id" + ], + "title": "RuntimeMailQueue", + "type": "object" + }, + "RuntimeMailQueueKind": { + "description": "Observed mail queue kind.", + "enum": [ + "incoming", + "active", + "deferred", + "hold", + "bounce", + "corrupt", + "maildrop", + "mailbox", + "other", + "unknown" + ], + "title": "RuntimeMailQueueKind", + "type": "string" + }, + "RuntimeMailQueueStability": { + "description": "Stability class for dynamic mail queue facts.", + "enum": [ + "steady_state", + "dynamic", + "volatile", + "log_derived", + "unknown", + "other" + ], + "title": "RuntimeMailQueueStability", + "type": "string" + }, + "RuntimeMailRoutingKind": { + "description": "Portable mail routing/aliasing rule kind.", + "enum": [ + "alias", + "forward", + "virtual_alias", + "relay", + "local_delivery", + "catch_all", + "list", + "other", + "unknown" + ], + "title": "RuntimeMailRoutingKind", + "type": "string" + }, + "RuntimeMailRoutingRule": { + "additionalProperties": false, + "description": "A portable routing, aliasing, local-delivery, or relay rule.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailRoutingKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "relay_host": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Relay Host", + "type": "string" + }, + "rule_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "source_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source Ref", + "type": "string" + }, + "target_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Ref", + "type": "string" + } + }, + "required": [ + "rule_id" + ], + "title": "RuntimeMailRoutingRule", + "type": "object" + }, + "RuntimeMailService": { + "additionalProperties": false, + "description": "A node-scoped runtime mail-service logical inventory.", + "properties": { + "aliases": { + "items": { + "$ref": "#/$defs/RuntimeMailAlias" + }, + "title": "Aliases", + "type": "array" + }, + "components": { + "items": { + "$ref": "#/$defs/RuntimeMailComponent" + }, + "title": "Components", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "domains": { + "items": { + "$ref": "#/$defs/RuntimeMailDomain" + }, + "title": "Domains", + "type": "array" + }, + "engine": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Engine", + "type": "string" + }, + "listeners": { + "items": { + "$ref": "#/$defs/RuntimeMailListener" + }, + "title": "Listeners", + "type": "array" + }, + "mail_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "mailbox_stores": { + "items": { + "$ref": "#/$defs/RuntimeMailMailboxStore" + }, + "title": "Mailbox Stores", + "type": "array" + }, + "mailboxes": { + "items": { + "$ref": "#/$defs/RuntimeMailMailbox" + }, + "title": "Mailboxes", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "queues": { + "items": { + "$ref": "#/$defs/RuntimeMailQueue" + }, + "title": "Queues", + "type": "array" + }, + "routing_rules": { + "items": { + "$ref": "#/$defs/RuntimeMailRoutingRule" + }, + "title": "Routing Rules", + "type": "array" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "settings": { + "items": { + "$ref": "#/$defs/RuntimeMailSetting" + }, + "title": "Settings", + "type": "array" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "mail_service_id" + ], + "title": "RuntimeMailService", + "type": "object" + }, + "RuntimeMailSetting": { + "additionalProperties": false, + "description": "A mail-service runtime setting with source/provenance and redaction.", + "properties": { + "component_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Component Ref", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMailSettingProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "source_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source Path", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + } + }, + "required": [ + "setting_id", + "name" + ], + "title": "RuntimeMailSetting", + "type": "object" + }, + "RuntimeMailSettingProvenance": { + "description": "Origin/provenance class for a mail-service configuration setting.", + "enum": [ + "configuration_file", + "command_output", + "image_default", + "operator_override", + "runtime_default", + "environment", + "unknown", + "other" + ], + "title": "RuntimeMailSettingProvenance", + "type": "string" + }, + "RuntimeMailTlsMode": { + "description": "Observed TLS/STARTTLS posture for a mail listener or relationship.", + "enum": [ + "none", + "starttls_available", + "starttls_required", + "implicit_tls", + "tls_terminated_upstream", + "unknown", + "other" + ], + "title": "RuntimeMailTlsMode", + "type": "string" + }, + "RuntimeMount": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "source_sensitivity": { + "anyOf": [ + { + "pattern": "^[Rr][Ee][Dd][Aa][Cc][Tt][Ee][Dd]$", + "type": "string" + }, + { + "pattern": "^[Oo][Pp][Ee][Rr][Aa][Tt][Oo][Rr][_-][Ss][Ee][Cc][Rr][Ee][Tt]$", + "type": "string" + } + ] + } + }, + "required": [ + "source_sensitivity" + ] + }, + "then": { + "not": { + "properties": { + "source": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "source" + ] + } + } + }, + { + "if": { + "properties": { + "options_sensitivity": { + "anyOf": [ + { + "pattern": "^[Rr][Ee][Dd][Aa][Cc][Tt][Ee][Dd]$", + "type": "string" + }, + { + "pattern": "^[Oo][Pp][Ee][Rr][Aa][Tt][Oo][Rr][_-][Ss][Ee][Cc][Rr][Ee][Tt]$", + "type": "string" + } + ] + } + }, + "required": [ + "options_sensitivity" + ] + }, + "then": { + "not": { + "properties": { + "options": { + "minItems": 1, + "type": "array" + } + }, + "required": [ + "options" + ] + } + } + } + ], + "description": "A filesystem mount observed on a runtime node.", + "properties": { + "backend_generated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Backend Generated" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "filesystem_type": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Filesystem Type", + "type": "string" + }, + "options": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Options", + "type": "array" + }, + "options_sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Options Sensitivity" + }, + "propagation": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMountPropagation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Propagation" + }, + "read_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Read Only" + }, + "source": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source", + "type": "string" + }, + "source_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeMountSourceKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Source Kind" + }, + "source_sensitivity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Source Sensitivity" + }, + "stability": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeFilesystemStability" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Stability" + }, + "target": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target", + "type": "string" + } + }, + "required": [ + "target" + ], + "title": "RuntimeMount", + "type": "object" + }, + "RuntimeMountPropagation": { + "description": "Portable propagation mode for a runtime filesystem mount.", + "enum": [ + "private", + "rprivate", + "shared", + "rshared", + "slave", + "rslave", + "unknown", + "other" + ], + "title": "RuntimeMountPropagation", + "type": "string" + }, + "RuntimeMountSourceKind": { + "description": "Portable source kind for a runtime filesystem mount.", + "enum": [ + "volume", + "bind", + "tmpfs", + "image", + "other", + "unknown" + ], + "title": "RuntimeMountSourceKind", + "type": "string" + }, + "RuntimeNamespaceConfiguration": { + "additionalProperties": false, + "description": "Observed namespace modes for a runtime node or container.", + "properties": { + "cgroup": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Cgroup", + "type": "string" + }, + "ipc": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ipc", + "type": "string" + }, + "pid": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Pid", + "type": "string" + }, + "userns": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Userns", + "type": "string" + }, + "uts": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Uts", + "type": "string" + } + }, + "title": "RuntimeNamespaceConfiguration", + "type": "object" + }, + "RuntimeNetworkBackendDetail": { + "additionalProperties": false, + "description": "Observed backend network driver and IPAM detail for an endpoint's network.\n\n``driver_options`` and ``ipam_options`` are the bounded extension seam for\nbackend-native key/value facts; raw backend inspect payloads are not stored.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "driver": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDriver" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Driver" + }, + "driver_options": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Driver Options", + "type": "object" + }, + "ipam_driver": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ipam Driver", + "type": "string" + }, + "ipam_options": { + "additionalProperties": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Ipam Options", + "type": "object" + } + }, + "title": "RuntimeNetworkBackendDetail", + "type": "object" + }, + "RuntimeNetworkDetectionAppProtocol": { + "description": "App-layer parser family enabled in the detection engine.", + "enum": [ + "http", + "tls", + "dns", + "ssh", + "smtp", + "ftp", + "smb", + "http2", + "nfs", + "rdp", + "krb5", + "mqtt", + "modbus", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionAppProtocol", + "type": "string" + }, + "RuntimeNetworkDetectionControlCapability": { + "description": "Operation supported by a detection-engine control channel.", + "enum": [ + "rule_reload", + "ruleset_update", + "config_reload", + "stats_query", + "pause", + "resume", + "shutdown", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionControlCapability", + "type": "string" + }, + "RuntimeNetworkDetectionControlChannel": { + "additionalProperties": false, + "description": "A bounded detection-engine control or reload channel.", + "properties": { + "auth_required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Auth Required" + }, + "capabilities": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionControlCapability" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Capabilities", + "type": "array" + }, + "channel_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionControlChannelKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + } + }, + "required": [ + "channel_id" + ], + "title": "RuntimeNetworkDetectionControlChannel", + "type": "object" + }, + "RuntimeNetworkDetectionControlChannelKind": { + "description": "Portable local or remote control-channel family.", + "enum": [ + "unix_socket", + "tcp_api", + "http_api", + "cli", + "signal", + "file_drop", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionControlChannelKind", + "type": "string" + }, + "RuntimeNetworkDetectionEngine": { + "additionalProperties": false, + "description": "Node-scoped runtime inventory for an IDS/NDR detection engine.", + "properties": { + "app_layer_protocols": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionAppProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "App Layer Protocols", + "type": "array" + }, + "configuration_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Configuration File Refs", + "type": "array" + }, + "control_channels": { + "items": { + "$ref": "#/$defs/RuntimeNetworkDetectionControlChannel" + }, + "title": "Control Channels", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "engine_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionEngineKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Engine Kind" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "implementation": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionEngineImplementation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Implementation" + }, + "log_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Log File Refs", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "network_detection_engine_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "network_sets": { + "items": { + "$ref": "#/$defs/RuntimeNetworkDetectionNetworkSet" + }, + "title": "Network Sets", + "type": "array" + }, + "output_streams": { + "items": { + "$ref": "#/$defs/RuntimeNetworkDetectionOutputStream" + }, + "title": "Output Streams", + "type": "array" + }, + "process_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Process Ref", + "type": "string" + }, + "revision": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Revision", + "type": "string" + }, + "rule_sources": { + "items": { + "$ref": "#/$defs/RuntimeNetworkDetectionRuleSource" + }, + "title": "Rule Sources", + "type": "array" + }, + "sensor_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Sensor Ref", + "type": "string" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "network_detection_engine_id" + ], + "title": "RuntimeNetworkDetectionEngine", + "type": "object" + }, + "RuntimeNetworkDetectionEngineImplementation": { + "description": "Observed product family for a network detection engine.", + "enum": [ + "suricata", + "snort", + "zeek", + "security_onion", + "arkime", + "corelight", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionEngineImplementation", + "type": "string" + }, + "RuntimeNetworkDetectionEngineKind": { + "description": "Portable detection-engine role/family.", + "enum": [ + "ids", + "ips", + "ndr", + "nsm", + "nta", + "detection_engine", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionEngineKind", + "type": "string" + }, + "RuntimeNetworkDetectionEventType": { + "description": "Event family emitted by a detection output stream.", + "enum": [ + "alert", + "http", + "dns", + "tls", + "ssh", + "smtp", + "ftp", + "smb", + "flow", + "netflow", + "stats", + "fileinfo", + "anomaly", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionEventType", + "type": "string" + }, + "RuntimeNetworkDetectionNetworkSet": { + "additionalProperties": false, + "description": "A network zoning or service-group address-set variable.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionNetworkSetKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "network_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Network Refs", + "type": "array" + }, + "selector_values": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Selector Values", + "type": "array" + }, + "set_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "set_id" + ], + "title": "RuntimeNetworkDetectionNetworkSet", + "type": "object" + }, + "RuntimeNetworkDetectionNetworkSetKind": { + "description": "Portable network/address-set variable role.", + "enum": [ + "home_net", + "external_net", + "dmz_net", + "internal_net", + "service_group", + "custom", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionNetworkSetKind", + "type": "string" + }, + "RuntimeNetworkDetectionOutputFormat": { + "description": "Portable detection output stream format.", + "enum": [ + "eve_json", + "fast_log", + "json", + "pcap", + "netflow", + "syslog", + "text", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionOutputFormat", + "type": "string" + }, + "RuntimeNetworkDetectionOutputStream": { + "additionalProperties": false, + "description": "A detection alert, telemetry, or summary output stream.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "event_types": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionEventType" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "title": "Event Types", + "type": "array" + }, + "format": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionOutputFormat" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Format" + }, + "path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Path", + "type": "string" + }, + "stream_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "stream_id" + ], + "title": "RuntimeNetworkDetectionOutputStream", + "type": "object" + }, + "RuntimeNetworkDetectionRuleFormat": { + "description": "Portable rule/content syntax family.", + "enum": [ + "suricata_rule", + "snort_rule", + "zeek_script", + "sigma", + "yara", + "stix", + "json", + "yaml", + "text", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionRuleFormat", + "type": "string" + }, + "RuntimeNetworkDetectionRuleSource": { + "additionalProperties": false, + "description": "A loaded rule, IOC, script, or managed detection-content source.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "File Refs", + "type": "array" + }, + "format": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionRuleFormat" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Format" + }, + "generated_by": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Generated By", + "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkDetectionRuleSourceKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "loaded": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Loaded" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "rule_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rule Count" + }, + "source_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "source_id" + ], + "title": "RuntimeNetworkDetectionRuleSource", + "type": "object" + }, + "RuntimeNetworkDetectionRuleSourceKind": { + "description": "Portable rule-source provenance/family.", + "enum": [ + "built_in", + "community", + "managed", + "local", + "threat_intel", + "ioc", + "emerging_threats", + "unknown", + "other" + ], + "title": "RuntimeNetworkDetectionRuleSourceKind", + "type": "string" + }, + "RuntimeNetworkDriver": { + "description": "Observed backend network driver class for a realized network.", + "enum": [ + "bridge", + "overlay", + "host", + "ipvlan", + "macvlan", + "none", + "other", + "unknown" + ], + "title": "RuntimeNetworkDriver", + "type": "string" + }, + "RuntimeNetworkEndpoint": { + "additionalProperties": false, + "description": "An observed container network endpoint (per-network attachment).\n\n``network`` references a declared switch-backed infrastructure network by\nname. Per-network ``aliases``, observed ``dns_names``, and backend-generated\n``generated_dns_names`` are kept as three distinct lists; generated names\nare not stable scenario identity.", + "properties": { + "aliases": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Aliases", + "type": "array" + }, + "backend": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkBackendDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "backend_generated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Backend Generated" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "dns_names": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Dns Names", + "type": "array" + }, + "endpoint_id": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Endpoint Id", + "type": "string" + }, + "endpoint_id_stability": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkIdStability" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Endpoint Id Stability" + }, + "gateway": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Gateway", + "type": "string" + }, + "generated_dns_names": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Generated Dns Names", + "type": "array" + }, + "ip_address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ip Address", + "type": "string" + }, + "ip_prefix_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ip Prefix Length" + }, + "mac_address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Mac Address", + "type": "string" + }, + "network": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Network", + "type": "string" + }, + "network_id": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Network Id", + "type": "string" + }, + "network_id_stability": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkIdStability" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Network Id Stability" + } + }, + "required": [ + "network" + ], + "title": "RuntimeNetworkEndpoint", + "type": "object" + }, + "RuntimeNetworkIdStability": { + "description": "Stability class for a backend-generated network or endpoint identifier.\n\nDistinct from :class:`RuntimeFilesystemStability`: filesystem stability and\nbackend-identifier stability are different concepts (ADR-025).", + "enum": [ + "stable", + "ephemeral", + "unknown", + "other" + ], + "title": "RuntimeNetworkIdStability", + "type": "string" + }, + "RuntimeNetworkRealization": { + "additionalProperties": false, + "description": "Observed container network realization facts for a node.\n\n``hostname`` and ``domainname`` are the container's network identity facts,\nkept distinct from per-network aliases and DNS names.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "domainname": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Domainname", + "type": "string" + }, + "endpoints": { + "items": { + "$ref": "#/$defs/RuntimeNetworkEndpoint" + }, + "title": "Endpoints", + "type": "array" + }, + "hostname": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Hostname", + "type": "string" + }, + "published_ports": { + "items": { + "$ref": "#/$defs/RuntimePublishedPort" + }, + "title": "Published Ports", + "type": "array" + } + }, + "title": "RuntimeNetworkRealization", + "type": "object" + }, + "RuntimeNetworkSensor": { + "additionalProperties": false, + "description": "Node-scoped runtime inventory for a passive or inline network sensor.", + "properties": { + "capture_interfaces": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Capture Interfaces", + "type": "array" + }, + "capture_mode": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkSensorCaptureMode" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Capture Mode" + }, + "configuration_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Configuration File Refs", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "implementation": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkSensorImplementation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Implementation" + }, + "log_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Log File Refs", + "type": "array" + }, + "monitored_network_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Monitored Network Refs", + "type": "array" + }, + "monitoring_posture": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkSensorMonitoringPosture" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Monitoring Posture" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "network_sensor_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "process_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Process Ref", + "type": "string" + }, + "revision": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Revision", + "type": "string" + }, + "sensor_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeNetworkSensorKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Sensor Kind" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "network_sensor_id" + ], + "title": "RuntimeNetworkSensor", + "type": "object" + }, + "RuntimeNetworkSensorCaptureMode": { + "description": "Portable capture mechanism family for an observed sensor.", + "enum": [ + "pcap", + "af_packet", + "nfqueue", + "netmap", + "dpdk", + "flow", + "unknown", + "other" + ], + "title": "RuntimeNetworkSensorCaptureMode", + "type": "string" + }, + "RuntimeNetworkSensorImplementation": { + "description": "Observed product family for a network sensor.", + "enum": [ + "suricata", + "snort", + "zeek", + "tcpdump", + "security_onion", + "unknown", + "other" + ], + "title": "RuntimeNetworkSensorImplementation", + "type": "string" + }, + "RuntimeNetworkSensorKind": { + "description": "Portable network-sensor function.", + "enum": [ + "ids", + "ips", + "nsm", + "ndr", + "packet_capture", + "flow_collector", + "other", + "unknown" + ], + "title": "RuntimeNetworkSensorKind", + "type": "string" + }, + "RuntimeNetworkSensorMonitoringPosture": { + "description": "Whether the sensor observes passively, inline, or locally.", + "enum": [ + "passive", + "inline", + "host_local", + "unknown", + "other" + ], + "title": "RuntimeNetworkSensorMonitoringPosture", + "type": "string" + }, + "RuntimeOperationalPolicy": { + "additionalProperties": false, + "description": "Observed restart and resource-limit policy for a runtime node.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "resource_limits": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeResourceLimits" + }, + { + "type": "null" + } + ], + "default": null + }, + "restart": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeRestartPolicy" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Restart" + } + }, + "title": "RuntimeOperationalPolicy", + "type": "object" + }, + "RuntimeOrchestrationAuthority": { + "additionalProperties": false, + "description": "Node-scoped runtime inventory for a container-spawn control authority.\n\n``control_interface_ref`` is the ``control_interface_id`` of a same-node\n``RuntimeControlInterface`` (the docker.sock shell), resolved by\n``validator.py`` \u2014 referenced, never duplicated. The\n``privilege_class`` discriminator selects the required profile the\n``require_profile_for_privilege_class`` guard enforces.", + "properties": { + "control_interface_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Control Interface Ref", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "engine": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeOrchestrationEngine" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Engine" + }, + "engine_api_version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Engine Api Version", + "type": "string" + }, + "lifecycle_policy": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeOrchestrationLifecyclePolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "orchestration_authority_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "privilege_class": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeOrchestrationPrivilegeClass" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Privilege Class" + }, + "realized_children": { + "items": { + "$ref": "#/$defs/RuntimeOrchestrationRealizedChild" + }, + "title": "Realized Children", + "type": "array" + }, + "scope": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeOrchestrationScope" + }, + { + "type": "null" + } + ], + "default": null + }, + "spawn_templates": { + "items": { + "$ref": "#/$defs/RuntimeOrchestrationSpawnTemplate" + }, + "title": "Spawn Templates", + "type": "array" + } + }, + "required": [ + "orchestration_authority_id" + ], + "title": "RuntimeOrchestrationAuthority", + "type": "object" + }, + "RuntimeOrchestrationEngine": { + "description": "Open taxonomy for the orchestration engine/runtime family.\n\nOpen taxonomy: carries both ``unknown`` and ``other`` per the DSL-139\nenum-sentinel rule.", + "enum": [ + "docker", + "containerd", + "podman", + "kubernetes", + "cri_o", + "unknown", + "other" + ], + "title": "RuntimeOrchestrationEngine", + "type": "string" + }, + "RuntimeOrchestrationLifecyclePolicy": { + "additionalProperties": false, + "description": "The lifecycle posture applied to children the authority spawns.", + "properties": { + "cleanup": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Cleanup", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "execution_timeout": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Execution Timeout", + "type": "string" + }, + "timeout": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Timeout", + "type": "string" + } + }, + "title": "RuntimeOrchestrationLifecyclePolicy", + "type": "object" + }, + "RuntimeOrchestrationPrivilegeClass": { + "description": "Open taxonomy for the privilege an orchestration authority commands.\n\n``host_root_equivalent`` denotes an authority whose spawn surface is\nequivalent to host root (e.g. a read-write ``docker.sock`` holder);\n``namespaced`` denotes a privilege-scoped/rootless authority. Open\ntaxonomy: carries both ``unknown`` and ``other``.", + "enum": [ + "host_root_equivalent", + "namespaced", + "unknown", + "other" + ], + "title": "RuntimeOrchestrationPrivilegeClass", + "type": "string" + }, + "RuntimeOrchestrationRealizedChild": { + "additionalProperties": false, + "description": "An observed, realized child workload spawned by the authority.", + "properties": { + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Count" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Evidence Ref", + "type": "string" + }, + "image_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Image Ref", + "type": "string" + }, + "workload_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "workload_id" + ], + "title": "RuntimeOrchestrationRealizedChild", + "type": "object" + }, + "RuntimeOrchestrationScope": { + "additionalProperties": false, + "description": "The organizational/environment scope an orchestration authority governs.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "environment_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Environment Name", + "type": "string" + }, + "organization_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Organization Ref", + "type": "string" + } + }, + "title": "RuntimeOrchestrationScope", + "type": "object" + }, + "RuntimeOrchestrationSpawnTemplate": { + "additionalProperties": false, + "description": "A template the authority is authorized to instantiate (image + purpose).", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "image_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Image Ref", + "type": "string" + }, + "purpose": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Purpose", + "type": "string" + }, + "template_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "template_id" + ], + "title": "RuntimeOrchestrationSpawnTemplate", + "type": "object" + }, + "RuntimePackage": { + "additionalProperties": false, + "description": "A package observed in a runtime image or node.", + "properties": { + "architecture": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Architecture", + "type": "string" + }, + "manager": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Manager", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "purl": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Purl", + "type": "string" + }, + "source": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source", + "type": "string" + }, + "version": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "manager", + "name", + "version" + ], + "title": "RuntimePackage", + "type": "object" + }, + "RuntimePackageVulnerabilityFinding": { + "additionalProperties": false, + "description": "A scanner-derived CVE/advisory finding for an observed package.", + "properties": { + "advisory_url": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Advisory Url", + "type": "string" + }, + "fixed_version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Fixed Version", + "type": "string" + }, + "id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Id", + "type": "string" + }, + "image_digest": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Image Digest", + "type": "string" + }, + "installed_version": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Installed Version", + "type": "string" + }, + "package_name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Package Name", + "type": "string" + }, + "scan_time": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Scan Time", + "type": "string" + }, + "scanner": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Scanner", + "type": "string" + }, + "scanner_database": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Scanner Database", + "type": "string" + }, + "scanner_version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Scanner Version", + "type": "string" + }, + "severity": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePackageVulnerabilitySeverity" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Severity" + } + }, + "required": [ + "id", + "package_name", + "installed_version", + "scanner", + "image_digest", + "scan_time" + ], + "title": "RuntimePackageVulnerabilityFinding", + "type": "object" + }, + "RuntimePackageVulnerabilitySeverity": { + "description": "Scanner-derived package finding severity.", + "enum": [ + "unknown", + "low", + "medium", + "high", + "critical", + "other" + ], + "title": "RuntimePackageVulnerabilitySeverity", + "type": "string" + }, + "RuntimePlatformApplication": { + "additionalProperties": false, + "description": "Node-scoped runtime inventory for a security platform application.\n\n``service`` references the owning same-node ``Node.services[].name``. The\ninventory is observation metadata above transport; it never duplicates the\ninbound HTTP route surface (``runtime.applications``) or mutates\n``Node.services``. ``authorization_ref`` is the ``app_authorization_id`` of\nthe platform's internal RBAC store (resolved by the semantic validator).", + "properties": { + "authorization_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Authorization Ref", + "type": "string" + }, + "connectors": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplicationConnector" + }, + "title": "Connectors", + "type": "array" + }, + "content_objects": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplicationContentObject" + }, + "title": "Content Objects", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "execution_policy": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationExecutionPolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "markings": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplicationMarking" + }, + "title": "Markings", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "organizations": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplicationOrganization" + }, + "title": "Organizations", + "type": "array" + }, + "platform_application_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "platform_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Platform Kind" + }, + "product": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Product", + "type": "string" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "settings": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplicationSetting" + }, + "title": "Settings", + "type": "array" + }, + "tenants": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplicationTenant" + }, + "title": "Tenants", + "type": "array" + }, + "upstream_bindings": { + "items": { + "$ref": "#/$defs/RuntimePlatformApplicationUpstreamBinding" + }, + "title": "Upstream Bindings", + "type": "array" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "platform_application_id" + ], + "title": "RuntimePlatformApplication", + "type": "object" + }, + "RuntimePlatformApplicationConnector": { + "additionalProperties": false, + "description": "A connector/integration wired into the platform.\n\nA connector never carries a raw credential value; its credential posture is\nrecorded purely via :attr:`credential_classification`.", + "properties": { + "connector_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "credential_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationSettingClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "plain", + "title": "Credential Classification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationConnectorKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + } + }, + "required": [ + "connector_id" + ], + "title": "RuntimePlatformApplicationConnector", + "type": "object" + }, + "RuntimePlatformApplicationConnectorKind": { + "description": "Open taxonomy of platform connector/integration kinds.\n\nOpen taxonomy: carries both ``unknown`` and ``other``.", + "enum": [ + "analyzer_engine", + "responder", + "webhook", + "notification", + "sync", + "unknown", + "other" + ], + "title": "RuntimePlatformApplicationConnectorKind", + "type": "string" + }, + "RuntimePlatformApplicationContentObject": { + "additionalProperties": false, + "description": "A typed platform content object as a bounded parsed manifest entry.\n\nThe object carries a typed ``kind``, bounded typed ``attributes``, typed\n``references`` (to other ``content_object_id`` values), ``marking_refs``,\nand ``evidence_refs`` \u2014 structurally **never** a raw object body.", + "properties": { + "attributes": { + "additionalProperties": true, + "title": "Attributes", + "type": "object" + }, + "content_object_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationContentObjectKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Kind" + }, + "marking_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Marking Refs", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "references": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "content_object_id" + ], + "title": "RuntimePlatformApplicationContentObject", + "type": "object" + }, + "RuntimePlatformApplicationContentObjectKind": { + "description": "Open taxonomy of typed platform content objects.\n\nA content object is a bounded parsed manifest entry (typed kind + bounded\nattributes + typed references) \u2014 never a raw object body. Open taxonomy:\ncarries both ``unknown`` and ``other``.", + "enum": [ + "index_pattern", + "visualization", + "dashboard", + "search", + "feed", + "taxonomy", + "galaxy_cluster", + "warninglist", + "object_template", + "sharing_group", + "case_template", + "custom_field", + "analyzer", + "responder", + "workflow", + "app", + "unknown", + "other" + ], + "title": "RuntimePlatformApplicationContentObjectKind", + "type": "string" + }, + "RuntimePlatformApplicationExecutionPolicy": { + "additionalProperties": false, + "description": "The platform's job/analyzer execution model (rate limits, runner).", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "job_timeout": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Job Timeout", + "type": "string" + }, + "max_concurrent_jobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Concurrent Jobs" + }, + "policy_id": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Policy Id", + "type": "string" + }, + "rate_limit": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Rate Limit", + "type": "string" + }, + "runner": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Runner", + "type": "string" + } + }, + "title": "RuntimePlatformApplicationExecutionPolicy", + "type": "object" + }, + "RuntimePlatformApplicationKind": { + "description": "Open discriminator for the platform-application product family.\n\nThe ``require_profile_for_platform_kind`` guard ties each concrete member\nto a required content/binding profile. Open taxonomy: carries both\n``unknown`` and ``other``.", + "enum": [ + "analytics_dashboard", + "threat_intel", + "case_management", + "analyzer_engine", + "soar", + "unknown", + "other" + ], + "title": "RuntimePlatformApplicationKind", + "type": "string" + }, + "RuntimePlatformApplicationMarking": { + "additionalProperties": false, + "description": "A releasability marking (TLP/PAP/distribution) defined by the platform.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "level": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Level", + "type": "string" + }, + "marking_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "scheme": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationMarkingScheme" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "tlp", + "title": "Scheme" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + } + }, + "required": [ + "marking_id" + ], + "title": "RuntimePlatformApplicationMarking", + "type": "object" + }, + "RuntimePlatformApplicationMarkingScheme": { + "description": "Closed releasability-marking scheme vocabulary.\n\nStandard-fixed (FIRST TLP 2.0 / PAP / MISP distribution lattice), so per\nthe enum-sentinel rule it carries neither ``unknown`` nor ``other``.", + "enum": [ + "tlp", + "pap", + "distribution" + ], + "title": "RuntimePlatformApplicationMarkingScheme", + "type": "string" + }, + "RuntimePlatformApplicationOrganization": { + "additionalProperties": false, + "description": "An organization/owner tenant known to the platform application.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "organization_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "organization_id" + ], + "title": "RuntimePlatformApplicationOrganization", + "type": "object" + }, + "RuntimePlatformApplicationSetting": { + "additionalProperties": false, + "description": "An observed platform setting with provenance and sensitivity.\n\nExplicitly redacted/operator-secret settings must omit their raw ``value``.\nA setting name does not by itself redact SDL scenario content.", + "properties": { + "classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationSettingClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "plain", + "title": "Classification" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationSettingProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "redaction": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Redaction", + "type": "string" + }, + "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + } + }, + "required": [ + "setting_id" + ], + "title": "RuntimePlatformApplicationSetting", + "type": "object" + }, + "RuntimePlatformApplicationSettingClassification": { + "description": "Closed sensitivity classification for a platform setting value.\n\nClosed structural redaction vocabulary: carries neither ``unknown`` nor\n``other`` (an unclassified value is ``plain``).", + "enum": [ + "plain", + "redacted", + "operator_secret" + ], + "title": "RuntimePlatformApplicationSettingClassification", + "type": "string" + }, + "RuntimePlatformApplicationSettingProvenance": { + "description": "Open taxonomy of where an observed platform setting originated.\n\nOpen taxonomy: carries both ``unknown`` and ``other``.", + "enum": [ + "default", + "environment", + "config_file", + "database", + "runtime", + "unknown", + "other" + ], + "title": "RuntimePlatformApplicationSettingProvenance", + "type": "string" + }, + "RuntimePlatformApplicationTenant": { + "additionalProperties": false, + "description": "A tenant/namespace scope within the platform application.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "tenant_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "tenant_id" + ], + "title": "RuntimePlatformApplicationTenant", + "type": "object" + }, + "RuntimePlatformApplicationUpstreamBinding": { + "additionalProperties": false, + "description": "An outbound binding to an upstream node/service (data source, backend).", + "properties": { + "binding_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimePlatformApplicationUpstreamBindingRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Role" + }, + "target_node_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Node Ref", + "type": "string" + }, + "target_service_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Target Service Ref", + "type": "string" + } + }, + "required": [ + "binding_id" + ], + "title": "RuntimePlatformApplicationUpstreamBinding", + "type": "object" + }, + "RuntimePlatformApplicationUpstreamBindingRole": { + "description": "Open taxonomy for the role an outbound upstream binding plays.\n\nOpen taxonomy: carries both ``unknown`` and ``other``.", + "enum": [ + "data_source", + "backend_api", + "sync_peer", + "index_backend", + "cql_backend", + "unknown", + "other" + ], + "title": "RuntimePlatformApplicationUpstreamBindingRole", + "type": "string" + }, + "RuntimeProcessCapabilityOverride": { + "additionalProperties": false, + "description": "A capability delta scoped to a single process or its descendant subtree.\n\nPer ADR-030, scoped Linux-capability facts stay under\n``RuntimeCapabilityPolicy`` rather than migrating onto\n``RuntimeProcessIdentity``. ``subject`` reuses the existing process\nidentity model as a selector / evidence anchor \u2014 it identifies *which*\nprocess or subtree the delta applies to but does not own capability\nsemantics. ``effective`` / ``add`` / ``drop`` record the scoped delta\nrelative to the container-wide baseline on the enclosing\n``RuntimeCapabilityPolicy``; ``required`` is intentionally omitted from\nthis surface because overrides describe deltas, not new container-wide\nrequirements.", + "properties": { + "add": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Add", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "drop": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Drop", + "type": "array" + }, + "effective": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Effective", + "type": "array" + }, + "scope": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeCapabilityOverrideScope" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "process", + "title": "Scope" + }, + "subject": { + "$ref": "#/$defs/RuntimeProcessIdentity" + } + }, + "required": [ + "subject" + ], + "title": "RuntimeProcessCapabilityOverride", + "type": "object" + }, + "RuntimeProcessIdentity": { + "additionalProperties": false, + "description": "Observed process identity for a runtime node.", + "properties": { + "command": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Command", + "type": "array" + }, + "command_redacted": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Command Redacted" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "group": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Group", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "parent_pid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Pid" + }, + "pid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pid" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeProcessRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Role" + }, + "user": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "User", + "type": "string" + }, + "working_directory": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Working Directory", + "type": "string" + } + }, + "title": "RuntimeProcessIdentity", + "type": "object" + }, + "RuntimeProcessRole": { + "description": "Observed role of a process in a runtime process set.", + "enum": [ + "primary", + "supervisor", + "worker", + "sidecar", + "agent", + "other", + "unknown" + ], + "title": "RuntimeProcessRole", + "type": "string" + }, + "RuntimePublishedPort": { + "additionalProperties": false, + "description": "An observed host-published port binding for a container endpoint.\n\nHost IP, host port, container port, and protocol are kept distinct; this is\na runtime/host exposure fact, not a node service declaration (ADR-025).", + "properties": { + "container_port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Container Port" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "host_ip": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Host Ip", + "type": "string" + }, + "host_port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Host Port" + }, + "protocol": { + "default": "tcp", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + } + }, + "required": [ + "container_port" + ], + "title": "RuntimePublishedPort", + "type": "object" + }, + "RuntimePublishedPortRef": { + "additionalProperties": false, + "description": "Typed reference to an observed host-published port binding.\n\n``RuntimePublishedPort`` currently has no stable id, so the tuple is the\nreference shape used by semantic validation.", + "properties": { + "container_port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Container Port" + }, + "host_ip": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Host Ip", + "type": "string" + }, + "host_port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Host Port" + }, + "protocol": { + "default": "tcp", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + } + }, + "required": [ + "container_port" + ], + "title": "RuntimePublishedPortRef", + "type": "object" + }, + "RuntimeResourceLimits": { + "additionalProperties": false, + "description": "Observed runtime/cgroup resource limits for a node.", + "properties": { + "cpu": { + "anyOf": [ + { + "type": "number" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cpu" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "memory": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Memory" + }, + "memory_swap": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Memory Swap" + }, + "open_files": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Open Files" + }, + "pids": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pids" + } + }, + "title": "RuntimeResourceLimits", + "type": "object" + }, + "RuntimeRestartPolicy": { + "description": "Portable restart policy classification observed at runtime.", + "enum": [ + "no", + "always", + "on_failure", + "unless_stopped", + "unknown", + "other" + ], + "title": "RuntimeRestartPolicy", + "type": "string" + }, + "RuntimeScheduledJob": { + "additionalProperties": false, + "description": "Node-scoped runtime inventory for a recurring scheduled job (cadence + run-state).", + "properties": { + "command_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Command Ref", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "run_state": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeScheduledJobRunState" + }, + { + "type": "null" + } + ], + "default": null + }, + "schedule": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeScheduledJobSchedule" + }, + { + "type": "null" + } + ], + "default": null + }, + "scheduled_job_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "scheduled_job_id" + ], + "title": "RuntimeScheduledJob", + "type": "object" + }, + "RuntimeScheduledJobLastResult": { + "description": "Open run-outcome taxonomy for the most recent scheduled-job execution.", + "enum": [ + "success", + "failure", + "pending", + "unknown", + "other" + ], + "title": "RuntimeScheduledJobLastResult", + "type": "string" + }, + "RuntimeScheduledJobRunState": { + "additionalProperties": false, + "description": "Observed run-state for a scheduled job.", + "properties": { + "last_result": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeScheduledJobLastResult" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Last Result" + }, + "last_run": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Last Run", + "type": "string" + }, + "next_run": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Next Run", + "type": "string" + } + }, + "title": "RuntimeScheduledJobRunState", + "type": "object" + }, + "RuntimeScheduledJobSchedule": { + "additionalProperties": false, + "description": "The recurrence cadence for a scheduled job.", + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeScheduledJobScheduleKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "interval", + "title": "Kind" + }, + "spec": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Spec", + "type": "string" + } + }, + "title": "RuntimeScheduledJobSchedule", + "type": "object" + }, + "RuntimeScheduledJobScheduleKind": { + "description": "Closed structural recurrence vocabulary for a scheduled-job schedule.\n\nThis is a fixed structural vocabulary (POSIX crontab / RFC 5545 RRULE /\nfixed-interval), so per the DSL-139 enum-sentinel rule it carries neither\n``unknown`` nor ``other``.", + "enum": [ + "interval", + "cron", + "calendar" + ], + "title": "RuntimeScheduledJobScheduleKind", + "type": "string" + }, + "RuntimeSecurityMonitoringAgent": { + "additionalProperties": false, + "description": "An agent or sensor enrolled with the manager.", + "properties": { + "address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "agent_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "group_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Group Refs", + "type": "array" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "node_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Node Ref", + "type": "string" + }, + "os": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Os", + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringAgentStatus" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Status" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "agent_id", + "name" + ], + "title": "RuntimeSecurityMonitoringAgent", + "type": "object" + }, + "RuntimeSecurityMonitoringAgentGroup": { + "additionalProperties": false, + "description": "A manager-side enrolled-agent group.", + "properties": { + "configuration_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Configuration File Refs", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "group_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "member_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Member Refs", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + } + }, + "required": [ + "group_id" + ], + "title": "RuntimeSecurityMonitoringAgentGroup", + "type": "object" + }, + "RuntimeSecurityMonitoringAgentStatus": { + "description": "Observed enrolled-agent status.", + "enum": [ + "available", + "active", + "disconnected", + "never_connected", + "pending", + "removed", + "unknown", + "other" + ], + "title": "RuntimeSecurityMonitoringAgentStatus", + "type": "string" + }, + "RuntimeSecurityMonitoringComponent": { + "additionalProperties": false, + "description": "A manager daemon, module, or internal component.", + "properties": { + "component_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringComponentKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "process_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Process Ref", + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringComponentStatus" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Status" + } + }, + "required": [ + "component_id", + "name" + ], + "title": "RuntimeSecurityMonitoringComponent", + "type": "object" + }, + "RuntimeSecurityMonitoringComponentKind": { + "description": "Portable component/module kind inside a security-monitoring manager.", + "enum": [ + "analysis_engine", + "agent_ingestion", + "agent_enrollment", + "module_supervisor", + "log_collection", + "alerting", + "api", + "cluster", + "indexer_forwarder", + "vulnerability_detection", + "file_integrity_monitoring", + "rootkit_detection", + "sca", + "active_response", + "integration", + "database", + "other", + "unknown" + ], + "title": "RuntimeSecurityMonitoringComponentKind", + "type": "string" + }, + "RuntimeSecurityMonitoringComponentStatus": { + "description": "Observed component/module status.", + "enum": [ + "running", + "stopped", + "disabled", + "enabled", + "degraded", + "failed", + "unknown", + "other" + ], + "title": "RuntimeSecurityMonitoringComponentStatus", + "type": "string" + }, + "RuntimeSecurityMonitoringContentFormat": { + "description": "Portable format family for manager-owned content.", + "enum": [ + "wazuh_rule_xml", + "wazuh_decoder_xml", + "sigma", + "yara", + "stix", + "json", + "yaml", + "xml", + "query", + "unknown", + "other" + ], + "title": "RuntimeSecurityMonitoringContentFormat", + "type": "string" + }, + "RuntimeSecurityMonitoringContentKind": { + "description": "Kind of manager-owned detection or monitoring content.", + "enum": [ + "rule_corpus", + "decoder_corpus", + "correlation_rules", + "sca_policies", + "active_response", + "cdb_list", + "threat_intel", + "dashboard", + "other", + "unknown" + ], + "title": "RuntimeSecurityMonitoringContentKind", + "type": "string" + }, + "RuntimeSecurityMonitoringContentSet": { + "additionalProperties": false, + "description": "A manager-owned rule, decoder, policy, list, or query corpus.", + "properties": { + "content_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "file_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "File Count" + }, + "file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "File Refs", + "type": "array" + }, + "format": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringContentFormat" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Format" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringContentKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Kind" + }, + "loaded": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Loaded" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + } + }, + "required": [ + "content_id" + ], + "title": "RuntimeSecurityMonitoringContentSet", + "type": "object" + }, + "RuntimeSecurityMonitoringDetectionDefinition": { + "additionalProperties": false, + "description": "A parsed detection definition loaded by a security-monitoring manager.", + "properties": { + "canonical_digest": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Canonical Digest", + "type": "string" + }, + "compliance_tags": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Compliance Tags", + "type": "array" + }, + "content_set_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Content Set Ref", + "type": "string" + }, + "decoded_as": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Decoded As", + "type": "array" + }, + "decoder_fields": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Decoder Fields", + "type": "array" + }, + "decoder_names": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Decoder Names", + "type": "array" + }, + "definition_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "definition_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringDetectionDefinitionKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Definition Kind" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "digest_algorithm": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Digest Algorithm", + "type": "string" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "engine": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringDetectionEngine" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Engine" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "field_predicates": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringFieldPredicate" + }, + "title": "Field Predicates", + "type": "array" + }, + "frequency": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Frequency" + }, + "groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Groups", + "type": "array" + }, + "if_matched_sid_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "If Matched Sid Refs", + "type": "array" + }, + "if_sid_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "If Sid Refs", + "type": "array" + }, + "level": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Level" + }, + "loaded": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Loaded" + }, + "match_strings": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Match Strings", + "type": "array" + }, + "mitre_attack_ids": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Mitre Attack Ids", + "type": "array" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "native_id": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Native Id", + "type": "string" + }, + "parent_definition_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Parent Definition Refs", + "type": "array" + }, + "parser_accepted": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parser Accepted" + }, + "regex_patterns": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Regex Patterns", + "type": "array" + }, + "same_source_constraints": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Same Source Constraints", + "type": "array" + }, + "severity": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Severity", + "type": "string" + }, + "source_artifact_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source Artifact Ref", + "type": "string" + }, + "source_end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source End Line" + }, + "source_file_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source File Ref", + "type": "string" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "tactic_labels": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Tactic Labels", + "type": "array" + }, + "tags": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Tags", + "type": "array" + }, + "target_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Target Refs", + "type": "array" + }, + "technique_labels": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Technique Labels", + "type": "array" + }, + "timeframe_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeframe Seconds" + } + }, + "required": [ + "definition_id" + ], + "title": "RuntimeSecurityMonitoringDetectionDefinition", + "type": "object" + }, + "RuntimeSecurityMonitoringDetectionDefinitionKind": { + "description": "Portable kind of parsed security-monitoring detection definition.", + "enum": [ + "rule", + "correlation_rule", + "decoder", + "list_backed_rule", + "sca_policy_check", + "active_response_trigger", + "sigma_rule", + "yara_rule", + "suricata_rule", + "siem_analytic", + "other", + "unknown" + ], + "title": "RuntimeSecurityMonitoringDetectionDefinitionKind", + "type": "string" + }, + "RuntimeSecurityMonitoringDetectionEngine": { + "description": "Portable detection-definition engine family.", + "enum": [ + "wazuh", + "ossec", + "sigma", + "yara", + "suricata", + "elastic_security", + "splunk_enterprise_security", + "microsoft_sentinel", + "unknown", + "other" + ], + "title": "RuntimeSecurityMonitoringDetectionEngine", + "type": "string" + }, + "RuntimeSecurityMonitoringFieldPredicate": { + "additionalProperties": false, + "description": "A normalized field predicate extracted from a detection definition.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "field": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Field", + "type": "string" + }, + "operator": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringFieldPredicateOperator" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Operator" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + } + }, + "required": [ + "field" + ], + "title": "RuntimeSecurityMonitoringFieldPredicate", + "type": "object" + }, + "RuntimeSecurityMonitoringFieldPredicateOperator": { + "description": "Portable operator for a parsed field-level predicate.", + "enum": [ + "equals", + "matches", + "contains", + "exists", + "in", + "regex", + "less_than", + "greater_than", + "other", + "unknown" + ], + "title": "RuntimeSecurityMonitoringFieldPredicateOperator", + "type": "string" + }, + "RuntimeSecurityMonitoringImplementation": { + "description": "Product family for an observed security-monitoring manager.", + "enum": [ + "wazuh", + "ossec", + "elastic_security", + "splunk_enterprise_security", + "security_onion", + "microsoft_sentinel", + "unknown", + "other" + ], + "title": "RuntimeSecurityMonitoringImplementation", + "type": "string" + }, + "RuntimeSecurityMonitoringListener": { + "additionalProperties": false, + "description": "A manager listener bound to a same-node transport service.", + "properties": { + "auth_required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Auth Required" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "listener_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "protocol": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + }, + "role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringListenerRole" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Role" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "tls_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tls Enabled" + } + }, + "required": [ + "listener_id" + ], + "title": "RuntimeSecurityMonitoringListener", + "type": "object" + }, + "RuntimeSecurityMonitoringListenerRole": { + "description": "Logical role of a manager transport listener.", + "enum": [ + "agent_event_ingestion", + "agent_enrollment", + "syslog_ingestion", + "api", + "alert_forwarding", + "indexer_forwarding", + "dashboard", + "other", + "unknown" + ], + "title": "RuntimeSecurityMonitoringListenerRole", + "type": "string" + }, + "RuntimeSecurityMonitoringManager": { + "additionalProperties": false, + "description": "Node-scoped runtime inventory for a SIEM/security-monitoring manager.", + "properties": { + "agent_groups": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringAgentGroup" + }, + "title": "Agent Groups", + "type": "array" + }, + "agents": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringAgent" + }, + "title": "Agents", + "type": "array" + }, + "components": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringComponent" + }, + "title": "Components", + "type": "array" + }, + "configuration_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Configuration File Refs", + "type": "array" + }, + "content_sets": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringContentSet" + }, + "title": "Content Sets", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "detection_definitions": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringDetectionDefinition" + }, + "title": "Detection Definitions", + "type": "array" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "implementation": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringImplementation" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Implementation" + }, + "listeners": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringListener" + }, + "title": "Listeners", + "type": "array" + }, + "log_file_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Log File Refs", + "type": "array" + }, + "manager_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringManagerKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Manager Kind" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "revision": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Revision", + "type": "string" + }, + "security_monitoring_manager_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "settings": { + "items": { + "$ref": "#/$defs/RuntimeSecurityMonitoringSetting" + }, + "title": "Settings", + "type": "array" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "security_monitoring_manager_id" + ], + "title": "RuntimeSecurityMonitoringManager", + "type": "object" + }, + "RuntimeSecurityMonitoringManagerKind": { + "description": "Portable manager role/family.", + "enum": [ + "siem", + "xdr", + "hids", + "ndr", + "log_management", + "detection_engine", + "security_monitoring", + "unknown", + "other" + ], + "title": "RuntimeSecurityMonitoringManagerKind", + "type": "string" + }, + "RuntimeSecurityMonitoringSetting": { + "additionalProperties": false, + "description": "A bounded manager setting with sensitivity classification.", + "properties": { + "component_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Component Ref", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSecurityMonitoringSettingProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "source_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Source Path", + "type": "string" + }, + "value": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + }, + "value_classification": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSensitivityClassification" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Value Classification" + } + }, + "required": [ + "setting_id", + "name" + ], + "title": "RuntimeSecurityMonitoringSetting", + "type": "object" + }, + "RuntimeSecurityMonitoringSettingProvenance": { + "description": "Where an observed manager setting came from.", + "enum": [ + "introspection", + "configuration_file", + "api", + "image_default", + "operator_override", + "runtime_default", + "unknown", + "other" + ], + "title": "RuntimeSecurityMonitoringSettingProvenance", + "type": "string" + }, + "RuntimeSensitivityClassification": { + "description": "Sensitivity/redaction class for observed runtime facts.", + "enum": [ + "plain", + "redacted", + "secret_fixture", + "operator_secret", + "unknown", + "other" + ], + "title": "RuntimeSensitivityClassification", + "type": "string" + }, + "RuntimeServiceListener": { + "additionalProperties": false, + "description": "Observed generic runtime listener attached to a node.", + "properties": { + "address": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Address", + "type": "string" + }, + "address_family": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeListenerAddressFamily" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unspecified", + "title": "Address Family" + }, + "bind_interface": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Bind Interface", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_refs": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Port" + }, + "process_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Process Name", + "type": "string" + }, + "process_ref": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Process Ref", + "type": "string" + }, + "protocol": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeListenerProtocol" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "tcp", + "title": "Protocol" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeListenerProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "published_port_refs": { + "items": { + "$ref": "#/$defs/RuntimePublishedPortRef" + }, + "title": "Published Port Refs", + "type": "array" + }, + "readiness": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeListenerReadiness" + }, + { + "type": "null" + } + ], + "default": null + }, + "scope": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeListenerScope" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Scope" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "service_listener_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "socket_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Socket Path", + "type": "string" + } + }, + "required": [ + "service_listener_id" + ], + "title": "RuntimeServiceListener", + "type": "object" + }, + "RuntimeSoftwareComponent": { + "additionalProperties": false, + "description": "A software component observed as part of a runtime node's state.", + "properties": { + "component_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Component Id", + "type": "string" + }, + "component_type": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSoftwareComponentType" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Component Type" + }, + "cpe": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Cpe", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "ecosystem": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Ecosystem", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/RuntimeSoftwareComponentHash" + }, + "title": "Hashes", + "type": "array" + }, + "installed_paths": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Installed Paths", + "type": "array" + }, + "manifest_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Manifest Path", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "package_manager": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Package Manager", + "type": "string" + }, + "package_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Package Name", + "type": "string" + }, + "package_version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Package Version", + "type": "string" + }, + "provenance": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSoftwareComponentProvenance" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Provenance" + }, + "purl": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Purl", + "type": "string" + }, + "version": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "component_id", + "name" + ], + "title": "RuntimeSoftwareComponent", + "type": "object" + }, + "RuntimeSoftwareComponentHash": { + "additionalProperties": false, + "description": "Digest attached to an observed runtime software component.", + "properties": { + "algorithm": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Algorithm", + "type": "string" + }, + "value": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ], + "title": "RuntimeSoftwareComponentHash", + "type": "object" + }, + "RuntimeSoftwareComponentProvenance": { + "description": "Origin class for an observed runtime software component fact.", + "enum": [ + "package_manager", + "dependency_manifest", + "sbom", + "scanner", + "image_metadata", + "filesystem", + "process_inspection", + "operator", + "self_reported", + "unknown", + "other" + ], + "title": "RuntimeSoftwareComponentProvenance", + "type": "string" + }, + "RuntimeSoftwareComponentType": { + "description": "Portable type for a software component observed on a runtime node.", + "enum": [ + "application", + "framework", + "library", + "container", + "platform", + "operating_system", + "device", + "device_driver", + "firmware", + "file", + "data", + "unknown", + "other" + ], + "title": "RuntimeSoftwareComponentType", + "type": "string" + }, + "RuntimeSshServer": { + "additionalProperties": false, + "description": "A scoped sshd server configuration observed on a node.\n\nEach configuration carries a stable ``ssh_server_id`` and an explicit\n``service`` reference pointing at the owning same-node service (bare\nname or ``nodes..services.`` form). Global sshd directives\nsit at the top level; scoped overrides live in ``match_rules``.\n\nThe configuration is the SDL surface for participant-observable sshd\npolicy. It is not a backend sshd-payload dump and must not embed raw\n``sshd_config`` text, raw ``sshd -T`` output, container inspect\npayloads, session transcripts, or environment values.", + "properties": { + "accept_env": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Accept Env", + "type": "array" + }, + "allow_groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allow Groups", + "type": "array" + }, + "allow_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allow Users", + "type": "array" + }, + "authentication_methods": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Authentication Methods", + "type": "array" + }, + "authorized_keys_file": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Authorized Keys File", + "type": "string" + }, + "chroot_directory": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Chroot Directory", + "type": "string" + }, + "deny_groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Deny Groups", + "type": "array" + }, + "deny_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Deny Users", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "forced_command": { + "anyOf": [ + { + "$ref": "#/$defs/SshForcedCommand" + }, + { + "type": "null" + } + ], + "default": null + }, + "match_rules": { + "items": { + "$ref": "#/$defs/SshMatchRule" + }, + "title": "Match Rules", + "type": "array" + }, + "password_authentication": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Password Authentication" + }, + "permit_tty": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Permit Tty" + }, + "pubkey_authentication": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pubkey Authentication" + }, + "service": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "ssh_server_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "required": [ + "ssh_server_id", + "service" + ], + "title": "RuntimeSshServer", + "type": "object" + }, + "RuntimeSudoPrincipalKind": { + "description": "Kind of principal a sudo rule grants privilege to.", + "enum": [ + "user", + "group", + "other", + "unknown" + ], + "title": "RuntimeSudoPrincipalKind", + "type": "string" + }, + "RuntimeSudoRule": { + "additionalProperties": false, + "description": "An observed sudo/sudoers privilege grant.\n\nThe portable model is the structured principal/run-as/command scope.\n``raw_entry`` may carry the original sudoers line only as optional\ndescriptive evidence; ``command_redacted`` marks a rule whose command\nscope was withheld because it carried sensitive arguments.", + "properties": { + "command_redacted": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Command Redacted" + }, + "commands": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Commands", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "host_scope": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Host Scope", + "type": "string" + }, + "nopasswd": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Nopasswd" + }, + "principal": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Principal", + "type": "string" + }, + "principal_kind": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeSudoPrincipalKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "user", + "title": "Principal Kind" + }, + "raw_entry": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Raw Entry", + "type": "string" + }, + "run_as_groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Run As Groups", + "type": "array" + }, + "run_as_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Run As Users", + "type": "array" + } + }, + "required": [ + "principal" + ], + "title": "RuntimeSudoRule", + "type": "object" + }, + "Script": { + "additionalProperties": false, + "description": "A timed sequence of human-readable durations parsed to seconds.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "end_time": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "End Time" + }, + "events": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ] + }, + "minProperties": 1, + "title": "Events", + "type": "object" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "speed": { + "anyOf": [ + { + "type": "number" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Speed" + }, + "start_time": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Start Time" + } + }, + "required": [ + "start_time", + "end_time", + "speed", + "events" + ], + "title": "Script", + "type": "object" + }, + "SemanticDigest": { + "additionalProperties": false, + "description": "Profile-labelled digest of one expanded authoring scenario.", + "properties": { + "algorithm": { + "const": "sha256", + "title": "Algorithm", + "type": "string" + }, + "profile": { + "const": "aces-sdl-semantic/v1", + "title": "Profile", + "type": "string" + }, + "value": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Value", + "type": "string" + } + }, + "required": [ + "profile", + "algorithm", + "value" + ], + "title": "SemanticDigest", + "type": "object" + }, + "ServiceManagerKind": { + "description": "Kind of service manager whose unit inventory is being recorded.\n\nInitially scoped to ``systemd``; ``other`` is the escape hatch for managers\nthat have not yet been promoted to their own typed enum value (per\nADR-035 \u00a7 5).", + "enum": [ + "systemd", + "other", + "unknown" + ], + "title": "ServiceManagerKind", + "type": "string" + }, + "ServiceManagerUnit": { + "additionalProperties": false, + "description": "Observed service-manager unit state on a realized range node.\n\nEach record carries a stable ACES ``unit_id`` (the reference target), the\nnative ``unit_name`` (e.g. ``sshd.service``), the participant-observable\nlifecycle facts captured by ``systemctl`` (load/active/sub/enabled/result),\nand bounded optional evidence: the unit-file path, an ``ExecStart``\ndescriptor with explicit redaction discipline, the main PID when present,\nand an optional same-node ``Node.services[]`` ref.\n\nThis is observed WHAT-IS state, not provisioning intent or authored\nbehavior; see ADR-035 for the boundary against ``Node.services``,\n``conditions``, ``runtime.processes``, ``runtime.container.init_process``,\n``runtime.operational_policy``, and ``runtime.ssh_servers``.", + "properties": { + "active_state": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceUnitActiveState" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Active State" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "enabled_state": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceUnitEnabledState" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Enabled State" + }, + "exec_start": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceUnitExecStart" + }, + { + "type": "null" + } + ], + "default": null + }, + "exit_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Exit Code" + }, + "load_state": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceUnitLoadState" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Load State" + }, + "main_pid": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Main Pid" + }, + "manager_kind": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceManagerKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "systemd", + "title": "Manager Kind" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceUnitResult" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "unknown", + "title": "Result" + }, + "service": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Service", + "type": "string" + }, + "status_text": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Status Text", + "type": "string" + }, + "sub_state": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Sub State", + "type": "string" + }, + "unit_file_path": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Unit File Path", + "type": "string" + }, + "unit_id": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Unit Id", + "type": "string" + }, + "unit_name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Unit Name", + "type": "string" + }, + "unit_type": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceUnitKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "other", + "title": "Unit Type" + } + }, + "required": [ + "unit_id", + "unit_name" + ], + "title": "ServiceManagerUnit", + "type": "object" + }, + "ServicePort": { + "additionalProperties": false, + "description": "A network service exposed by a node. From OCSF NetworkEndpoint.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "anyOf": [ + { + "const": "" + }, + { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + ], + "default": "", + "title": "Name" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Port" + }, + "protocol": { + "default": "tcp", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Protocol", + "type": "string" + } + }, + "required": [ + "port" + ], + "title": "ServicePort", + "type": "object" + }, + "ServiceUnitActiveState": { + "description": "Observed unit *active* state (``systemctl list-units`` ACTIVE column).", + "enum": [ + "active", + "reloading", + "inactive", + "failed", + "activating", + "deactivating", + "unknown", + "other" + ], + "title": "ServiceUnitActiveState", + "type": "string" + }, + "ServiceUnitEnabledState": { + "description": "Observed unit-file enable state (``systemctl is-enabled``).", + "enum": [ + "enabled", + "enabled_runtime", + "disabled", + "static", + "alias", + "masked", + "generated", + "indirect", + "transient", + "unknown", + "other" + ], + "title": "ServiceUnitEnabledState", + "type": "string" + }, + "ServiceUnitExecStart": { + "additionalProperties": false, + "description": "A typed service-manager ``ExecStart`` configuration.\n\nMirrors the SSH ``ForcedCommand`` redaction discipline (ADR-031): the\n``command_kind`` discriminates how ``command`` is interpreted, and\n``command_redacted=True`` forces ``command`` empty and\n``command_kind=redacted``. The model never carries raw secret-bearing\nargv -- if the underlying ``ExecStart=`` includes credentials, tokens, or\noperator-only arguments, the surface stores only the redacted shape.", + "properties": { + "command": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Command", + "type": "string" + }, + "command_kind": { + "anyOf": [ + { + "$ref": "#/$defs/ServiceUnitExecStartKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "absolute_path", + "title": "Command Kind" + }, + "command_redacted": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Command Redacted" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + } + }, + "title": "ServiceUnitExecStart", + "type": "object" + }, + "ServiceUnitExecStartKind": { + "description": "How a service-manager unit's ``ExecStart`` command is carried.\n\n``absolute_path`` records a concrete executable path; ``redacted`` records\nthat the underlying ``ExecStart`` carries secret-bearing arguments and is\nwithheld from the SDL model.", + "enum": [ + "absolute_path", + "redacted" + ], + "title": "ServiceUnitExecStartKind", + "type": "string" + }, + "ServiceUnitKind": { + "description": "Type of service-manager unit (systemd unit kinds with ``other`` escape).", + "enum": [ + "service", + "socket", + "target", + "timer", + "path", + "mount", + "automount", + "swap", + "device", + "slice", + "scope", + "other", + "unknown" + ], + "title": "ServiceUnitKind", + "type": "string" + }, + "ServiceUnitLoadState": { + "description": "Observed unit *load* state (``systemctl list-units`` LOAD column).", + "enum": [ + "loaded", + "not_found", + "masked", + "error", + "merged", + "stub", + "bad_setting", + "unknown", + "other" + ], + "title": "ServiceUnitLoadState", + "type": "string" + }, + "ServiceUnitResult": { + "description": "Observed last-run unit result class (systemd ``Result=`` values).", + "enum": [ + "success", + "exit_code", + "signal", + "timeout", + "watchdog", + "oom_kill", + "core_dump", + "start_limit_hit", + "resources", + "protocol", + "other", + "unknown" + ], + "title": "ServiceUnitResult", + "type": "string" + }, + "SimpleProperties": { + "additionalProperties": false, + "description": "Network properties for a switch/subnet: CIDR, gateway, and flags.", + "properties": { + "cidr": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Cidr", + "type": "string" + }, + "gateway": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Gateway", + "type": "string" + }, + "internal": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Internal" + } + }, + "required": [ + "cidr", + "gateway" + ], + "title": "SimpleProperties", + "type": "object" + }, + "Source": { + "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": { + "build": { + "anyOf": [ + { + "$ref": "#/$defs/ContainerImageBuildProvenance" + }, + { + "type": "null" + } + ], + "default": null + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "version": { + "default": "*", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Source", + "type": "object" + }, + "SshForcedCommand": { + "additionalProperties": false, + "description": "A typed sshd ``ForceCommand`` configuration.\n\nThe ``command_kind`` discriminates how ``command`` is interpreted:\n\n- ``absolute_path``: a concrete executable path (or ``${var}``) starting\n with ``/``. Validated via ``absolute_path_or_var``.\n- ``internal_sftp``: the sshd-internal sftp subsystem; ``command`` must\n equal the literal ``\"internal-sftp\"``.\n- ``redacted``: the underlying command is withheld from the SDL model\n because it carries secrets, session identifiers, or operator-only\n arguments; ``command`` must be empty and ``command_redacted`` must\n be true.", + "properties": { + "command": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Command", + "type": "string" + }, + "command_kind": { + "anyOf": [ + { + "$ref": "#/$defs/SshForcedCommandKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "absolute_path", + "title": "Command Kind" + }, + "command_redacted": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Command Redacted" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + } + }, + "title": "SshForcedCommand", + "type": "object" + }, + "SshForcedCommandKind": { + "description": "Kind of forced command an sshd ``ForceCommand`` directive carries.", + "enum": [ + "absolute_path", + "internal_sftp", + "redacted" + ], + "title": "SshForcedCommandKind", + "type": "string" + }, + "SshMatchCriterion": { + "additionalProperties": false, + "description": "A single sshd ``Match`` criterion (one ``kind`` + one ``pattern``).", + "properties": { + "kind": { + "anyOf": [ + { + "$ref": "#/$defs/SshMatchCriterionKind" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Kind" + }, + "pattern": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Pattern", + "type": "string" + } + }, + "required": [ + "kind", + "pattern" + ], + "title": "SshMatchCriterion", + "type": "object" + }, + "SshMatchCriterionKind": { + "description": "OpenSSH ``Match`` criterion kinds.\n\n``LOCAL_USER`` is the explicit \"match this concrete account\" form that\nsemantic validation may cross-check against ``runtime.local_identity``\nwhen that inventory is present. ``USER`` is the looser pattern form\n(wildcards, comma-separated lists) and is not cross-checked.", + "enum": [ + "user", + "group", + "host", + "address", + "local_user", + "local_port", + "rdomain", + "all" + ], + "title": "SshMatchCriterionKind", + "type": "string" + }, + "SshMatchRule": { + "additionalProperties": false, + "description": "A scoped sshd ``Match`` rule.\n\n``criteria`` is the ordered conjunction of one-or-more match criteria\n(``Match User kali`` is one criterion; ``Match User kali Address 10.0.0.0/8``\nis two). Per-rule sshd directives override the surrounding global\ndirectives for sessions that match all listed criteria.", + "properties": { + "accept_env": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Accept Env", + "type": "array" + }, + "allow_groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allow Groups", + "type": "array" + }, + "allow_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Allow Users", + "type": "array" + }, + "authentication_methods": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Authentication Methods", + "type": "array" + }, + "authorized_keys_file": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Authorized Keys File", + "type": "string" + }, + "chroot_directory": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Chroot Directory", + "type": "string" + }, + "criteria": { + "items": { + "$ref": "#/$defs/SshMatchCriterion" + }, + "title": "Criteria", + "type": "array" + }, + "deny_groups": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Deny Groups", + "type": "array" + }, + "deny_users": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Deny Users", + "type": "array" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "forced_command": { + "anyOf": [ + { + "$ref": "#/$defs/SshForcedCommand" + }, + { + "type": "null" + } + ], + "default": null + }, + "match_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "password_authentication": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Password Authentication" + }, + "permit_tty": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Permit Tty" + }, + "pubkey_authentication": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pubkey Authentication" + } + }, + "required": [ + "match_id" + ], + "title": "SshMatchRule", + "type": "object" + }, + "Story": { + "additionalProperties": false, + "description": "Top-level exercise orchestration \u2014 a group of scripts.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "scripts": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Scripts", + "type": "array" + }, + "speed": { + "anyOf": [ + { + "type": "number" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": 1.0, + "title": "Speed" + } + }, + "required": [ + "scripts" + ], + "title": "Story", + "type": "object" + }, + "StringPredicate": { + "additionalProperties": false, + "description": "Compare a governed string property with typed string operands.", + "properties": { + "expected": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "type": "array" + } + ], + "title": "Expected" + }, + "kind": { + "const": "string", + "default": "string", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals", + "in", + "not_in" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected" + ], + "title": "StringPredicate", + "type": "object" + }, + "SubjectQuantifier": { + "description": "Quantification over a proposition's finite, resolved subject set.", + "enum": [ + "all", + "any", + "at_least" + ], + "title": "SubjectQuantifier", + "type": "string" + }, + "TruthCompositionMode": { + "description": "Portable composition over assertion outcomes.", + "enum": [ + "all_of", + "any_of", + "at_least" + ], + "title": "TruthCompositionMode", + "type": "string" + }, + "Vulnerability": { + "additionalProperties": false, + "description": "A named vulnerability with CWE classification.", + "properties": { + "class": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Class", + "type": "string" + }, + "description": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "name": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Name", + "type": "string" + }, + "technical": { + "anyOf": [ + { + "type": "boolean" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": false, + "title": "Technical" + } + }, + "required": [ + "name", + "description", + "class" + ], + "title": "Vulnerability", + "type": "object" + }, + "Workflow": { + "additionalProperties": false, + "description": "A declarative experiment control graph over objectives.", + "properties": { + "compensation": { + "anyOf": [ + { + "$ref": "#/$defs/WorkflowCompensationPolicy" + }, + { + "type": "null" + } + ], + "default": null + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "start": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Start", + "type": "string" + }, + "steps": { + "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/WorkflowStep" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, + "title": "Steps", + "type": "object" + }, + "timeout": { + "anyOf": [ + { + "$ref": "#/$defs/WorkflowTimeoutPolicy" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "start", + "steps" + ], + "title": "Workflow", + "type": "object" + }, + "WorkflowCompensationFailurePolicy": { + "description": "How primary workflow status should treat compensation failures.", + "enum": [ + "fail_workflow", + "record_and_continue" + ], + "title": "WorkflowCompensationFailurePolicy", + "type": "string" + }, + "WorkflowCompensationMode": { + "description": "Workflow-level compensation behavior.", + "enum": [ + "automatic", + "disabled" + ], + "title": "WorkflowCompensationMode", + "type": "string" + }, + "WorkflowCompensationPolicy": { + "additionalProperties": false, + "description": "Workflow-level compensation policy.", + "properties": { + "failure_policy": { + "$ref": "#/$defs/WorkflowCompensationFailurePolicy", + "default": "fail_workflow" + }, + "mode": { + "$ref": "#/$defs/WorkflowCompensationMode", + "default": "disabled" + }, + "on": { + "items": { + "$ref": "#/$defs/WorkflowCompensationTrigger" + }, + "title": "On", + "type": "array" + }, + "order": { + "default": "reverse_completion", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Order", + "type": "string" + } + }, + "title": "WorkflowCompensationPolicy", + "type": "object" + }, + "WorkflowCompensationTrigger": { + "description": "Terminal workflow reasons that may trigger compensation.", + "enum": [ + "failed", + "cancelled", + "timed_out" + ], + "title": "WorkflowCompensationTrigger", + "type": "string" + }, + "WorkflowPredicate": { + "additionalProperties": false, + "description": "Typed branch predicate over assertions, objectives, and prior step state.", + "properties": { + "assertions": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Assertions", + "type": "array" + }, + "objectives": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Objectives", + "type": "array" + }, + "steps": { + "items": { + "$ref": "#/$defs/WorkflowStepStateRef" + }, + "title": "Steps", + "type": "array" + } + }, + "title": "WorkflowPredicate", + "type": "object" + }, + "WorkflowStep": { + "additionalProperties": false, + "description": "A named workflow step with explicit portable control semantics.", + "properties": { + "branches": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Branches", + "type": "array" + }, + "cases": { + "items": { + "$ref": "#/$defs/WorkflowSwitchCase" + }, + "title": "Cases", + "type": "array" + }, + "compensate_with": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Compensate With", + "type": "string" + }, + "default": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Default", + "type": "string" + }, + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "else": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Else", + "type": "string" + }, + "join": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Join", + "type": "string" + }, + "max_attempts": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Attempts" + }, + "next": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Next", + "type": "string" + }, + "objective": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Objective", + "type": "string" + }, + "on_exhausted": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "On Exhausted", + "type": "string" + }, + "on_failure": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "On Failure", + "type": "string" + }, + "on_success": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "On Success", + "type": "string" + }, + "then": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Then", + "type": "string" + }, + "type": { + "$ref": "#/$defs/WorkflowStepType" + }, + "when": { + "anyOf": [ + { + "$ref": "#/$defs/WorkflowPredicate" + }, + { + "type": "null" + } + ], + "default": null + }, + "workflow": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Workflow", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "WorkflowStep", + "type": "object" + }, + "WorkflowStepOutcome": { + "description": "Portable workflow-visible outcomes emitted by executable steps.", + "enum": [ + "succeeded", + "failed", + "exhausted" + ], + "title": "WorkflowStepOutcome", + "type": "string" + }, + "WorkflowStepStateRef": { + "additionalProperties": false, + "description": "Predicate reference to previously observed workflow step state.", + "properties": { + "min_attempts": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Min Attempts" + }, + "outcomes": { + "items": { + "$ref": "#/$defs/WorkflowStepOutcome" + }, + "minItems": 1, + "title": "Outcomes", + "type": "array" + }, + "step": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Step", + "type": "string" + } + }, + "required": [ + "step", + "outcomes" + ], + "title": "WorkflowStepStateRef", + "type": "object" + }, + "WorkflowStepType": { + "description": "Control-flow node types for declarative experiment workflows.", + "enum": [ + "objective", + "decision", + "switch", + "parallel", + "join", + "retry", + "call", + "end" + ], + "title": "WorkflowStepType", + "type": "string" + }, + "WorkflowSwitchCase": { + "additionalProperties": false, + "description": "One ordered branch case within a ``switch`` workflow step.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "next": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Next", + "type": "string" + }, + "when": { + "$ref": "#/$defs/WorkflowPredicate" + } + }, + "required": [ + "when", + "next" + ], + "title": "WorkflowSwitchCase", + "type": "object" + }, + "WorkflowTimeoutPolicy": { + "additionalProperties": false, + "description": "Workflow-level timeout policy.", + "properties": { + "seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "title": "Seconds" + } + }, + "required": [ + "seconds" + ], + "title": "WorkflowTimeoutPolicy", + "type": "object" + } + }, + "$id": "https://aces.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", + "title": "Profile", + "type": "string" + }, + "scenario": { + "$ref": "#/$defs/InstantiatedScenario" + } + }, + "required": [ + "profile", + "scenario" + ], + "title": "SDL Instantiated Scenario Snapshot v1", + "type": "object", + "x-aces-document-phase": "canonical-instantiated-snapshot" +} diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index 516c6a4c8..9eba3f1c1 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -20,7 +20,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -31,7 +31,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -39,7 +39,7 @@ "direction": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Direction", "type": "string" @@ -47,18 +47,35 @@ "from_net": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "From Net", "type": "string" }, "name": { + "anyOf": [ + { + "const": "" + }, + { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + ], "default": "", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Name", - "type": "string" + "title": "Name" }, "ports": { "items": { @@ -68,7 +85,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -80,7 +97,7 @@ "protocol": { "default": "any", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -88,7 +105,7 @@ "to_net": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "To Net", "type": "string" @@ -104,7 +121,7 @@ "auth_method": { "default": "password", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Auth Method", "type": "string" @@ -112,7 +129,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -124,7 +141,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -135,7 +152,7 @@ "groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -145,7 +162,7 @@ "home": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Home", "type": "string" @@ -153,7 +170,7 @@ "mail": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Mail", "type": "string" @@ -161,7 +178,7 @@ "node": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Node", "type": "string" @@ -173,7 +190,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -184,7 +201,7 @@ "shell": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Shell", "type": "string" @@ -192,14 +209,14 @@ "spn": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Spn", "type": "string" }, "username": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Username", "type": "string" @@ -213,12 +230,12 @@ }, "Agent": { "additionalProperties": false, - "description": "An autonomous participant in the scenario.\n\nAgents reference existing scenario elements:\n\n- ``entity`` links to the entities section (team/role) and supplies\n identity and role per ADR-020\n- ``starting_accounts`` links to the accounts section\n- ``allowed_subnets`` links to infrastructure entries\n- ``initial_knowledge`` references nodes and infrastructure\n- ``starting_conditions`` links to the conditions section, giving the\n authoring surface a declarative hook for participant-relevant\n precondition checks (ACT-601)\n- ``authority_anchors`` links to declared SDL elements (entities,\n relationships, content, etc.) that anchor what the participant is\n allowed or expected to do in scenario meaning (ACT-601, ADR-020)\n- ``operating_scope`` links to targetable named scenario elements\n (subnets, hosts, services, content) defining where the participant\n may act or observe (ACT-601, ADR-020)\n- ``observation_boundaries`` links to declared participant observation\n boundaries that define participant-specific projections of world and\n evidence state (SEM-208)\n\nPer ADR-073 the CybORG-inherited ``reward_calculator`` label was removed;\nit was an unbound, unvalidated string and graded reward lives in the\nexperiment/evaluator plane (ADR-055/064/069).", + "description": "An autonomous participant in the scenario.\n\nAgents reference existing scenario elements:\n\n- ``entity`` links to the entities section (team/role) and supplies\n identity and role per ADR-020\n- ``starting_accounts`` links to the accounts section\n- ``allowed_subnets`` links to infrastructure entries\n- ``initial_knowledge`` references nodes and infrastructure\n- ``starting_assertions`` links to precondition assertions, giving the\n authoring surface a declarative hook for participant-relevant starting\n state without equating a probe implementation with truth (ACT-601)\n- ``authority_anchors`` links to declared SDL elements (entities,\n relationships, content, etc.) that anchor what the participant is\n allowed or expected to do in scenario meaning (ACT-601, ADR-020)\n- ``operating_scope`` links to targetable named scenario elements\n (subnets, hosts, services, content) defining where the participant\n may act or observe (ACT-601, ADR-020)\n- ``observation_boundaries`` links to declared participant observation\n boundaries that define participant-specific projections of world and\n evidence state (SEM-208)\n\nPer ADR-073 the CybORG-inherited ``reward_calculator`` label was removed;\nit was an unbound, unvalidated string and graded reward lives in the\nexperiment/evaluator plane (ADR-055/064/069).", "properties": { "actions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -228,7 +245,7 @@ "allowed_subnets": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -238,7 +255,7 @@ "authority_anchors": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -248,7 +265,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -256,7 +273,7 @@ "entity": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Entity", "type": "string" @@ -275,7 +292,7 @@ "observation_boundaries": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -285,7 +302,7 @@ "operating_scope": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -295,27 +312,80 @@ "starting_accounts": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, "title": "Starting Accounts", "type": "array" }, - "starting_conditions": { + "starting_assertions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, - "title": "Starting Conditions", + "title": "Starting Assertions", "type": "array" } }, "title": "Agent", "type": "object" }, + "Assertion": { + "additionalProperties": false, + "description": "A typed use of one proposition at a governed semantic boundary.", + "properties": { + "description": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "polarity": { + "$ref": "#/$defs/AssertionPolarity", + "default": "positive" + }, + "proposition": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Proposition", + "type": "string" + }, + "role": { + "$ref": "#/$defs/AssertionRole" + } + }, + "required": [ + "proposition", + "role" + ], + "title": "Assertion", + "type": "object" + }, + "AssertionPolarity": { + "description": "Expected polarity of the referenced proposition.", + "enum": [ + "positive", + "negative" + ], + "title": "AssertionPolarity", + "type": "string" + }, + "AssertionRole": { + "description": "Boundary at which an assertion constrains its owning construct.", + "enum": [ + "precondition", + "invariant", + "postcondition" + ], + "title": "AssertionRole", + "type": "string" + }, "AssetValue": { "additionalProperties": false, "description": "CIA triad asset valuation for scoring and risk assessment.", @@ -327,7 +397,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -342,7 +412,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -357,7 +427,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -385,7 +455,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -415,15 +485,150 @@ } ] }, + "BindingOrigin": { + "description": "Why a concrete parameter value was selected.", + "enum": [ + "provided", + "default" + ], + "title": "BindingOrigin", + "type": "string" + }, + "BooleanPredicate": { + "additionalProperties": false, + "description": "Compare a governed Boolean property with an expected Boolean.", + "properties": { + "expected": { + "title": "Expected", + "type": "boolean" + }, + "kind": { + "const": "boolean", + "default": "boolean", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected" + ], + "title": "BooleanPredicate", + "type": "object" + }, + "CapabilityConstraint": { + "additionalProperties": false, + "description": "Pre-instantiation constraint retained for one concrete field.", + "properties": { + "allowed_values": { + "items": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "minItems": 1, + "title": "Allowed Values", + "type": "array", + "uniqueItems": true + }, + "field_pointer": { + "maxLength": 4096, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Field Pointer", + "type": "string" + }, + "kind": { + "const": "allowed-values", + "default": "allowed-values", + "title": "Kind", + "type": "string" + }, + "parameter": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 32, + "minItems": 1, + "title": "Parameter", + "type": "array" + } + }, + "required": [ + "field_pointer", + "parameter", + "allowed_values" + ], + "title": "CapabilityConstraint", + "type": "object" + }, "Condition": { "additionalProperties": false, - "description": "A monitoring check deployed to a VM.\n\nEither ``command`` + ``interval`` or ``source`` must be set, not both.", + "description": "A probe implementation deployed to a VM.\n\nEither ``command`` + ``interval`` or ``source`` must be set, not both.", "properties": { "command": { "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -437,7 +642,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -445,7 +650,7 @@ "environment": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -459,7 +664,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -473,11 +678,19 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, + "proposition": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Proposition", + "type": "string" + }, "retries": { "anyOf": [ { @@ -485,7 +698,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -514,7 +727,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -532,7 +745,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -565,7 +778,7 @@ "base_image": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Base Image", "type": "string" @@ -573,7 +786,7 @@ "base_image_digest": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Base Image Digest", "type": "string" @@ -606,7 +819,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -614,7 +827,7 @@ "dockerfile_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Dockerfile Path", "type": "string" @@ -651,7 +864,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -659,7 +872,7 @@ "destination": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Destination", "type": "string" @@ -667,7 +880,7 @@ "format": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Format", "type": "string" @@ -682,7 +895,7 @@ "path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" @@ -694,7 +907,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -716,7 +929,7 @@ "tags": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -726,7 +939,7 @@ "target": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target", "type": "string" @@ -735,7 +948,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -763,22 +976,40 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, + "display_name": { + "default": "", + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Display Name", + "type": "string" + }, "name": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, + "pattern": "^[a-z0-9]", "title": "Name", "type": "string" }, "tags": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -807,23 +1038,32 @@ "description": "An observed logical database within a database service.", "properties": { "database_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Database Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -835,7 +1075,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -902,21 +1142,21 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "grantee_role_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Grantee Role Ref", "type": "string" }, "object_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Object Ref", "type": "string" @@ -928,7 +1168,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -938,7 +1178,7 @@ "privileges": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -952,7 +1192,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -975,7 +1215,7 @@ "properties": { "address": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -983,7 +1223,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -995,7 +1235,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1060,7 +1300,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1074,14 +1314,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -1093,7 +1333,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1103,7 +1343,7 @@ }, "role_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Role Id", "type": "string" @@ -1115,7 +1355,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1153,14 +1393,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -1172,7 +1412,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1182,7 +1422,7 @@ }, "schema_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Schema Id", "type": "string" @@ -1209,14 +1449,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -1228,7 +1468,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1239,7 +1479,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -1251,7 +1491,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1287,21 +1527,21 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "table_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Table Id", "type": "string" @@ -1321,7 +1561,7 @@ "allowed_clients": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1331,7 +1571,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -1343,7 +1583,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1357,7 +1597,7 @@ "key_names": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1367,7 +1607,7 @@ "policy": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Policy", "type": "string" @@ -1382,7 +1622,7 @@ "properties": { "address": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -1390,7 +1630,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -1402,7 +1642,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1413,7 +1653,7 @@ "tls_server_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Tls Server Name", "type": "string" @@ -1425,7 +1665,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1474,7 +1714,7 @@ "properties": { "exchange": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Exchange", "type": "string" @@ -1486,7 +1726,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1566,7 +1806,7 @@ "allow_recursion": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1580,7 +1820,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1594,7 +1834,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -1606,7 +1846,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1628,7 +1868,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1643,7 +1883,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1661,7 +1901,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1683,7 +1923,7 @@ "address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -1691,7 +1931,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -1710,7 +1950,7 @@ "rdata": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Rdata", "type": "string" @@ -1740,7 +1980,7 @@ "target": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target", "type": "string" @@ -1748,7 +1988,7 @@ "text": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1766,14 +2006,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "owner": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Owner", "type": "string" @@ -1785,7 +2025,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1800,7 +2040,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1815,10 +2055,19 @@ "type": "array" }, "rrset_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Rrset Id", + "pattern": "^[a-z0-9]", "type": "string" }, "ttl": { @@ -1828,7 +2077,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1846,7 +2095,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -1864,7 +2113,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1888,14 +2137,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -1907,7 +2156,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -1918,7 +2167,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -1930,7 +2179,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2001,7 +2250,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2015,7 +2264,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2024,7 +2273,7 @@ }, "mname": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Mname", "type": "string" @@ -2036,7 +2285,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2050,7 +2299,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2059,7 +2308,7 @@ }, "rname": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Rname", "type": "string" @@ -2071,7 +2320,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2102,7 +2351,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2116,7 +2365,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2125,7 +2374,7 @@ }, "target": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target", "type": "string" @@ -2137,7 +2386,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2161,7 +2410,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -2173,7 +2422,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2183,7 +2432,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -2195,7 +2444,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2210,7 +2459,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2243,7 +2492,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2254,7 +2503,7 @@ "zone_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2262,10 +2511,19 @@ "type": "array" }, "zone_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Zone Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -2311,7 +2569,7 @@ "allowed_clients": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2325,7 +2583,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2339,7 +2597,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -2351,7 +2609,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2365,7 +2623,7 @@ "primary_servers": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2375,7 +2633,7 @@ "secondary_servers": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2406,7 +2664,7 @@ "arguments": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2416,7 +2674,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -2428,7 +2686,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2475,7 +2733,7 @@ "categories": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2485,14 +2743,23 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "entities": { - "additionalProperties": { - "$ref": "#/$defs/Entity" + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/Entity" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } }, "title": "Entities", "type": "object" @@ -2500,7 +2767,7 @@ "events": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2510,7 +2777,7 @@ "facts": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2520,7 +2787,7 @@ "mission": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Mission", "type": "string" @@ -2528,7 +2795,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -2540,7 +2807,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2554,7 +2821,7 @@ "vulnerabilities": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2567,22 +2834,22 @@ }, "Event": { "additionalProperties": false, - "description": "A triggered action combining conditions and injects.", + "description": "A triggered action combining assertion preconditions and injects.", "properties": { - "conditions": { + "assertions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, - "title": "Conditions", + "title": "Assertions", "type": "array" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -2590,7 +2857,7 @@ "injects": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2600,7 +2867,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -2664,7 +2931,7 @@ "artifact_role": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Artifact Role", "type": "string" @@ -2672,7 +2939,7 @@ "boundary_kind": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Boundary Kind", "type": "string" @@ -2680,7 +2947,7 @@ "boundary_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Boundary Ref", "type": "string" @@ -2688,7 +2955,7 @@ "capture_requirement_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Capture Requirement Ref", "type": "string" @@ -2696,7 +2963,7 @@ "capture_spec_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Capture Spec Ref", "type": "string" @@ -2708,7 +2975,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2722,7 +2989,7 @@ "channel_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2732,7 +2999,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -2744,7 +3011,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2758,7 +3025,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2768,7 +3035,7 @@ "media_types": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2778,7 +3045,7 @@ "notes": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2792,7 +3059,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2806,7 +3073,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2816,7 +3083,7 @@ "scope": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Scope", "type": "string" @@ -2824,7 +3091,7 @@ "scope_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2838,7 +3105,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -2852,7 +3119,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2866,7 +3133,7 @@ "source_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -2876,7 +3143,7 @@ "trigger_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Trigger Ref", "type": "string" @@ -2884,7 +3151,7 @@ "window": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Window", "type": "string" @@ -2956,34 +3223,115 @@ "title": "ExerciseRole", "type": "string" }, + "ExplicitnessClass": { + "description": "SEM-218 author-intent class for a declaration.", + "enum": [ + "exact", + "constrained", + "open" + ], + "title": "ExplicitnessClass", + "type": "string" + }, + "ExplicitnessProvenance": { + "description": "Where the classified value came from in the SDL lifecycle.", + "enum": [ + "author-declared", + "processor-derived", + "backend-realized" + ], + "title": "ExplicitnessProvenance", + "type": "string" + }, + "ExplicitnessProvenanceRecord": { + "additionalProperties": false, + "description": "Serializable SEM-218 classification for one instantiated model path.", + "properties": { + "classification": { + "$ref": "#/$defs/ExplicitnessClass" + }, + "model_path": { + "maxLength": 4096, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Model Path", + "type": "string" + }, + "parameters": { + "default": [], + "items": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "type": "array" + }, + "title": "Parameters", + "type": "array" + }, + "provenance": { + "$ref": "#/$defs/ExplicitnessProvenance", + "default": "author-declared" + }, + "reason": { + "default": "", + "maxLength": 1024, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Reason", + "type": "string" + } + }, + "required": [ + "model_path", + "classification" + ], + "title": "ExplicitnessProvenanceRecord", + "type": "object" + }, "ExternalMappingLoss": { "additionalProperties": false, "description": "Loss-labeled mapping from an external vocabulary to ACES semantics.", "properties": { "identifier": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Identifier", "type": "string" }, "loss_label": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Loss Label", "type": "string" }, "rationale": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Rationale", "type": "string" }, "system": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "System", "type": "string" @@ -3005,7 +3353,7 @@ "dependencies": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3015,7 +3363,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3023,7 +3371,7 @@ "destination": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Destination", "type": "string" @@ -3031,7 +3379,7 @@ "environment": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3041,7 +3389,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -3063,7 +3411,7 @@ "vulnerabilities": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3098,7 +3446,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -3109,7 +3457,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3117,7 +3465,7 @@ "evidence_reference": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Evidence Reference", "type": "string" @@ -3125,7 +3473,7 @@ "predicate_type": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Predicate Type", "type": "string" @@ -3137,7 +3485,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -3152,7 +3500,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -3194,14 +3542,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -3209,7 +3557,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -3221,7 +3569,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -3243,7 +3591,7 @@ "command": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3260,7 +3608,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3268,7 +3616,7 @@ "entrypoint": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3278,7 +3626,7 @@ "exposed_ports": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3288,7 +3636,7 @@ "labels": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3298,7 +3646,7 @@ "working_directory": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Working Directory", "type": "string" @@ -3314,14 +3662,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "destination_path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Destination Path", "type": "string" @@ -3329,14 +3677,14 @@ "from_stage": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "From Stage", "type": "string" }, "source_path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Path", "type": "string" @@ -3356,14 +3704,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -3371,7 +3719,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -3383,7 +3731,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -3405,7 +3753,7 @@ "created_by": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Created By", "type": "string" @@ -3413,7 +3761,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3421,7 +3769,7 @@ "digest": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Digest", "type": "string" @@ -3433,7 +3781,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3451,7 +3799,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3473,7 +3821,7 @@ "checksum": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Checksum", "type": "string" @@ -3481,7 +3829,7 @@ "checksum_algorithm": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Checksum Algorithm", "type": "string" @@ -3489,7 +3837,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3497,14 +3845,14 @@ "destination_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Destination Path", "type": "string" }, "identifier": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Identifier", "type": "string" @@ -3512,7 +3860,7 @@ "source_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Path", "type": "string" @@ -3536,59 +3884,6 @@ "title": "ImageVerificationStatus", "type": "string" }, - "ImportDecl": { - "additionalProperties": false, - "description": "A module import expanded before full semantic validation.", - "properties": { - "digest": { - "default": "", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Digest", - "type": "string" - }, - "namespace": { - "default": "", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Namespace", - "type": "string" - }, - "parameters": { - "additionalProperties": true, - "title": "Parameters", - "type": "object" - }, - "path": { - "default": "", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Path", - "type": "string" - }, - "source": { - "default": "", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Source", - "type": "string" - }, - "version": { - "default": "*", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Version", - "type": "string" - } - }, - "title": "ImportDecl", - "type": "object" - }, "InfraNode": { "additionalProperties": false, "description": "Deployment parameters for a node.\n\nShorthand: ``node-name: 3`` (just the count).\nLonghand: full dict with count, links, dependencies, properties, acls.", @@ -3607,7 +3902,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -3618,7 +3913,7 @@ "dependencies": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3628,7 +3923,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3636,7 +3931,7 @@ "links": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3652,7 +3947,7 @@ "items": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3678,7 +3973,7 @@ "accounts": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3688,7 +3983,7 @@ "hosts": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3698,7 +3993,7 @@ "services": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3708,7 +4003,7 @@ "subnets": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3726,7 +4021,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3734,7 +4029,7 @@ "environment": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3744,7 +4039,7 @@ "from_entity": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "From Entity", "type": "string" @@ -3752,7 +4047,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -3771,7 +4066,7 @@ "to_entities": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3782,61 +4077,66 @@ "title": "Inject", "type": "object" }, - "ModuleDescriptor": { + "InstantiationProvenance": { "additionalProperties": false, - "description": "Published module metadata for SDL composition.", + "description": "Complete portable derivation context for an instantiated scenario.", "properties": { - "description": { - "default": "", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Description", - "type": "string" + "authored_digest": { + "$ref": "#/$defs/SemanticDigest" }, - "exports": { - "additionalProperties": { - "items": { - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "type": "string" - }, - "type": "array" + "bindings": { + "default": [], + "items": { + "$ref": "#/$defs/ParameterBinding" }, - "title": "Exports", - "type": "object" + "title": "Bindings", + "type": "array" }, - "id": { - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "capability_constraints": { + "default": [], + "items": { + "$ref": "#/$defs/CapabilityConstraint" }, - "title": "Id", - "type": "string" + "title": "Capability Constraints", + "type": "array" }, - "parameters": { + "explicitness": { + "default": [], "items": { - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "type": "string" + "$ref": "#/$defs/ExplicitnessProvenanceRecord" }, - "title": "Parameters", + "title": "Explicitness", "type": "array" }, - "version": { - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "imports": { + "default": [], + "items": { + "$ref": "#/$defs/ResolvedImportProvenance" }, - "title": "Version", - "type": "string" + "title": "Imports", + "type": "array" + }, + "selected_profile": { + "anyOf": [ + { + "maxLength": 256, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Selected Profile" } }, "required": [ - "id", - "version" + "authored_digest" ], - "title": "ModuleDescriptor", + "title": "InstantiationProvenance", "type": "object" }, "Node": { @@ -3857,7 +4157,7 @@ "conditions": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3867,7 +4167,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -3875,7 +4175,7 @@ "features": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3885,7 +4185,7 @@ "injects": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3899,7 +4199,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3913,7 +4213,7 @@ "os_version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Os Version", "type": "string" @@ -3930,8 +4230,17 @@ "default": null }, "roles": { - "additionalProperties": { - "$ref": "#/$defs/Role" + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/Role" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } }, "title": "Roles", "type": "object" @@ -3971,7 +4280,7 @@ "vulnerabilities": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -3994,6 +4303,91 @@ "title": "NodeType", "type": "string" }, + "NumericPredicate": { + "additionalProperties": false, + "description": "Compare a governed numeric property using an explicit unit.", + "properties": { + "expected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "title": "Expected" + }, + "kind": { + "const": "number", + "default": "number", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals", + "less_than", + "less_than_or_equal", + "greater_than", + "greater_than_or_equal" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + }, + "unit": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Unit", + "type": "string" + }, + "unit_semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Unit Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected", + "unit", + "unit_semantic_ref" + ], + "title": "NumericPredicate", + "type": "object" + }, "OSFamily": { "description": "Operating system family. Vocabulary from OCSF Device.os.", "enum": [ @@ -4013,7 +4407,7 @@ "actions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4023,7 +4417,7 @@ "agent": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Agent", "type": "string" @@ -4031,7 +4425,7 @@ "depends_on": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4041,7 +4435,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -4049,7 +4443,7 @@ "entity": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Entity", "type": "string" @@ -4057,7 +4451,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -4068,7 +4462,7 @@ "targets": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4095,32 +4489,45 @@ }, "ObjectiveSuccess": { "additionalProperties": false, - "description": "Declarative success criteria for an objective.\n\nPer ADR-073, objective success references observable state (``conditions``)\nonly. The OCR-inherited scoring pipeline (``metrics`` / ``evaluations`` /\n``tlos`` / ``goals``) was removed from the SDL; graded scoring and reward\nlive in the experiment/evaluator plane (ADR-055/064/069).", + "description": "Declarative success criteria for an objective.\n\nSuccess composes backend-neutral invariant or postcondition assertions.\nProbe implementations, graded scoring, and reward remain outside this\nconstruct.", "properties": { - "conditions": { + "assertions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, - "title": "Conditions", + "title": "Assertions", "type": "array" }, "mode": { "anyOf": [ { - "$ref": "#/$defs/SuccessMode" + "$ref": "#/$defs/TruthCompositionMode" }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } ], "default": "all_of", "title": "Mode" + }, + "threshold": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" } }, "title": "ObjectiveSuccess", @@ -4133,7 +4540,7 @@ "events": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4143,7 +4550,7 @@ "scripts": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4153,7 +4560,7 @@ "steps": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4163,7 +4570,7 @@ "stories": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4173,7 +4580,7 @@ "workflows": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4199,7 +4606,7 @@ "diagnostics": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4209,7 +4616,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4219,7 +4626,7 @@ }, "interpretation_basis": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Interpretation Basis", "type": "string" @@ -4227,7 +4634,7 @@ "limitations": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4237,7 +4644,7 @@ }, "observation_point_basis": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Observation Point Basis", "type": "string" @@ -4247,7 +4654,7 @@ }, "semantic_version": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Semantic Version", "type": "string" @@ -4289,7 +4696,7 @@ "diagnostics": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4299,7 +4706,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4308,7 +4715,7 @@ }, "interpretation_role": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Interpretation Role", "type": "string" @@ -4316,7 +4723,7 @@ "provenance_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4325,14 +4732,14 @@ }, "ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ref", "type": "string" }, "source_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Id", "type": "string" @@ -4378,7 +4785,7 @@ "diagnostics": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4388,7 +4795,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4399,7 +4806,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4413,7 +4820,7 @@ "limitations": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4422,21 +4829,21 @@ }, "ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ref", "type": "string" }, "relation": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Relation", "type": "string" }, "target_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Id", "type": "string" @@ -4469,6 +4876,64 @@ "title": "OutcomeInterpretationTargetLayer", "type": "string" }, + "ParameterBinding": { + "additionalProperties": false, + "description": "One selected parameter value with an unambiguous qualified identity.", + "properties": { + "origin": { + "$ref": "#/$defs/BindingOrigin" + }, + "parameter": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 32, + "minItems": 1, + "title": "Parameter", + "type": "array" + }, + "value": { + "anyOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "title": "Value" + } + }, + "required": [ + "parameter", + "origin", + "value" + ], + "title": "ParameterBinding", + "type": "object" + }, "ParticipantActionContract": { "additionalProperties": false, "description": "Governed semantic contract for one participant action name.", @@ -4500,7 +4965,7 @@ "evidence_expectations": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4523,7 +4988,7 @@ }, "fidelity_claim": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Fidelity Claim", "type": "string" @@ -4542,7 +5007,7 @@ "observation_expectations": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4558,21 +5023,21 @@ }, "procedure_basis": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Procedure Basis", "type": "string" }, "realization_profile": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Realization Profile", "type": "string" }, "semantic_version": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Semantic Version", "type": "string" @@ -4580,7 +5045,7 @@ "state_transition_effects": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4611,7 +5076,7 @@ "properties": { "description": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -4621,7 +5086,7 @@ }, "effect_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Effect Id", "type": "string" @@ -4629,7 +5094,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4639,7 +5104,7 @@ "target_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4681,7 +5146,7 @@ "properties": { "description": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -4689,7 +5154,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4701,7 +5166,7 @@ }, "precondition_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Precondition Id", "type": "string" @@ -4709,7 +5174,7 @@ "support_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4731,14 +5196,14 @@ "properties": { "backend_error_code": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Backend Error Code", "type": "string" }, "diagnostic": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Diagnostic", "type": "string" @@ -4762,7 +5227,7 @@ "affected_temporal_ids": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4772,14 +5237,14 @@ }, "description": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "disclosure_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Disclosure Id", "type": "string" @@ -4790,7 +5255,7 @@ "limitations": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4830,7 +5295,7 @@ "action_contract_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4840,7 +5305,7 @@ "ai_offensive_behavior_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4850,7 +5315,7 @@ "authority_scope_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4860,7 +5325,7 @@ "backend_feature_support_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4871,7 +5336,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4885,7 +5350,7 @@ "evidence_contract_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4895,7 +5360,7 @@ "extension_policy": { "default": "governed-extension", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Extension Policy", "type": "string" @@ -4914,7 +5379,7 @@ "observation_boundary_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4924,7 +5389,7 @@ "offensive_behavior_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4934,7 +5399,7 @@ "outcome_interpretation_rule_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4944,7 +5409,7 @@ "participant_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4954,7 +5419,7 @@ "participant_role_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4965,7 +5430,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -4978,7 +5443,7 @@ }, "semantic_version": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Semantic Version", "type": "string" @@ -5075,7 +5540,7 @@ }, "rationale": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Rationale", "type": "string" @@ -5083,7 +5548,7 @@ "related_actions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5093,7 +5558,7 @@ "shared_state_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5102,7 +5567,7 @@ }, "target": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target", "type": "string" @@ -5123,7 +5588,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5133,7 +5598,7 @@ "hidden_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5142,7 +5607,7 @@ }, "latency_profile": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Latency Profile", "type": "string" @@ -5150,7 +5615,7 @@ "observable_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5160,7 +5625,7 @@ "observer_effects": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5169,7 +5634,7 @@ }, "projection_basis": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Projection Basis", "type": "string" @@ -5178,7 +5643,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5191,7 +5656,7 @@ }, "redaction_policy": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Redaction Policy", "type": "string" @@ -5241,7 +5706,7 @@ "backend_disclosure_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5250,14 +5715,14 @@ }, "clock_authority": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Clock Authority", "type": "string" }, "description": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -5266,7 +5731,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5287,7 +5752,7 @@ }, "ordering_basis": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ordering Basis", "type": "string" @@ -5296,7 +5761,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5311,7 +5776,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5326,7 +5791,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5339,7 +5804,7 @@ }, "temporal_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Temporal Id", "type": "string" @@ -5354,7 +5819,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5470,7 +5935,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5485,7 +5950,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5503,7 +5968,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5517,7 +5982,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5526,7 +5991,7 @@ }, "information_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Information Ref", "type": "string" @@ -5535,7 +6000,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5550,7 +6015,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5563,7 +6028,7 @@ }, "visibility_basis": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Visibility Basis", "type": "string" @@ -5586,7 +6051,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5599,7 +6064,7 @@ }, "certainty": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Certainty", "type": "string" @@ -5608,7 +6073,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5621,7 +6086,7 @@ }, "effective_from": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Effective From", "type": "string" @@ -5633,7 +6098,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5648,14 +6113,14 @@ }, "information_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Information Ref", "type": "string" }, "latency_profile": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Latency Profile", "type": "string" @@ -5664,7 +6129,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5680,7 +6145,7 @@ }, "transition_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Transition Id", "type": "string" @@ -5690,7 +6155,7 @@ }, "trigger": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Trigger", "type": "string" @@ -5736,6 +6201,148 @@ "title": "PasswordStrength", "type": "string" }, + "PresencePredicate": { + "additionalProperties": false, + "description": "Test whether a governed property is present on a subject.", + "properties": { + "kind": { + "const": "presence", + "default": "presence", + "title": "Kind", + "type": "string" + }, + "operator": { + "const": "exists", + "default": "exists", + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref" + ], + "title": "PresencePredicate", + "type": "object" + }, + "Proposition": { + "additionalProperties": false, + "description": "A side-effect-free claim over a finite set of stable SDL subjects.", + "properties": { + "basis": { + "$ref": "#/$defs/PropositionBasis" + }, + "description": { + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Description", + "type": "string" + }, + "evidence_requirements": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "title": "Evidence Requirements", + "type": "array" + }, + "predicate": { + "discriminator": { + "mapping": { + "boolean": "#/$defs/BooleanPredicate", + "number": "#/$defs/NumericPredicate", + "presence": "#/$defs/PresencePredicate", + "string": "#/$defs/StringPredicate" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/PresencePredicate" + }, + { + "$ref": "#/$defs/BooleanPredicate" + }, + { + "$ref": "#/$defs/StringPredicate" + }, + { + "$ref": "#/$defs/NumericPredicate" + } + ], + "title": "Predicate" + }, + "quantifier": { + "$ref": "#/$defs/SubjectQuantifier", + "default": "all" + }, + "subjects": { + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "minItems": 1, + "title": "Subjects", + "type": "array" + }, + "threshold": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" + } + }, + "required": [ + "description", + "subjects", + "basis", + "predicate" + ], + "title": "Proposition", + "type": "object" + }, + "PropositionBasis": { + "description": "Source of facts that may decide a proposition.", + "enum": [ + "declared_state", + "observed_state" + ], + "title": "PropositionBasis", + "type": "string" + }, "Relationship": { "additionalProperties": false, "description": "A typed directed edge between two named scenario elements.\n\nSource and target can reference any named element in the scenario:\nnodes, features, accounts, entities, or infrastructure entries.\nThe validator checks that both endpoints resolve.\n\nThe ``properties`` dict carries type-specific metadata (e.g.,\n``trust_type: parent-child`` for AD trusts, ``protocol: SAML``\nfor federation). It's a flat dict rather than typed sub-models\nbecause relationship properties vary widely and we don't want\nto gate expressiveness on pre-modeling every variant.\n\n``database_access`` and ``mail_access`` are typed exceptions where\nprotocol/auth details need structural validation rather than prose.\n``forwarding_edge``, ``service_integration``, and ``proxy_upstream`` are\nthe same kind of typed exception for, respectively, a forwarding /\nintel-sync agent's inter-node trust edge (SCN-010 \u00a75.7), a platform\nconsumer-to-engine service integration, and a reverse-proxy/gateway\nroute-to-origin upstream hop. Each keeps protocol/auth/topology facts\nstructurally validated and cross-referable rather than buried in prose.", @@ -5754,7 +6361,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -5784,7 +6391,7 @@ "properties": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -5815,14 +6422,14 @@ }, "source": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source", "type": "string" }, "target": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target", "type": "string" @@ -5850,7 +6457,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -5861,7 +6468,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -5869,7 +6476,7 @@ "role_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Role Ref", "type": "string" @@ -5885,7 +6492,7 @@ "crypto_method": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Crypto Method", "type": "string" @@ -5893,7 +6500,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -5905,7 +6512,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -5916,14 +6523,14 @@ "enrollment_identity_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Enrollment Identity Ref", "type": "string" }, "forwarder_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Forwarder Ref", "type": "string" @@ -5935,7 +6542,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -5946,7 +6553,7 @@ "protocol": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -5958,7 +6565,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -5984,7 +6591,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -5995,7 +6602,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6003,7 +6610,7 @@ "domain_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Domain Ref", "type": "string" @@ -6011,7 +6618,7 @@ "listener_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Listener Ref", "type": "string" @@ -6019,7 +6626,7 @@ "mailbox_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Mailbox Ref", "type": "string" @@ -6031,7 +6638,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6046,7 +6653,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6065,7 +6672,7 @@ "body_limit": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Body Limit", "type": "string" @@ -6077,7 +6684,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6091,7 +6698,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6103,7 +6710,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6116,7 +6723,7 @@ }, "route_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Route Ref", "type": "string" @@ -6124,7 +6731,7 @@ "upstream_node_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Upstream Node Ref", "type": "string" @@ -6132,7 +6739,7 @@ "upstream_service_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Upstream Service Ref", "type": "string" @@ -6151,7 +6758,7 @@ "auth_principal_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Auth Principal Ref", "type": "string" @@ -6159,7 +6766,7 @@ "consumer_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Consumer Ref", "type": "string" @@ -6167,7 +6774,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6179,7 +6786,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6194,7 +6801,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6208,7 +6815,7 @@ "engine_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Engine Ref", "type": "string" @@ -6220,7 +6827,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6270,6 +6877,141 @@ "title": "RelationshipType", "type": "string" }, + "ResolvedImportProvenance": { + "additionalProperties": false, + "description": "Portable evidence emitted after one trusted module resolution.", + "properties": { + "bindings": { + "default": [], + "items": { + "$ref": "#/$defs/ParameterBinding" + }, + "title": "Bindings", + "type": "array" + }, + "content_digest": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Content Digest", + "type": "string" + }, + "export_hash": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Export Hash", + "type": "string" + }, + "manifest_digest": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Manifest Digest", + "type": "string" + }, + "module_id": { + "maxLength": 129, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Module Id", + "type": "string" + }, + "module_version": { + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Module Version", + "type": "string" + }, + "namespace": { + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 31, + "minItems": 1, + "title": "Namespace", + "type": "array" + }, + "requested_digest": { + "default": "", + "maxLength": 71, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Requested Digest", + "type": "string" + }, + "requested_source": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Requested Source", + "type": "string" + }, + "requested_version": { + "default": "*", + "maxLength": 256, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Requested Version", + "type": "string" + }, + "resolved_source": { + "maxLength": 4096, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Resolved Source", + "type": "string" + }, + "signer_id": { + "default": "", + "maxLength": 256, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "title": "Signer Id", + "type": "string" + } + }, + "required": [ + "namespace", + "requested_source", + "module_id", + "module_version", + "resolved_source" + ], + "title": "ResolvedImportProvenance", + "type": "object" + }, "Resources": { "additionalProperties": false, "description": "Compute resources for a VM node.", @@ -6281,7 +7023,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6296,7 +7038,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6319,7 +7061,7 @@ "entities": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6328,7 +7070,7 @@ }, "username": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Username", "type": "string" @@ -6345,10 +7087,19 @@ "description": "Application-internal RBAC store inventory for a single owning spine.", "properties": { "app_authorization_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "App Authorization Id", + "pattern": "^[a-z0-9]", "type": "string" }, "auth_enabled": { @@ -6358,7 +7109,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6372,7 +7123,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6380,7 +7131,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -6406,7 +7157,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6459,7 +7210,7 @@ "actions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6469,7 +7220,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6481,7 +7232,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6490,10 +7241,19 @@ "title": "Effect" }, "grant_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Grant Id", + "pattern": "^[a-z0-9]", "type": "string" }, "resource_kind": { @@ -6503,7 +7263,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6514,7 +7274,7 @@ "resource_patterns": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6524,7 +7284,7 @@ "role_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Role Ref", "type": "string" @@ -6552,7 +7312,7 @@ "backend_roles": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6566,7 +7326,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6577,7 +7337,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6589,7 +7349,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6607,7 +7367,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6618,16 +7378,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "principal_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Principal Id", + "pattern": "^[a-z0-9]", "type": "string" }, "reserved": { @@ -6637,7 +7406,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6688,7 +7457,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6696,16 +7465,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "role_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Role Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -6722,7 +7500,7 @@ "backend_roles": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6732,7 +7510,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6740,7 +7518,7 @@ "hosts": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6748,16 +7526,25 @@ "type": "array" }, "mapping_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Mapping Id", + "pattern": "^[a-z0-9]", "type": "string" }, "role_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Role Ref", "type": "string" @@ -6765,7 +7552,7 @@ "users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6786,7 +7573,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6794,16 +7581,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "tenant_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Tenant Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -6820,7 +7616,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6828,7 +7624,7 @@ "disclosure": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Disclosure", "type": "string" @@ -6840,7 +7636,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6855,7 +7651,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -6869,7 +7665,7 @@ "trigger": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Trigger", "type": "string" @@ -6885,14 +7681,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -6904,7 +7700,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6915,7 +7711,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -6934,7 +7730,7 @@ "data_type": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Data Type", "type": "string" @@ -6942,7 +7738,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -6954,7 +7750,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -6964,7 +7760,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -6976,7 +7772,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7030,7 +7826,7 @@ "condition": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Condition", "type": "string" @@ -7038,7 +7834,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -7050,7 +7846,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7063,7 +7859,7 @@ }, "target": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target", "type": "string" @@ -7082,7 +7878,7 @@ "content_type": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Content Type", "type": "string" @@ -7090,7 +7886,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -7102,7 +7898,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -7127,7 +7923,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7141,7 +7937,7 @@ "auth_scheme": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Auth Scheme", "type": "string" @@ -7149,7 +7945,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -7171,7 +7967,7 @@ "methods": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7181,7 +7977,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -7195,7 +7991,7 @@ }, "path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" @@ -7216,7 +8012,7 @@ }, "route_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Route Id", "type": "string" @@ -7228,7 +8024,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7242,7 +8038,7 @@ "static_assets": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7252,7 +8048,7 @@ "templates": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7273,7 +8069,7 @@ "vulnerability_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7308,7 +8104,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -7319,7 +8115,7 @@ "target_node_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Node Ref", "type": "string" @@ -7327,7 +8123,7 @@ "target_service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Service", "type": "string" @@ -7339,7 +8135,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7359,16 +8155,25 @@ "description": "An observed application surface hosted by a transport service on a node.\n\n``service`` references the owning same-node ``Node.services[].name`` (bare\nname or the qualified ``nodes..services.`` form). The surface is\nobservation metadata; it never mutates ``Node.services``.", "properties": { "application_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Application Id", + "pattern": "^[a-z0-9]", "type": "string" }, "base_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Base Path", "type": "string" @@ -7376,7 +8181,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -7384,7 +8189,7 @@ "framework": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Framework", "type": "string" @@ -7392,7 +8197,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -7404,7 +8209,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -7422,7 +8227,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -7450,7 +8255,7 @@ "add": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7460,7 +8265,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -7468,7 +8273,7 @@ "drop": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7478,7 +8283,7 @@ "effective": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7495,7 +8300,7 @@ "required": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7586,7 +8391,32 @@ }, "forwarding_agents": { "items": { - "$ref": "#/$defs/RuntimeForwardingAgent" + "allOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgent" + }, + { + "properties": { + "forwarding_agent_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "type": "object" + } + ] }, "title": "Forwarding Agents", "type": "array" @@ -7780,7 +8610,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7794,7 +8624,7 @@ "cgroup_parent": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Cgroup Parent", "type": "string" @@ -7802,7 +8632,7 @@ "command": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7812,7 +8642,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -7820,7 +8650,7 @@ "device_cgroup_rules": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7837,7 +8667,7 @@ "dns": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7847,7 +8677,7 @@ "dns_options": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7857,7 +8687,7 @@ "dns_search": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7867,7 +8697,7 @@ "entrypoint": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7884,7 +8714,7 @@ "group_add": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7905,7 +8735,7 @@ "log_driver": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Log Driver", "type": "string" @@ -7913,7 +8743,7 @@ "log_options": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7923,7 +8753,7 @@ "masked_paths": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7948,7 +8778,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7966,7 +8796,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7980,7 +8810,7 @@ "read_only_paths": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -7994,7 +8824,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8008,7 +8838,7 @@ "runtime_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Runtime Name", "type": "string" @@ -8016,7 +8846,7 @@ "seccomp_profile": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Seccomp Profile", "type": "string" @@ -8024,7 +8854,7 @@ "security_opt": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8038,7 +8868,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8100,7 +8930,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -8111,7 +8941,7 @@ "bind_source": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Bind Source", "type": "string" @@ -8123,7 +8953,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -8133,7 +8963,7 @@ }, "control_interface_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Control Interface Id", "type": "string" @@ -8141,7 +8971,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -8153,7 +8983,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -8163,7 +8993,7 @@ }, "path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" @@ -8171,7 +9001,7 @@ "protocol": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -8212,10 +9042,19 @@ "description": "An observed database service hosted by a transport service on a node.\n\n``service`` references the owning same-node ``Node.services[].name`` (bare\nname or the qualified ``nodes..services.`` form). The inventory\nis observation metadata; it never mutates ``Node.services``.", "properties": { "database_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Database Service Id", + "pattern": "^[a-z0-9]", "type": "string" }, "databases": { @@ -8228,7 +9067,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -8240,7 +9079,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -8265,7 +9104,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -8277,7 +9116,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -8295,7 +9134,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -8310,7 +9149,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -8328,7 +9167,7 @@ "properties": { "cluster_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Cluster Id", "type": "string" @@ -8336,7 +9175,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -8344,7 +9183,7 @@ "discovery_mode": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Discovery Mode", "type": "string" @@ -8356,7 +9195,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8370,7 +9209,7 @@ "health": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Health", "type": "string" @@ -8378,7 +9217,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -8386,7 +9225,7 @@ "native_protocol_version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Native Protocol Version", "type": "string" @@ -8398,7 +9237,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8412,7 +9251,7 @@ "partitioner": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Partitioner", "type": "string" @@ -8424,7 +9263,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8442,7 +9281,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8460,7 +9299,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8474,7 +9313,7 @@ "uuid": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Uuid", "type": "string" @@ -8522,7 +9361,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -8530,22 +9369,31 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "plugin_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Plugin Id", + "pattern": "^[a-z0-9]", "type": "string" }, "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -8585,7 +9433,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8599,7 +9447,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -8607,7 +9455,7 @@ "dynamic_policy": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Dynamic Policy", "type": "string" @@ -8619,7 +9467,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8633,7 +9481,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8648,7 +9496,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -8664,7 +9512,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8676,16 +9524,25 @@ "title": "Leaf Field Count" }, "mapping_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Mapping Id", + "pattern": "^[a-z0-9]", "type": "string" }, "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -8693,7 +9550,7 @@ "partition_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Partition Ref", "type": "string" @@ -8701,7 +9558,7 @@ "schema_digest": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Schema Digest", "type": "string" @@ -8713,7 +9570,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8738,7 +9595,7 @@ "build_hash": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Build Hash", "type": "string" @@ -8746,7 +9603,7 @@ "build_type": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Build Type", "type": "string" @@ -8754,7 +9611,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -8769,7 +9626,7 @@ "engine_version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Engine Version", "type": "string" @@ -8781,7 +9638,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8799,7 +9656,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8817,7 +9674,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8835,7 +9692,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8849,16 +9706,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "node_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Node Id", + "pattern": "^[a-z0-9]", "type": "string" }, "plugins": { @@ -8876,7 +9742,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -8899,7 +9765,7 @@ "address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -8907,16 +9773,25 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "endpoint_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Endpoint Id", + "pattern": "^[a-z0-9]", "type": "string" }, "port": { @@ -8926,7 +9801,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -8940,7 +9815,7 @@ "protocol": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -8952,7 +9827,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9001,7 +9876,7 @@ "creation_timestamp": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Creation Timestamp", "type": "string" @@ -9014,7 +9889,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9026,7 +9901,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -9038,7 +9913,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9056,7 +9931,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9074,7 +9949,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9088,7 +9963,7 @@ "health": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Health", "type": "string" @@ -9100,7 +9975,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9111,7 +9986,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -9119,16 +9994,25 @@ "open_closed_status": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Open Closed Status", "type": "string" }, "partition_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Partition Id", + "pattern": "^[a-z0-9]", "type": "string" }, "per_dc_factor_map": { @@ -9139,7 +10023,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9155,7 +10039,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9173,7 +10057,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9191,7 +10075,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9206,7 +10090,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9224,7 +10108,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9238,7 +10122,7 @@ "uuid": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Uuid", "type": "string" @@ -9274,7 +10158,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9288,7 +10172,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -9300,7 +10184,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9311,14 +10195,14 @@ "maxmemory": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Maxmemory", "type": "string" }, "persistence_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Persistence Id", "type": "string" @@ -9326,7 +10210,7 @@ "rdb_save_points": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9358,7 +10242,7 @@ "aliases": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9368,7 +10252,7 @@ "authorization_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Authorization Ref", "type": "string" @@ -9376,7 +10260,7 @@ "backup_targets": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9401,7 +10285,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9410,16 +10294,25 @@ "title": "Data Model" }, "datastore_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Datastore Service Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -9431,7 +10324,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9442,7 +10335,7 @@ "ingest_pipelines": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9452,7 +10345,7 @@ "lifecycle_policies": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9469,7 +10362,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -9502,7 +10395,7 @@ "protocol": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -9510,7 +10403,7 @@ "pubsub_channels": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9520,7 +10413,7 @@ "queues_streams": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9530,7 +10423,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -9563,7 +10456,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -9586,7 +10479,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9597,7 +10490,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -9605,7 +10498,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -9617,7 +10510,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9632,7 +10525,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9641,16 +10534,25 @@ "title": "Scope" }, "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Setting Id", + "pattern": "^[a-z0-9]", "type": "string" }, "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -9694,7 +10596,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -9702,7 +10604,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9712,7 +10614,7 @@ "index_patterns": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9722,7 +10624,7 @@ "mapping_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Mapping Ref", "type": "string" @@ -9730,7 +10632,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -9740,7 +10642,7 @@ "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9758,16 +10660,25 @@ "template_digest": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Template Digest", "type": "string" }, "template_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Template Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -9788,7 +10699,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9802,7 +10713,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -9814,7 +10725,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -9829,7 +10740,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9842,7 +10753,7 @@ }, "transport_security_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Transport Security Id", "type": "string" @@ -9870,7 +10781,7 @@ "properties": { "ecosystem": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ecosystem", "type": "string" @@ -9878,7 +10789,7 @@ "format": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Format", "type": "string" @@ -9886,14 +10797,14 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" @@ -9901,7 +10812,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -9920,7 +10831,7 @@ "properties": { "container_path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Container Path", "type": "string" @@ -9928,14 +10839,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "host_path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Host Path", "type": "string" @@ -9943,7 +10854,7 @@ "permissions": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Permissions", "type": "string" @@ -9963,7 +10874,7 @@ "configuration_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -9973,16 +10884,25 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "dns_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Dns Service Id", + "pattern": "^[a-z0-9]", "type": "string" }, "dynamic_update": { @@ -10003,7 +10923,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10014,7 +10934,7 @@ "log_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10024,7 +10944,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -10048,7 +10968,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10060,7 +10980,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -10075,7 +10995,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -10114,14 +11034,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -10133,7 +11053,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10144,7 +11064,7 @@ "source": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source", "type": "string" @@ -10152,7 +11072,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -10164,7 +11084,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10199,7 +11119,7 @@ "properties": { "address": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -10207,14 +11127,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "hostname": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Hostname", "type": "string" @@ -10248,7 +11168,7 @@ "backend": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Backend", "type": "string" @@ -10256,16 +11176,25 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "file_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "File Service Id", + "pattern": "^[a-z0-9]", "type": "string" }, "principals": { @@ -10282,7 +11211,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10293,7 +11222,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -10367,7 +11296,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10382,7 +11311,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10393,16 +11322,25 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "observation_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Observation Id", + "pattern": "^[a-z0-9]", "type": "string" }, "outcome": { @@ -10412,7 +11350,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10422,7 +11360,7 @@ }, "resource_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Resource Ref", "type": "string" @@ -10434,7 +11372,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10444,7 +11382,7 @@ }, "subject_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Subject Ref", "type": "string" @@ -10482,7 +11420,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10497,7 +11435,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10508,7 +11446,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -10520,7 +11458,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10530,21 +11468,30 @@ }, "resource_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Resource Ref", "type": "string" }, "rule_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Rule Id", + "pattern": "^[a-z0-9]", "type": "string" }, "subject_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Subject Ref", "type": "string" @@ -10583,7 +11530,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10594,7 +11541,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -10602,7 +11549,7 @@ "directory_subject_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Directory Subject Ref", "type": "string" @@ -10610,7 +11557,7 @@ "external_id": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "External Id", "type": "string" @@ -10622,7 +11569,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10633,14 +11580,14 @@ "local_user_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Local User Ref", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -10652,7 +11599,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10661,10 +11608,19 @@ "title": "Origin" }, "principal_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Principal Id", + "pattern": "^[a-z0-9]", "type": "string" }, "status": { @@ -10674,7 +11630,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10757,7 +11713,7 @@ "backing_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Backing Path", "type": "string" @@ -10769,7 +11725,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10783,7 +11739,7 @@ "comment": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Comment", "type": "string" @@ -10791,7 +11747,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -10803,7 +11759,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10817,7 +11773,7 @@ "invalid_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10831,7 +11787,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10841,7 +11797,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -10853,7 +11809,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10865,16 +11821,25 @@ "title": "Read Only" }, "share_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Share Id", + "pattern": "^[a-z0-9]", "type": "string" }, "valid_groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10884,7 +11849,7 @@ "valid_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10894,7 +11859,7 @@ "write_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10928,7 +11893,7 @@ "content_digest": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Content Digest", "type": "string" @@ -10936,7 +11901,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -10944,7 +11909,7 @@ "digest_algorithm": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Digest Algorithm", "type": "string" @@ -10956,7 +11921,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -10971,7 +11936,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -10985,7 +11950,7 @@ "mode": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Mode", "type": "string" @@ -10993,7 +11958,7 @@ "owner_group": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Owner Group", "type": "string" @@ -11001,14 +11966,14 @@ "owner_user": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Owner User", "type": "string" }, "path": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" @@ -11020,7 +11985,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11031,7 +11996,7 @@ "provenance": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Provenance", "type": "string" @@ -11043,7 +12008,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11058,7 +12023,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11072,7 +12037,7 @@ "source_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Path", "type": "string" @@ -11084,7 +12049,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11099,7 +12064,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11170,7 +12135,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11192,14 +12157,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "forwarding_agent_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Forwarding Agent Id", "type": "string" @@ -11211,7 +12176,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11222,7 +12187,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -11265,7 +12230,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -11325,7 +12290,7 @@ "properties": { "buffer_policy_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Buffer Policy Id", "type": "string" @@ -11337,7 +12302,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11348,7 +12313,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -11360,7 +12325,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11378,7 +12343,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11396,7 +12361,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11467,7 +12432,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -11479,7 +12444,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11488,16 +12453,25 @@ "title": "Kind" }, "reload_channel_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Reload Channel Id", + "pattern": "^[a-z0-9]", "type": "string" }, "target_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Ref", "type": "string" @@ -11534,7 +12508,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11545,7 +12519,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -11553,7 +12527,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -11565,7 +12539,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11574,16 +12548,25 @@ "title": "Provenance" }, "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Setting Id", + "pattern": "^[a-z0-9]", "type": "string" }, "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -11625,7 +12608,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -11637,7 +12620,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11652,7 +12635,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11670,7 +12653,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11688,7 +12671,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11697,16 +12680,25 @@ "title": "Protocol" }, "target_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Target Id", + "pattern": "^[a-z0-9]", "type": "string" }, "target_node_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Node Ref", "type": "string" @@ -11714,7 +12706,7 @@ "target_service_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Service Ref", "type": "string" @@ -11733,7 +12725,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -11745,7 +12737,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11756,7 +12748,7 @@ "location": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Location", "type": "string" @@ -11768,7 +12760,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11779,16 +12771,25 @@ "selector": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Selector", "type": "string" }, "source_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Source Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -11817,7 +12818,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -11829,7 +12830,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11840,16 +12841,25 @@ "sid_namespace": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Sid Namespace", "type": "string" }, "transform_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Transform Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -11878,7 +12888,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -11890,7 +12900,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11915,7 +12925,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11947,7 +12957,7 @@ "end": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "End", "type": "string" @@ -11959,7 +12969,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -11973,7 +12983,7 @@ "output": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Output", "type": "string" @@ -11985,7 +12995,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -11996,7 +13006,7 @@ "start": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Start", "type": "string" @@ -12012,14 +13022,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -12031,7 +13041,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12046,7 +13056,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12061,7 +13071,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12072,7 +13082,7 @@ "values": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12093,7 +13103,7 @@ "base_dn": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Base Dn", "type": "string" @@ -12101,7 +13111,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12109,22 +13119,31 @@ "domain_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Domain Name", "type": "string" }, "identity_authority_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Identity Authority Id", + "pattern": "^[a-z0-9]", "type": "string" }, "issuer": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Issuer", "type": "string" @@ -12136,7 +13155,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12147,7 +13166,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -12155,7 +13174,7 @@ "namespace": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Namespace", "type": "string" @@ -12170,7 +13189,7 @@ "realm": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Realm", "type": "string" @@ -12199,7 +13218,7 @@ "tenant_id": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Tenant Id", "type": "string" @@ -12251,7 +13270,7 @@ "address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -12259,7 +13278,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12271,7 +13290,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12289,7 +13308,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12300,16 +13319,25 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" }, "service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Service Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -12326,7 +13354,7 @@ "applies_to_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12336,7 +13364,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12344,16 +13372,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "policy_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Policy Id", + "pattern": "^[a-z0-9]", "type": "string" }, "policy_kind": { @@ -12363,7 +13400,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12438,7 +13475,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12446,16 +13483,25 @@ "external_target": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "External Target", "type": "string" }, "relationship_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Relationship Id", + "pattern": "^[a-z0-9]", "type": "string" }, "relationship_type": { @@ -12465,7 +13511,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12475,7 +13521,7 @@ }, "source_ref": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Ref", "type": "string" @@ -12483,7 +13529,7 @@ "target_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Ref", "type": "string" @@ -12528,7 +13574,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12536,7 +13582,7 @@ "display_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Display Name", "type": "string" @@ -12544,7 +13590,7 @@ "distinguished_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Distinguished Name", "type": "string" @@ -12552,7 +13598,7 @@ "domain": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Domain", "type": "string" @@ -12564,7 +13610,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12582,7 +13628,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12592,7 +13638,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -12604,7 +13650,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12615,7 +13661,7 @@ "principal_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Principal Name", "type": "string" @@ -12623,7 +13669,7 @@ "service_principal_names": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12631,10 +13677,19 @@ "type": "array" }, "subject_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Subject Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -12670,7 +13725,7 @@ "argv": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12684,7 +13739,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12695,7 +13750,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12707,7 +13762,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12721,7 +13776,7 @@ "executable_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Executable Path", "type": "string" @@ -12729,7 +13784,7 @@ "implementation": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Implementation", "type": "string" @@ -12741,7 +13796,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12808,7 +13863,7 @@ "criteria": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Criteria", "type": "string" @@ -12816,7 +13871,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12824,7 +13879,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12834,7 +13889,7 @@ "probe": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Probe", "type": "string" @@ -12864,7 +13919,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12876,7 +13931,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12890,7 +13945,7 @@ "members": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -12899,7 +13954,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -12911,7 +13966,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12933,7 +13988,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12970,7 +14025,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -12982,7 +14037,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -12993,7 +14048,7 @@ "gecos": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Gecos", "type": "string" @@ -13001,7 +14056,7 @@ "home": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Home", "type": "string" @@ -13013,7 +14068,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13028,7 +14083,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13043,7 +14098,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13057,7 +14112,7 @@ "primary_group": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Primary Group", "type": "string" @@ -13069,7 +14124,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13080,7 +14135,7 @@ "shell": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Shell", "type": "string" @@ -13092,7 +14147,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13103,7 +14158,7 @@ "supplemental_groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13117,7 +14172,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13130,7 +14185,7 @@ }, "username": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Username", "type": "string" @@ -13149,22 +14204,31 @@ "address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" }, "alias_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Alias Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -13172,7 +14236,7 @@ "domain_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Domain Ref", "type": "string" @@ -13180,7 +14244,7 @@ "external_targets": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13190,7 +14254,7 @@ "target_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13228,16 +14292,25 @@ "description": "A mail-service engine/component such as Postfix, Dovecot, or a filter.", "properties": { "component_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Component Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -13249,7 +14322,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13259,7 +14332,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -13267,7 +14340,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -13322,21 +14395,30 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "domain_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Domain Id", + "pattern": "^[a-z0-9]", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -13348,7 +14430,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13386,7 +14468,7 @@ "advertised_identity": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Advertised Identity", "type": "string" @@ -13399,7 +14481,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13411,7 +14493,7 @@ "banner": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Banner", "type": "string" @@ -13419,7 +14501,7 @@ "capabilities": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13429,7 +14511,7 @@ "component_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Component Ref", "type": "string" @@ -13437,16 +14519,25 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "listener_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Listener Id", + "pattern": "^[a-z0-9]", "type": "string" }, "protocol": { @@ -13456,7 +14547,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13471,7 +14562,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13482,7 +14573,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -13494,7 +14585,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13505,7 +14596,7 @@ "tls_versions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13543,14 +14634,14 @@ "account_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Account Ref", "type": "string" }, "address": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -13563,7 +14654,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13579,7 +14670,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13590,7 +14681,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -13598,7 +14689,7 @@ "domain_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Domain Ref", "type": "string" @@ -13606,7 +14697,7 @@ "local_part": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Local Part", "type": "string" @@ -13614,16 +14705,25 @@ "local_user_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Local User Ref", "type": "string" }, "mailbox_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Mailbox Id", + "pattern": "^[a-z0-9]", "type": "string" }, "role": { @@ -13633,7 +14733,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13648,7 +14748,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13659,7 +14759,7 @@ "store_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Store Ref", "type": "string" @@ -13708,7 +14808,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -13720,7 +14820,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13731,16 +14831,25 @@ "path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" }, "store_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Store Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -13790,7 +14899,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -13802,7 +14911,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13817,7 +14926,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -13831,16 +14940,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "queue_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Queue Id", + "pattern": "^[a-z0-9]", "type": "string" }, "stability": { @@ -13850,7 +14968,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13918,7 +15036,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -13930,7 +15048,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -13941,22 +15059,31 @@ "relay_host": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Relay Host", "type": "string" }, "rule_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Rule Id", + "pattern": "^[a-z0-9]", "type": "string" }, "source_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Ref", "type": "string" @@ -13964,7 +15091,7 @@ "target_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Ref", "type": "string" @@ -13997,7 +15124,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -14012,7 +15139,7 @@ "engine": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Engine", "type": "string" @@ -14025,10 +15152,19 @@ "type": "array" }, "mail_service_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Mail Service Id", + "pattern": "^[a-z0-9]", "type": "string" }, "mailbox_stores": { @@ -14048,7 +15184,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -14070,7 +15206,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -14085,7 +15221,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -14104,7 +15240,7 @@ "component_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Component Ref", "type": "string" @@ -14112,14 +15248,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -14131,7 +15267,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14140,16 +15276,25 @@ "title": "Provenance" }, "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Setting Id", + "pattern": "^[a-z0-9]", "type": "string" }, "source_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Path", "type": "string" @@ -14157,7 +15302,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -14169,7 +15314,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14295,7 +15440,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14309,7 +15454,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -14317,7 +15462,7 @@ "filesystem_type": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Filesystem Type", "type": "string" @@ -14325,7 +15470,7 @@ "options": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14339,7 +15484,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14354,7 +15499,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14369,7 +15514,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14380,7 +15525,7 @@ "source": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source", "type": "string" @@ -14392,7 +15537,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14407,7 +15552,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14422,7 +15567,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14432,7 +15577,7 @@ }, "target": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target", "type": "string" @@ -14479,7 +15624,7 @@ "cgroup": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Cgroup", "type": "string" @@ -14487,7 +15632,7 @@ "ipc": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ipc", "type": "string" @@ -14495,7 +15640,7 @@ "pid": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Pid", "type": "string" @@ -14503,7 +15648,7 @@ "userns": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Userns", "type": "string" @@ -14511,7 +15656,7 @@ "uts": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Uts", "type": "string" @@ -14527,7 +15672,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -14539,7 +15684,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14550,7 +15695,7 @@ "driver_options": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14560,7 +15705,7 @@ "ipam_driver": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ipam Driver", "type": "string" @@ -14568,7 +15713,7 @@ "ipam_options": { "additionalProperties": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14628,7 +15773,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14647,7 +15792,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14657,16 +15802,25 @@ "type": "array" }, "channel_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Channel Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -14678,7 +15832,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14689,7 +15843,7 @@ "path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" @@ -14697,7 +15851,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -14736,7 +15890,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14748,7 +15902,7 @@ "configuration_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14765,7 +15919,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -14777,7 +15931,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14788,7 +15942,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14802,7 +15956,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14813,7 +15967,7 @@ "log_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14823,16 +15977,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "network_detection_engine_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Network Detection Engine Id", + "pattern": "^[a-z0-9]", "type": "string" }, "network_sets": { @@ -14852,7 +16015,7 @@ "process_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Process Ref", "type": "string" @@ -14860,7 +16023,7 @@ "revision": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Revision", "type": "string" @@ -14875,7 +16038,7 @@ "sensor_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Sensor Ref", "type": "string" @@ -14883,7 +16046,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -14954,7 +16117,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -14966,7 +16129,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -14977,7 +16140,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -14985,7 +16148,7 @@ "network_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -14995,7 +16158,7 @@ "selector_values": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15003,10 +16166,19 @@ "type": "array" }, "set_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Set Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -15054,7 +16226,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15066,7 +16238,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15085,7 +16257,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15101,7 +16273,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15112,16 +16284,25 @@ "path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Path", "type": "string" }, "stream_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Stream Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -15156,7 +16337,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15164,7 +16345,7 @@ "file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15178,7 +16359,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15189,7 +16370,7 @@ "generated_by": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Generated By", "type": "string" @@ -15201,7 +16382,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15216,7 +16397,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15230,7 +16411,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -15242,7 +16423,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15254,10 +16435,19 @@ "title": "Rule Count" }, "source_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Source Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -15305,7 +16495,7 @@ "aliases": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15330,7 +16520,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15344,7 +16534,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15352,7 +16542,7 @@ "dns_names": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15362,7 +16552,7 @@ "endpoint_id": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Endpoint Id", "type": "string" @@ -15374,7 +16564,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15385,7 +16575,7 @@ "gateway": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Gateway", "type": "string" @@ -15393,7 +16583,7 @@ "generated_dns_names": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15403,7 +16593,7 @@ "ip_address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ip Address", "type": "string" @@ -15415,7 +16605,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15429,14 +16619,14 @@ "mac_address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Mac Address", "type": "string" }, "network": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Network", "type": "string" @@ -15444,7 +16634,7 @@ "network_id": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Network Id", "type": "string" @@ -15456,7 +16646,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15489,7 +16679,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15497,7 +16687,7 @@ "domainname": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Domainname", "type": "string" @@ -15512,7 +16702,7 @@ "hostname": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Hostname", "type": "string" @@ -15535,7 +16725,7 @@ "capture_interfaces": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15549,7 +16739,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15560,7 +16750,7 @@ "configuration_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15570,7 +16760,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15578,7 +16768,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15592,7 +16782,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15603,7 +16793,7 @@ "log_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15613,7 +16803,7 @@ "monitored_network_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -15627,7 +16817,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15638,22 +16828,31 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "network_sensor_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Network Sensor Id", + "pattern": "^[a-z0-9]", "type": "string" }, "process_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Process Ref", "type": "string" @@ -15661,7 +16860,7 @@ "revision": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Revision", "type": "string" @@ -15673,7 +16872,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15684,7 +16883,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -15759,7 +16958,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15782,7 +16981,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15801,7 +17000,7 @@ "control_interface_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Control Interface Ref", "type": "string" @@ -15809,7 +17008,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15821,7 +17020,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15832,7 +17031,7 @@ "engine_api_version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Engine Api Version", "type": "string" @@ -15851,16 +17050,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "orchestration_authority_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Orchestration Authority Id", + "pattern": "^[a-z0-9]", "type": "string" }, "privilege_class": { @@ -15870,7 +17078,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -15931,7 +17139,7 @@ "cleanup": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Cleanup", "type": "string" @@ -15939,7 +17147,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -15947,7 +17155,7 @@ "execution_timeout": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Execution Timeout", "type": "string" @@ -15955,7 +17163,7 @@ "timeout": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Timeout", "type": "string" @@ -15986,7 +17194,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -16000,7 +17208,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16008,7 +17216,7 @@ "evidence_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Evidence Ref", "type": "string" @@ -16016,16 +17224,25 @@ "image_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Image Ref", "type": "string" }, "workload_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Workload Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -16042,7 +17259,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16050,7 +17267,7 @@ "environment_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Environment Name", "type": "string" @@ -16058,7 +17275,7 @@ "organization_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Organization Ref", "type": "string" @@ -16074,7 +17291,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16082,7 +17299,7 @@ "image_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Image Ref", "type": "string" @@ -16090,16 +17307,25 @@ "purpose": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Purpose", "type": "string" }, "template_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Template Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -16116,21 +17342,21 @@ "architecture": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Architecture", "type": "string" }, "manager": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Manager", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -16138,7 +17364,7 @@ "purl": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Purl", "type": "string" @@ -16146,14 +17372,14 @@ "source": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source", "type": "string" }, "version": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -16174,7 +17400,7 @@ "advisory_url": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Advisory Url", "type": "string" @@ -16182,49 +17408,49 @@ "fixed_version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Fixed Version", "type": "string" }, "id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Id", "type": "string" }, "image_digest": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Image Digest", "type": "string" }, "installed_version": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Installed Version", "type": "string" }, "package_name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Package Name", "type": "string" }, "scan_time": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Scan Time", "type": "string" }, "scanner": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Scanner", "type": "string" @@ -16232,7 +17458,7 @@ "scanner_database": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Scanner Database", "type": "string" @@ -16240,7 +17466,7 @@ "scanner_version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Scanner Version", "type": "string" @@ -16252,7 +17478,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16292,7 +17518,7 @@ "authorization_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Authorization Ref", "type": "string" @@ -16314,7 +17540,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16340,7 +17566,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -16353,10 +17579,19 @@ "type": "array" }, "platform_application_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Platform Application Id", + "pattern": "^[a-z0-9]", "type": "string" }, "platform_kind": { @@ -16366,7 +17601,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16377,7 +17612,7 @@ "product": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Product", "type": "string" @@ -16385,7 +17620,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -16414,7 +17649,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -16431,10 +17666,19 @@ "description": "A connector/integration wired into the platform.\n\nA connector never carries a raw credential value; its credential posture is\nrecorded purely via :attr:`credential_classification`.", "properties": { "connector_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Connector Id", + "pattern": "^[a-z0-9]", "type": "string" }, "credential_classification": { @@ -16444,7 +17688,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16455,7 +17699,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16467,7 +17711,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -16485,7 +17729,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16496,7 +17740,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -16532,16 +17776,25 @@ "type": "object" }, "content_object_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Content Object Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16549,7 +17802,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -16563,7 +17816,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16574,7 +17827,7 @@ "marking_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -16584,7 +17837,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -16592,7 +17845,7 @@ "references": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -16638,7 +17891,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16646,7 +17899,7 @@ "job_timeout": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Job Timeout", "type": "string" @@ -16658,7 +17911,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -16672,7 +17925,7 @@ "policy_id": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Policy Id", "type": "string" @@ -16680,7 +17933,7 @@ "rate_limit": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Rate Limit", "type": "string" @@ -16688,7 +17941,7 @@ "runner": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Runner", "type": "string" @@ -16718,7 +17971,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16726,16 +17979,25 @@ "level": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Level", "type": "string" }, "marking_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Marking Id", + "pattern": "^[a-z0-9]", "type": "string" }, "scheme": { @@ -16745,7 +18007,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16756,7 +18018,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -16785,7 +18047,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16793,16 +18055,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "organization_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Organization Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -16823,7 +18094,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16834,7 +18105,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16842,7 +18113,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -16854,7 +18125,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16865,22 +18136,31 @@ "redaction": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Redaction", "type": "string" }, "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Setting Id", + "pattern": "^[a-z0-9]", "type": "string" }, "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -16923,7 +18203,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16931,16 +18211,25 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" }, "tenant_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Tenant Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -16955,16 +18244,25 @@ "description": "An outbound binding to an upstream node/service (data source, backend).", "properties": { "binding_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Binding Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -16976,7 +18274,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -16987,7 +18285,7 @@ "target_node_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Node Ref", "type": "string" @@ -16995,7 +18293,7 @@ "target_service_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Target Service Ref", "type": "string" @@ -17028,7 +18326,7 @@ "add": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17038,7 +18336,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -17046,7 +18344,7 @@ "drop": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17056,7 +18354,7 @@ "effective": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17070,7 +18368,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17095,7 +18393,7 @@ "command": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17109,7 +18407,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17120,7 +18418,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -17128,7 +18426,7 @@ "group": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Group", "type": "string" @@ -17136,7 +18434,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -17148,7 +18446,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17166,7 +18464,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17184,7 +18482,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17195,7 +18493,7 @@ "user": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "User", "type": "string" @@ -17203,7 +18501,7 @@ "working_directory": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Working Directory", "type": "string" @@ -17237,7 +18535,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17247,7 +18545,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -17255,7 +18553,7 @@ "host_ip": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Host Ip", "type": "string" @@ -17267,7 +18565,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17281,7 +18579,7 @@ "protocol": { "default": "tcp", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -17304,7 +18602,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17314,7 +18612,7 @@ "host_ip": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Host Ip", "type": "string" @@ -17326,7 +18624,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17340,7 +18638,7 @@ "protocol": { "default": "tcp", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -17363,7 +18661,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17377,7 +18675,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -17389,7 +18687,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17407,7 +18705,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17425,7 +18723,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17443,7 +18741,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17478,7 +18776,7 @@ "command_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Command Ref", "type": "string" @@ -17486,7 +18784,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -17498,7 +18796,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17512,7 +18810,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -17540,10 +18838,19 @@ "default": null }, "scheduled_job_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Scheduled Job Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -17576,7 +18883,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17587,7 +18894,7 @@ "last_run": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Last Run", "type": "string" @@ -17595,7 +18902,7 @@ "next_run": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Next Run", "type": "string" @@ -17615,7 +18922,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17633,7 +18940,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17644,7 +18951,7 @@ "spec": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Spec", "type": "string" @@ -17670,22 +18977,31 @@ "address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" }, "agent_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Agent Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -17693,7 +19009,7 @@ "group_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17702,7 +19018,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -17710,7 +19026,7 @@ "node_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Node Ref", "type": "string" @@ -17718,7 +19034,7 @@ "os": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Os", "type": "string" @@ -17730,7 +19046,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17741,7 +19057,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -17761,7 +19077,7 @@ "configuration_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17771,22 +19087,31 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "group_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Group Id", + "pattern": "^[a-z0-9]", "type": "string" }, "member_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17796,7 +19121,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -17828,16 +19153,25 @@ "description": "A manager daemon, module, or internal component.", "properties": { "component_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Component Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -17849,7 +19183,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -17867,7 +19201,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17877,7 +19211,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -17885,7 +19219,7 @@ "process_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Process Ref", "type": "string" @@ -17897,7 +19231,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -17993,16 +19327,25 @@ "description": "A manager-owned rule, decoder, policy, list, or query corpus.", "properties": { "content_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Content Id", + "pattern": "^[a-z0-9]", "type": "string" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -18014,7 +19357,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18028,7 +19371,7 @@ "file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18042,7 +19385,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18057,7 +19400,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18072,7 +19415,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18086,7 +19429,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -18105,7 +19448,7 @@ "canonical_digest": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Canonical Digest", "type": "string" @@ -18113,7 +19456,7 @@ "compliance_tags": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18123,7 +19466,7 @@ "content_set_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Content Set Ref", "type": "string" @@ -18131,7 +19474,7 @@ "decoded_as": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18141,7 +19484,7 @@ "decoder_fields": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18151,7 +19494,7 @@ "decoder_names": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18159,10 +19502,19 @@ "type": "array" }, "definition_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Definition Id", + "pattern": "^[a-z0-9]", "type": "string" }, "definition_kind": { @@ -18172,7 +19524,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18183,7 +19535,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -18191,7 +19543,7 @@ "digest_algorithm": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Digest Algorithm", "type": "string" @@ -18203,7 +19555,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18221,7 +19573,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18232,7 +19584,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18253,7 +19605,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18267,7 +19619,7 @@ "groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18277,7 +19629,7 @@ "if_matched_sid_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18287,7 +19639,7 @@ "if_sid_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18301,7 +19653,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18319,7 +19671,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18333,7 +19685,7 @@ "match_strings": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18343,7 +19695,7 @@ "mitre_attack_ids": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18353,7 +19705,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -18361,7 +19713,7 @@ "native_id": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Native Id", "type": "string" @@ -18369,7 +19721,7 @@ "parent_definition_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18383,7 +19735,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18397,7 +19749,7 @@ "regex_patterns": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18407,7 +19759,7 @@ "same_source_constraints": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18417,7 +19769,7 @@ "severity": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Severity", "type": "string" @@ -18425,7 +19777,7 @@ "source_artifact_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Artifact Ref", "type": "string" @@ -18437,7 +19789,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18451,7 +19803,7 @@ "source_file_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source File Ref", "type": "string" @@ -18463,7 +19815,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18477,7 +19829,7 @@ "tactic_labels": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18487,7 +19839,7 @@ "tags": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18497,7 +19849,7 @@ "target_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18507,7 +19859,7 @@ "technique_labels": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18521,7 +19873,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18582,14 +19934,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "field": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Field", "type": "string" @@ -18601,7 +19953,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18612,7 +19964,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -18667,7 +20019,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18681,22 +20033,31 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "listener_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Listener Id", + "pattern": "^[a-z0-9]", "type": "string" }, "protocol": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -18708,7 +20069,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18719,7 +20080,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -18731,7 +20092,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18793,7 +20154,7 @@ "configuration_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18810,7 +20171,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -18825,7 +20186,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18839,7 +20200,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18857,7 +20218,7 @@ "log_file_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -18871,7 +20232,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18882,7 +20243,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -18890,22 +20251,31 @@ "revision": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Revision", "type": "string" }, "security_monitoring_manager_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Security Monitoring Manager Id", + "pattern": "^[a-z0-9]", "type": "string" }, "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -18920,7 +20290,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -18955,7 +20325,7 @@ "component_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Component Ref", "type": "string" @@ -18963,14 +20333,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -18982,7 +20352,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -18991,16 +20361,25 @@ "title": "Provenance" }, "setting_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Setting Id", + "pattern": "^[a-z0-9]", "type": "string" }, "source_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Source Path", "type": "string" @@ -19008,7 +20387,7 @@ "value": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -19020,7 +20399,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19071,7 +20450,7 @@ "address": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Address", "type": "string" @@ -19083,7 +20462,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19094,7 +20473,7 @@ "bind_interface": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Bind Interface", "type": "string" @@ -19102,7 +20481,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -19110,7 +20489,7 @@ "evidence_refs": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19124,7 +20503,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19138,7 +20517,7 @@ "process_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Process Name", "type": "string" @@ -19146,7 +20525,7 @@ "process_ref": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Process Ref", "type": "string" @@ -19158,7 +20537,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19173,7 +20552,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19206,7 +20585,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19217,22 +20596,31 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" }, "service_listener_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Service Listener Id", + "pattern": "^[a-z0-9]", "type": "string" }, "socket_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Socket Path", "type": "string" @@ -19250,7 +20638,7 @@ "properties": { "component_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Component Id", "type": "string" @@ -19262,7 +20650,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19273,7 +20661,7 @@ "cpe": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Cpe", "type": "string" @@ -19281,7 +20669,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -19289,7 +20677,7 @@ "ecosystem": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Ecosystem", "type": "string" @@ -19304,7 +20692,7 @@ "installed_paths": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19314,14 +20702,14 @@ "manifest_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Manifest Path", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -19329,7 +20717,7 @@ "package_manager": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Package Manager", "type": "string" @@ -19337,7 +20725,7 @@ "package_name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Package Name", "type": "string" @@ -19345,7 +20733,7 @@ "package_version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Package Version", "type": "string" @@ -19357,7 +20745,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19368,7 +20756,7 @@ "purl": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Purl", "type": "string" @@ -19376,7 +20764,7 @@ "version": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -19395,14 +20783,14 @@ "properties": { "algorithm": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Algorithm", "type": "string" }, "value": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Value", "type": "string" @@ -19460,7 +20848,7 @@ "accept_env": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19470,7 +20858,7 @@ "allow_groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19480,7 +20868,7 @@ "allow_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19490,7 +20878,7 @@ "authentication_methods": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19500,7 +20888,7 @@ "authorized_keys_file": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Authorized Keys File", "type": "string" @@ -19508,7 +20896,7 @@ "chroot_directory": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Chroot Directory", "type": "string" @@ -19516,7 +20904,7 @@ "deny_groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19526,7 +20914,7 @@ "deny_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19536,7 +20924,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -19566,7 +20954,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19584,7 +20972,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19602,7 +20990,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19615,16 +21003,25 @@ }, "service": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" }, "ssh_server_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Ssh Server Id", + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -19657,7 +21054,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19668,7 +21065,7 @@ "commands": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19678,7 +21075,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -19686,7 +21083,7 @@ "host_scope": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Host Scope", "type": "string" @@ -19698,7 +21095,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19708,7 +21105,7 @@ }, "principal": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Principal", "type": "string" @@ -19720,7 +21117,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19731,7 +21128,7 @@ "raw_entry": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Raw Entry", "type": "string" @@ -19739,7 +21136,7 @@ "run_as_groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19749,7 +21146,7 @@ "run_as_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19765,12 +21162,12 @@ }, "Script": { "additionalProperties": false, - "description": "A timed sequence of events.\n\nTime values are human-readable duration strings parsed to seconds.", + "description": "A timed sequence of human-readable durations parsed to seconds.", "properties": { "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -19782,7 +21179,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19797,7 +21194,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19810,7 +21207,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -19822,7 +21219,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19836,7 +21233,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19853,6 +21250,37 @@ "title": "Script", "type": "object" }, + "SemanticDigest": { + "additionalProperties": false, + "description": "Profile-labelled digest of one expanded authoring scenario.", + "properties": { + "algorithm": { + "const": "sha256", + "title": "Algorithm", + "type": "string" + }, + "profile": { + "const": "aces-sdl-semantic/v1", + "title": "Profile", + "type": "string" + }, + "value": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Value", + "type": "string" + } + }, + "required": [ + "profile", + "algorithm", + "value" + ], + "title": "SemanticDigest", + "type": "object" + }, "ServiceManagerKind": { "description": "Kind of service manager whose unit inventory is being recorded.\n\nInitially scoped to ``systemd``; ``other`` is the escape hatch for managers\nthat have not yet been promoted to their own typed enum value (per\nADR-035 \u00a7 5).", "enum": [ @@ -19874,7 +21302,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19885,7 +21313,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -19897,7 +21325,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19923,7 +21351,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19941,7 +21369,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19956,7 +21384,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -19974,7 +21402,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -19989,7 +21417,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20000,7 +21428,7 @@ "service": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Service", "type": "string" @@ -20008,7 +21436,7 @@ "status_text": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Status Text", "type": "string" @@ -20016,7 +21444,7 @@ "sub_state": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Sub State", "type": "string" @@ -20024,21 +21452,21 @@ "unit_file_path": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Unit File Path", "type": "string" }, "unit_id": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Unit Id", "type": "string" }, "unit_name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Unit Name", "type": "string" @@ -20050,7 +21478,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20073,18 +21501,35 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { + "anyOf": [ + { + "const": "" + }, + { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + ], "default": "", - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "title": "Name", - "type": "string" + "title": "Name" }, "port": { "anyOf": [ @@ -20093,7 +21538,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20103,7 +21548,7 @@ "protocol": { "default": "tcp", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Protocol", "type": "string" @@ -20155,7 +21600,7 @@ "command": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Command", "type": "string" @@ -20167,7 +21612,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20182,7 +21627,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20193,7 +21638,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -20272,14 +21717,14 @@ "properties": { "cidr": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Cidr", "type": "string" }, "gateway": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Gateway", "type": "string" @@ -20291,7 +21736,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20324,7 +21769,7 @@ }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -20332,7 +21777,7 @@ "version": { "default": "*", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -20351,7 +21796,7 @@ "command": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Command", "type": "string" @@ -20363,7 +21808,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20378,7 +21823,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20389,7 +21834,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -20419,7 +21864,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20428,7 +21873,7 @@ }, "pattern": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Pattern", "type": "string" @@ -20463,7 +21908,7 @@ "accept_env": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20473,7 +21918,7 @@ "allow_groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20483,7 +21928,7 @@ "allow_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20493,7 +21938,7 @@ "authentication_methods": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20503,7 +21948,7 @@ "authorized_keys_file": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Authorized Keys File", "type": "string" @@ -20511,7 +21956,7 @@ "chroot_directory": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Chroot Directory", "type": "string" @@ -20526,7 +21971,7 @@ "deny_groups": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20536,7 +21981,7 @@ "deny_users": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20546,7 +21991,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -20563,10 +22008,19 @@ "default": null }, "match_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, - "title": "Match Id", + "pattern": "^[a-z0-9]", "type": "string" }, "password_authentication": { @@ -20576,7 +22030,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20594,7 +22048,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20612,7 +22066,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20637,7 +22091,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -20645,7 +22099,7 @@ "name": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -20653,7 +22107,7 @@ "scripts": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20668,7 +22122,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20683,98 +22137,94 @@ "title": "Story", "type": "object" }, - "SuccessMode": { - "description": "How referenced success criteria combine.", - "enum": [ - "all_of", - "any_of" - ], - "title": "SuccessMode", - "type": "string" - }, - "Variable": { + "StringPredicate": { "additionalProperties": false, - "description": "A named variable for scenario parameterization.\n\nVariables define configurable parameters with types, defaults,\nand optional value constraints. They're referenced in other\nsections via ``${variable_name}`` syntax.", + "description": "Compare a governed string property with typed string operands.", "properties": { - "allowed_values": { - "items": { - "anyOf": [ - { - "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" - }, - "type": "string" - }, - { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "boolean" - } - ] - }, - "title": "Allowed Values", - "type": "array" - }, - "default": { + "expected": { "anyOf": [ { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" + "items": { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + }, + "type": "array" } ], - "default": null, - "title": "Default" + "title": "Expected" }, - "description": { - "default": "", + "kind": { + "const": "string", + "default": "string", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals", + "in", + "not_in" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, - "title": "Description", + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", "type": "string" }, - "required": { - "default": false, - "title": "Required", - "type": "boolean" - }, - "type": { - "$ref": "#/$defs/VariableType" + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" } }, "required": [ - "type" + "property", + "semantic_ref", + "expected" ], - "title": "Variable", + "title": "StringPredicate", "type": "object" }, - "VariableType": { - "description": "Data type of a variable.", + "SubjectQuantifier": { + "description": "Quantification over a proposition's finite, resolved subject set.", + "enum": [ + "all", + "any", + "at_least" + ], + "title": "SubjectQuantifier", + "type": "string" + }, + "TruthCompositionMode": { + "description": "Portable composition over assertion outcomes.", "enum": [ - "string", - "integer", - "boolean", - "number" + "all_of", + "any_of", + "at_least" ], - "title": "VariableType", + "title": "TruthCompositionMode", "type": "string" }, "Vulnerability": { @@ -20783,21 +22233,21 @@ "properties": { "class": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Class", "type": "string" }, "description": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "name": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Name", "type": "string" @@ -20809,7 +22259,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -20844,23 +22294,32 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "start": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Start", "type": "string" }, "steps": { - "additionalProperties": { - "$ref": "#/$defs/WorkflowStep" - }, "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/WorkflowStep" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, "title": "Steps", "type": "object" }, @@ -20923,7 +22382,7 @@ "order": { "default": "reverse_completion", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Order", "type": "string" @@ -20944,22 +22403,22 @@ }, "WorkflowPredicate": { "additionalProperties": false, - "description": "Branch predicate over observable state, objectives, and prior step state.\n\nPer ADR-073 the OCR scoring references (``metrics`` / ``evaluations`` /\n``tlos`` / ``goals``) were removed; a predicate branches on observable\n``conditions``, declared ``objectives``, and prior workflow ``steps``.", + "description": "Typed branch predicate over assertions, objectives, and prior step state.", "properties": { - "conditions": { + "assertions": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, - "title": "Conditions", + "title": "Assertions", "type": "array" }, "objectives": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20984,7 +22443,7 @@ "branches": { "items": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -20998,18 +22457,18 @@ "title": "Cases", "type": "array" }, - "compensate-with": { + "compensate_with": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, - "title": "Compensate-With", + "title": "Compensate With", "type": "string" }, "default": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Default", "type": "string" @@ -21017,7 +22476,7 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -21025,7 +22484,7 @@ "else": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Else", "type": "string" @@ -21033,19 +22492,19 @@ "join": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Join", "type": "string" }, - "max-attempts": { + "max_attempts": { "anyOf": [ { "type": "integer" }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -21054,12 +22513,12 @@ } ], "default": null, - "title": "Max-Attempts" + "title": "Max Attempts" }, "next": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Next", "type": "string" @@ -21067,39 +22526,39 @@ "objective": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Objective", "type": "string" }, - "on-exhausted": { + "on_exhausted": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, - "title": "On-Exhausted", + "title": "On Exhausted", "type": "string" }, - "on-failure": { + "on_failure": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, - "title": "On-Failure", + "title": "On Failure", "type": "string" }, - "on-success": { + "on_success": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, - "title": "On-Success", + "title": "On Success", "type": "string" }, "then": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Then", "type": "string" @@ -21121,7 +22580,7 @@ "workflow": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Workflow", "type": "string" @@ -21147,14 +22606,14 @@ "additionalProperties": false, "description": "Predicate reference to previously observed workflow step state.", "properties": { - "min-attempts": { + "min_attempts": { "anyOf": [ { "type": "integer" }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" }, @@ -21163,7 +22622,7 @@ } ], "default": null, - "title": "Min-Attempts" + "title": "Min Attempts" }, "outcomes": { "items": { @@ -21175,7 +22634,7 @@ }, "step": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Step", "type": "string" @@ -21210,14 +22669,14 @@ "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" }, "next": { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Next", "type": "string" @@ -21244,7 +22703,7 @@ }, { "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "type": "string" } @@ -21262,12 +22721,21 @@ "$id": "https://aces.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\nmarginally stricter than the runtime instantiation engine in one\npathological case: if a resolved variable *value* itself re-introduces a\nliteral ``${name}`` sequence (single-pass substitution does not re-scan\nit), the schema and this validator treat it as non-concrete and reject it.", + "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.", "properties": { "accounts": { "additionalProperties": { "$ref": "#/$defs/Account" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Accounts", "type": "object" }, @@ -21275,6 +22743,15 @@ "additionalProperties": { "$ref": "#/$defs/ParticipantActionContract" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Action Contracts", "type": "object" }, @@ -21282,13 +22759,47 @@ "additionalProperties": { "$ref": "#/$defs/Agent" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Agents", "type": "object" }, + "assertions": { + "additionalProperties": { + "$ref": "#/$defs/Assertion" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Assertions", + "type": "object" + }, "behavior_specifications": { "additionalProperties": { "$ref": "#/$defs/ParticipantBehaviorSpecification" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Behavior Specifications", "type": "object" }, @@ -21296,6 +22807,15 @@ "additionalProperties": { "$ref": "#/$defs/Condition" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Conditions", "type": "object" }, @@ -21303,13 +22823,22 @@ "additionalProperties": { "$ref": "#/$defs/Content" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Content", "type": "object" }, "description": { "default": "", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Description", "type": "string" @@ -21318,6 +22847,15 @@ "additionalProperties": { "$ref": "#/$defs/Entity" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Entities", "type": "object" }, @@ -21325,6 +22863,15 @@ "additionalProperties": { "$ref": "#/$defs/Event" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Events", "type": "object" }, @@ -21332,6 +22879,15 @@ "additionalProperties": { "$ref": "#/$defs/EvidenceRequirement" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Evidence Requirements", "type": "object" }, @@ -21339,27 +22895,63 @@ "additionalProperties": { "$ref": "#/$defs/Feature" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Features", "type": "object" }, "forwarding_agents": { "items": { - "$ref": "#/$defs/RuntimeForwardingAgent" + "allOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgent" + }, + { + "properties": { + "forwarding_agent_id": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + } + }, + "type": "object" + } + ] }, "title": "Forwarding Agents", "type": "array" }, - "imports": { - "items": { - "$ref": "#/$defs/ImportDecl" - }, - "title": "Imports", - "type": "array" - }, "infrastructure": { "additionalProperties": { "$ref": "#/$defs/InfraNode" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Infrastructure", "type": "object" }, @@ -21367,24 +22959,36 @@ "additionalProperties": { "$ref": "#/$defs/Inject" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Injects", "type": "object" }, - "module": { - "anyOf": [ - { - "$ref": "#/$defs/ModuleDescriptor" - }, + "instantiation_provenance": { + "$ref": "#/$defs/InstantiationProvenance", + "x-aces-realization-dimension": false + }, + "name": { + "allOf": [ { - "type": "null" + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } } ], - "default": null - }, - "name": { + "maxLength": 64, + "minLength": 1, "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "[^a-z0-9_-]" }, + "pattern": "^[a-z0-9]", "title": "Name", "type": "string" }, @@ -21392,6 +22996,15 @@ "additionalProperties": { "$ref": "#/$defs/Node" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,34}$", + "type": "string" + }, "title": "Nodes", "type": "object" }, @@ -21399,6 +23012,15 @@ "additionalProperties": { "$ref": "#/$defs/Objective" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Objectives", "type": "object" }, @@ -21406,6 +23028,15 @@ "additionalProperties": { "$ref": "#/$defs/ParticipantObservationBoundary" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Observation Boundaries", "type": "object" }, @@ -21413,13 +23044,47 @@ "additionalProperties": { "$ref": "#/$defs/OutcomeInterpretationRule" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Outcome Interpretation Rules", "type": "object" }, + "propositions": { + "additionalProperties": { + "$ref": "#/$defs/Proposition" + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, + "title": "Propositions", + "type": "object" + }, "relationships": { "additionalProperties": { "$ref": "#/$defs/Relationship" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Relationships", "type": "object" }, @@ -21427,6 +23092,15 @@ "additionalProperties": { "$ref": "#/$defs/Script" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Scripts", "type": "object" }, @@ -21434,23 +23108,22 @@ "additionalProperties": { "$ref": "#/$defs/Story" }, - "title": "Stories", - "type": "object" - }, - "variables": { - "additionalProperties": false, - "patternProperties": { - "^[A-Za-z_][A-Za-z0-9_-]*$": { - "$ref": "#/$defs/Variable" - } + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" }, - "title": "Variables", + "title": "Stories", "type": "object" }, "version": { "default": "*", "not": { - "pattern": "\\$\\{([A-Za-z_][A-Za-z0-9_-]*)\\}" + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" }, "title": "Version", "type": "string" @@ -21459,6 +23132,15 @@ "additionalProperties": { "$ref": "#/$defs/Vulnerability" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Vulnerabilities", "type": "object" }, @@ -21466,13 +23148,25 @@ "additionalProperties": { "$ref": "#/$defs/Workflow" }, + "propertyNames": { + "maxLength": 2048, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63}$", + "type": "string" + }, "title": "Workflows", "type": "object" } }, "required": [ - "name" + "name", + "instantiation_provenance" ], - "title": "InstantiatedScenario", - "type": "object" + "title": "SDL Instantiated Scenario v1", + "type": "object", + "x-aces-authored-identity-profile": "aces-sdl-semantic/v1", + "x-aces-document-phase": "instantiated-scenario" } diff --git a/contracts/schemas/sdl/scenario-instantiation-request-v1.json b/contracts/schemas/sdl/scenario-instantiation-request-v1.json index 34d487bc6..89e7bd017 100644 --- a/contracts/schemas/sdl/scenario-instantiation-request-v1.json +++ b/contracts/schemas/sdl/scenario-instantiation-request-v1.json @@ -5,6 +5,15 @@ "properties": { "parameters": { "additionalProperties": true, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Parameters", "type": "object" }, diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index 12631467b..1ce9bed71 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -41,9 +41,22 @@ "type": "string" }, "name": { + "anyOf": [ + { + "const": "" + }, + { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + ], "default": "", - "title": "Name", - "type": "string" + "title": "Name" }, "ports": { "items": { @@ -156,7 +169,7 @@ }, "Agent": { "additionalProperties": false, - "description": "An autonomous participant in the scenario.\n\nAgents reference existing scenario elements:\n\n- ``entity`` links to the entities section (team/role) and supplies\n identity and role per ADR-020\n- ``starting_accounts`` links to the accounts section\n- ``allowed_subnets`` links to infrastructure entries\n- ``initial_knowledge`` references nodes and infrastructure\n- ``starting_conditions`` links to the conditions section, giving the\n authoring surface a declarative hook for participant-relevant\n precondition checks (ACT-601)\n- ``authority_anchors`` links to declared SDL elements (entities,\n relationships, content, etc.) that anchor what the participant is\n allowed or expected to do in scenario meaning (ACT-601, ADR-020)\n- ``operating_scope`` links to targetable named scenario elements\n (subnets, hosts, services, content) defining where the participant\n may act or observe (ACT-601, ADR-020)\n- ``observation_boundaries`` links to declared participant observation\n boundaries that define participant-specific projections of world and\n evidence state (SEM-208)\n\nPer ADR-073 the CybORG-inherited ``reward_calculator`` label was removed;\nit was an unbound, unvalidated string and graded reward lives in the\nexperiment/evaluator plane (ADR-055/064/069).", + "description": "An autonomous participant in the scenario.\n\nAgents reference existing scenario elements:\n\n- ``entity`` links to the entities section (team/role) and supplies\n identity and role per ADR-020\n- ``starting_accounts`` links to the accounts section\n- ``allowed_subnets`` links to infrastructure entries\n- ``initial_knowledge`` references nodes and infrastructure\n- ``starting_assertions`` links to precondition assertions, giving the\n authoring surface a declarative hook for participant-relevant starting\n state without equating a probe implementation with truth (ACT-601)\n- ``authority_anchors`` links to declared SDL elements (entities,\n relationships, content, etc.) that anchor what the participant is\n allowed or expected to do in scenario meaning (ACT-601, ADR-020)\n- ``operating_scope`` links to targetable named scenario elements\n (subnets, hosts, services, content) defining where the participant\n may act or observe (ACT-601, ADR-020)\n- ``observation_boundaries`` links to declared participant observation\n boundaries that define participant-specific projections of world and\n evidence state (SEM-208)\n\nPer ADR-073 the CybORG-inherited ``reward_calculator`` label was removed;\nit was an unbound, unvalidated string and graded reward lives in the\nexperiment/evaluator plane (ADR-055/064/069).", "properties": { "actions": { "items": { @@ -221,17 +234,64 @@ "title": "Starting Accounts", "type": "array" }, - "starting_conditions": { + "starting_assertions": { "items": { "type": "string" }, - "title": "Starting Conditions", + "title": "Starting Assertions", "type": "array" } }, "title": "Agent", "type": "object" }, + "Assertion": { + "additionalProperties": false, + "description": "A typed use of one proposition at a governed semantic boundary.", + "properties": { + "description": { + "default": "", + "title": "Description", + "type": "string" + }, + "polarity": { + "$ref": "#/$defs/AssertionPolarity", + "default": "positive" + }, + "proposition": { + "title": "Proposition", + "type": "string" + }, + "role": { + "$ref": "#/$defs/AssertionRole" + } + }, + "required": [ + "proposition", + "role" + ], + "title": "Assertion", + "type": "object" + }, + "AssertionPolarity": { + "description": "Expected polarity of the referenced proposition.", + "enum": [ + "positive", + "negative" + ], + "title": "AssertionPolarity", + "type": "string" + }, + "AssertionRole": { + "description": "Boundary at which an assertion constrains its owning construct.", + "enum": [ + "precondition", + "invariant", + "postcondition" + ], + "title": "AssertionRole", + "type": "string" + }, "AssetValue": { "additionalProperties": false, "description": "CIA triad asset valuation for scoring and risk assessment.", @@ -319,9 +379,55 @@ } ] }, + "BooleanPredicate": { + "additionalProperties": false, + "description": "Compare a governed Boolean property with an expected Boolean.", + "properties": { + "expected": { + "title": "Expected", + "type": "boolean" + }, + "kind": { + "const": "boolean", + "default": "boolean", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected" + ], + "title": "BooleanPredicate", + "type": "object" + }, "Condition": { "additionalProperties": false, - "description": "A monitoring check deployed to a VM.\n\nEither ``command`` + ``interval`` or ``source`` must be set, not both.", + "description": "A probe implementation deployed to a VM.\n\nEither ``command`` + ``interval`` or ``source`` must be set, not both.", "properties": { "command": { "anyOf": [ @@ -367,6 +473,11 @@ "title": "Name", "type": "string" }, + "proposition": { + "default": "", + "title": "Proposition", + "type": "string" + }, "retries": { "anyOf": [ { @@ -609,7 +720,18 @@ "title": "Description", "type": "string" }, + "display_name": { + "default": "", + "title": "Display Name", + "type": "string" + }, "name": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "title": "Name", "type": "string" }, @@ -642,7 +764,12 @@ "description": "An observed logical database within a database service.", "properties": { "database_id": { - "title": "Database Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -1473,7 +1600,12 @@ "type": "array" }, "rrset_id": { - "title": "Rrset Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "ttl": { @@ -1839,7 +1971,12 @@ "type": "array" }, "zone_id": { - "title": "Zone Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -2032,8 +2169,17 @@ "type": "string" }, "entities": { - "additionalProperties": { - "$ref": "#/$defs/Entity" + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/Entity" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } }, "title": "Entities", "type": "object" @@ -2090,13 +2236,13 @@ }, "Event": { "additionalProperties": false, - "description": "A triggered action combining conditions and injects.", + "description": "A triggered action combining assertion preconditions and injects.", "properties": { - "conditions": { + "assertions": { "items": { "type": "string" }, - "title": "Conditions", + "title": "Assertions", "type": "array" }, "description": { @@ -2864,7 +3010,16 @@ "type": "string" }, "parameters": { - "additionalProperties": true, + "patternProperties": { + "^[a-z0-9]": {} + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, "title": "Parameters", "type": "object" }, @@ -3063,6 +3218,12 @@ }, "parameters": { "items": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "title": "Parameters", @@ -3153,8 +3314,17 @@ "default": null }, "roles": { - "additionalProperties": { - "$ref": "#/$defs/Role" + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/Role" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } }, "title": "Roles", "type": "object" @@ -3214,6 +3384,79 @@ "title": "NodeType", "type": "string" }, + "NumericPredicate": { + "additionalProperties": false, + "description": "Compare a governed numeric property using an explicit unit.", + "properties": { + "expected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "title": "Expected" + }, + "kind": { + "const": "number", + "default": "number", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals", + "less_than", + "less_than_or_equal", + "greater_than", + "greater_than_or_equal" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + }, + "unit": { + "maxLength": 512, + "minLength": 1, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Unit", + "type": "string" + }, + "unit_semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Unit Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected", + "unit", + "unit_semantic_ref" + ], + "title": "NumericPredicate", + "type": "object" + }, "OSFamily": { "description": "Operating system family. Vocabulary from OCSF Device.os.", "enum": [ @@ -3294,19 +3537,19 @@ }, "ObjectiveSuccess": { "additionalProperties": false, - "description": "Declarative success criteria for an objective.\n\nPer ADR-073, objective success references observable state (``conditions``)\nonly. The OCR-inherited scoring pipeline (``metrics`` / ``evaluations`` /\n``tlos`` / ``goals``) was removed from the SDL; graded scoring and reward\nlive in the experiment/evaluator plane (ADR-055/064/069).", + "description": "Declarative success criteria for an objective.\n\nSuccess composes backend-neutral invariant or postcondition assertions.\nProbe implementations, graded scoring, and reward remain outside this\nconstruct.", "properties": { - "conditions": { + "assertions": { "items": { "type": "string" }, - "title": "Conditions", + "title": "Assertions", "type": "array" }, "mode": { "anyOf": [ { - "$ref": "#/$defs/SuccessMode" + "$ref": "#/$defs/TruthCompositionMode" }, { "type": "string" @@ -3314,6 +3557,19 @@ ], "default": "all_of", "title": "Mode" + }, + "threshold": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" } }, "title": "ObjectiveSuccess", @@ -4632,6 +4888,133 @@ "title": "PasswordStrength", "type": "string" }, + "PresencePredicate": { + "additionalProperties": false, + "description": "Test whether a governed property is present on a subject.", + "properties": { + "kind": { + "const": "presence", + "default": "presence", + "title": "Kind", + "type": "string" + }, + "operator": { + "const": "exists", + "default": "exists", + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref" + ], + "title": "PresencePredicate", + "type": "object" + }, + "Proposition": { + "additionalProperties": false, + "description": "A side-effect-free claim over a finite set of stable SDL subjects.", + "properties": { + "basis": { + "$ref": "#/$defs/PropositionBasis" + }, + "description": { + "minLength": 1, + "title": "Description", + "type": "string" + }, + "evidence_requirements": { + "items": { + "type": "string" + }, + "title": "Evidence Requirements", + "type": "array" + }, + "predicate": { + "discriminator": { + "mapping": { + "boolean": "#/$defs/BooleanPredicate", + "number": "#/$defs/NumericPredicate", + "presence": "#/$defs/PresencePredicate", + "string": "#/$defs/StringPredicate" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/PresencePredicate" + }, + { + "$ref": "#/$defs/BooleanPredicate" + }, + { + "$ref": "#/$defs/StringPredicate" + }, + { + "$ref": "#/$defs/NumericPredicate" + } + ], + "title": "Predicate" + }, + "quantifier": { + "$ref": "#/$defs/SubjectQuantifier", + "default": "all" + }, + "subjects": { + "items": { + "type": "string" + }, + "minItems": 1, + "title": "Subjects", + "type": "array" + }, + "threshold": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" + } + }, + "required": [ + "description", + "subjects", + "basis", + "predicate" + ], + "title": "Proposition", + "type": "object" + }, + "PropositionBasis": { + "description": "Source of facts that may decide a proposition.", + "enum": [ + "declared_state", + "observed_state" + ], + "title": "PropositionBasis", + "type": "string" + }, "Relationship": { "additionalProperties": false, "description": "A typed directed edge between two named scenario elements.\n\nSource and target can reference any named element in the scenario:\nnodes, features, accounts, entities, or infrastructure entries.\nThe validator checks that both endpoints resolve.\n\nThe ``properties`` dict carries type-specific metadata (e.g.,\n``trust_type: parent-child`` for AD trusts, ``protocol: SAML``\nfor federation). It's a flat dict rather than typed sub-models\nbecause relationship properties vary widely and we don't want\nto gate expressiveness on pre-modeling every variant.\n\n``database_access`` and ``mail_access`` are typed exceptions where\nprotocol/auth details need structural validation rather than prose.\n``forwarding_edge``, ``service_integration``, and ``proxy_upstream`` are\nthe same kind of typed exception for, respectively, a forwarding /\nintel-sync agent's inter-node trust edge (SCN-010 \u00a75.7), a platform\nconsumer-to-engine service integration, and a reverse-proxy/gateway\nroute-to-origin upstream hop. Each keeps protocol/auth/topology facts\nstructurally validated and cross-referable rather than buried in prose.", @@ -5121,7 +5504,12 @@ "description": "Application-internal RBAC store inventory for a single owning spine.", "properties": { "app_authorization_id": { - "title": "App Authorization Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "auth_enabled": { @@ -5242,7 +5630,12 @@ "title": "Effect" }, "grant_id": { - "title": "Grant Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "resource_kind": { @@ -5346,7 +5739,12 @@ "type": "string" }, "principal_id": { - "title": "Principal Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "reserved": { @@ -5412,7 +5810,12 @@ "type": "string" }, "role_id": { - "title": "Role Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -5446,7 +5849,12 @@ "type": "array" }, "mapping_id": { - "title": "Mapping Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "role_ref": { @@ -5483,7 +5891,12 @@ "type": "string" }, "tenant_id": { - "title": "Tenant Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -5931,7 +6344,12 @@ "description": "An observed application surface hosted by a transport service on a node.\n\n``service`` references the owning same-node ``Node.services[].name`` (bare\nname or the qualified ``nodes..services.`` form). The surface is\nobservation metadata; it never mutates ``Node.services``.", "properties": { "application_id": { - "title": "Application Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "base_path": { @@ -6122,7 +6540,25 @@ }, "forwarding_agents": { "items": { - "$ref": "#/$defs/RuntimeForwardingAgent" + "allOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgent" + }, + { + "properties": { + "forwarding_agent_id": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "type": "object" + } + ] }, "title": "Forwarding Agents", "type": "array" @@ -6661,7 +7097,12 @@ "description": "An observed database service hosted by a transport service on a node.\n\n``service`` references the owning same-node ``Node.services[].name`` (bare\nname or the qualified ``nodes..services.`` form). The inventory\nis observation metadata; it never mutates ``Node.services``.", "properties": { "database_service_id": { - "title": "Database Service Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "databases": { @@ -6919,7 +7360,12 @@ "type": "string" }, "plugin_id": { - "title": "Plugin Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "version": { @@ -7032,7 +7478,12 @@ "title": "Leaf Field Count" }, "mapping_id": { - "title": "Mapping Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "name": { @@ -7169,7 +7620,12 @@ "type": "string" }, "node_id": { - "title": "Node Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "plugins": { @@ -7215,7 +7671,12 @@ "type": "string" }, "endpoint_id": { - "title": "Endpoint Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "port": { @@ -7385,7 +7846,12 @@ "type": "string" }, "partition_id": { - "title": "Partition Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "per_dc_factor_map": { @@ -7616,7 +8082,12 @@ "title": "Data Model" }, "datastore_service_id": { - "title": "Datastore Service Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -7799,7 +8270,12 @@ "title": "Scope" }, "setting_id": { - "title": "Setting Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "value": { @@ -7895,7 +8371,12 @@ "type": "string" }, "template_id": { - "title": "Template Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -8059,7 +8540,12 @@ "type": "string" }, "dns_service_id": { - "title": "Dns Service Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "dynamic_update": { @@ -8288,7 +8774,12 @@ "type": "string" }, "file_service_id": { - "title": "File Service Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "principals": { @@ -8407,7 +8898,12 @@ "type": "string" }, "observation_id": { - "title": "Observation Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "outcome": { @@ -8514,7 +9010,12 @@ "type": "string" }, "rule_id": { - "title": "Rule Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "subject_ref": { @@ -8609,7 +9110,12 @@ "title": "Origin" }, "principal_id": { - "title": "Principal Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "status": { @@ -8780,7 +9286,12 @@ "title": "Read Only" }, "share_id": { - "title": "Share Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "valid_groups": { @@ -9301,7 +9812,12 @@ "title": "Kind" }, "reload_channel_id": { - "title": "Reload Channel Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "target_ref": { @@ -9369,7 +9885,12 @@ "title": "Provenance" }, "setting_id": { - "title": "Setting Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "value": { @@ -9471,7 +9992,12 @@ "title": "Protocol" }, "target_id": { - "title": "Target Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "target_node_ref": { @@ -9535,7 +10061,12 @@ "type": "string" }, "source_id": { - "title": "Source Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -9584,7 +10115,12 @@ "type": "string" }, "transform_id": { - "title": "Transform Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -9799,7 +10335,12 @@ "type": "string" }, "identity_authority_id": { - "title": "Identity Authority Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "issuer": { @@ -9954,7 +10495,12 @@ "type": "string" }, "service_id": { - "title": "Service Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -9986,7 +10532,12 @@ "type": "string" }, "policy_id": { - "title": "Policy Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "policy_kind": { @@ -10076,7 +10627,12 @@ "type": "string" }, "relationship_id": { - "title": "Relationship Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "relationship_type": { @@ -10213,7 +10769,12 @@ "type": "array" }, "subject_id": { - "title": "Subject Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -10638,7 +11199,12 @@ "type": "string" }, "alias_id": { - "title": "Alias Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -10696,7 +11262,12 @@ "description": "A mail-service engine/component such as Postfix, Dovecot, or a filter.", "properties": { "component_id": { - "title": "Component Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -10778,7 +11349,12 @@ "type": "string" }, "domain_id": { - "title": "Domain Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "name": { @@ -10866,7 +11442,12 @@ "type": "string" }, "listener_id": { - "title": "Listener Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "protocol": { @@ -11001,7 +11582,12 @@ "type": "string" }, "mailbox_id": { - "title": "Mailbox Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "role": { @@ -11097,7 +11683,12 @@ "type": "string" }, "store_id": { - "title": "Store Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -11182,7 +11773,12 @@ "type": "string" }, "queue_id": { - "title": "Queue Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "stability": { @@ -11277,7 +11873,12 @@ "type": "string" }, "rule_id": { - "title": "Rule Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "source_ref": { @@ -11340,7 +11941,12 @@ "type": "array" }, "mail_service_id": { - "title": "Mail Service Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "mailbox_stores": { @@ -11431,7 +12037,12 @@ "title": "Provenance" }, "setting_id": { - "title": "Setting Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "source_path": { @@ -11864,7 +12475,12 @@ "type": "array" }, "channel_id": { - "title": "Channel Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -11997,7 +12613,12 @@ "type": "string" }, "network_detection_engine_id": { - "title": "Network Detection Engine Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "network_sets": { @@ -12141,7 +12762,12 @@ "type": "array" }, "set_id": { - "title": "Set Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -12238,7 +12864,12 @@ "type": "string" }, "stream_id": { - "title": "Stream Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -12347,7 +12978,12 @@ "title": "Rule Count" }, "source_id": { - "title": "Source Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -12653,7 +13289,12 @@ "type": "string" }, "network_sensor_id": { - "title": "Network Sensor Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "process_ref": { @@ -12830,7 +13471,12 @@ "type": "string" }, "orchestration_authority_id": { - "title": "Orchestration Authority Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "privilege_class": { @@ -12965,7 +13611,12 @@ "type": "string" }, "workload_id": { - "title": "Workload Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -13018,7 +13669,12 @@ "type": "string" }, "template_id": { - "title": "Template Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -13212,7 +13868,12 @@ "type": "array" }, "platform_application_id": { - "title": "Platform Application Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "platform_kind": { @@ -13275,7 +13936,12 @@ "description": "A connector/integration wired into the platform.\n\nA connector never carries a raw credential value; its credential posture is\nrecorded purely via :attr:`credential_classification`.", "properties": { "connector_id": { - "title": "Connector Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "credential_classification": { @@ -13358,7 +14024,12 @@ "type": "object" }, "content_object_id": { - "title": "Content Object Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -13513,7 +14184,12 @@ "type": "string" }, "marking_id": { - "title": "Marking Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "scheme": { @@ -13565,7 +14241,12 @@ "type": "string" }, "organization_id": { - "title": "Organization Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -13619,7 +14300,12 @@ "type": "string" }, "setting_id": { - "title": "Setting Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "value": { @@ -13673,7 +14359,12 @@ "type": "string" }, "tenant_id": { - "title": "Tenant Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -13688,7 +14379,12 @@ "description": "An outbound binding to an upstream node/service (data source, backend).", "properties": { "binding_id": { - "title": "Binding Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -14156,7 +14852,12 @@ "default": null }, "scheduled_job_id": { - "title": "Scheduled Job Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -14268,7 +14969,12 @@ "type": "string" }, "agent_id": { - "title": "Agent Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -14339,7 +15045,12 @@ "type": "string" }, "group_id": { - "title": "Group Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "member_refs": { @@ -14381,7 +15092,12 @@ "description": "A manager daemon, module, or internal component.", "properties": { "component_id": { - "title": "Component Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -14525,7 +15241,12 @@ "description": "A manager-owned rule, decoder, policy, list, or query corpus.", "properties": { "content_id": { - "title": "Content Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "description": { @@ -14649,7 +15370,12 @@ "type": "array" }, "definition_id": { - "title": "Definition Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "definition_kind": { @@ -15066,7 +15792,12 @@ "type": "string" }, "listener_id": { - "title": "Listener Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "protocol": { @@ -15236,7 +15967,12 @@ "type": "string" }, "security_monitoring_manager_id": { - "title": "Security Monitoring Manager Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "service": { @@ -15310,7 +16046,12 @@ "title": "Provenance" }, "setting_id": { - "title": "Setting Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "source_path": { @@ -15494,7 +16235,12 @@ "type": "string" }, "service_listener_id": { - "title": "Service Listener Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "socket_path": { @@ -15799,7 +16545,12 @@ "type": "string" }, "ssh_server_id": { - "title": "Ssh Server Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" } }, @@ -15910,7 +16661,7 @@ }, "Script": { "additionalProperties": false, - "description": "A timed sequence of events.\n\nTime values are human-readable duration strings parsed to seconds.", + "description": "A timed sequence of human-readable durations parsed to seconds.", "properties": { "description": { "default": "", @@ -16158,9 +16909,22 @@ "type": "string" }, "name": { + "anyOf": [ + { + "const": "" + }, + { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + ], "default": "", - "title": "Name", - "type": "string" + "title": "Name" }, "port": { "anyOf": [ @@ -16561,7 +17325,12 @@ "default": null }, "match_id": { - "title": "Match Id", + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "type": "string" }, "password_authentication": { @@ -16657,13 +17426,82 @@ "title": "Story", "type": "object" }, - "SuccessMode": { - "description": "How referenced success criteria combine.", + "StringPredicate": { + "additionalProperties": false, + "description": "Compare a governed string property with typed string operands.", + "properties": { + "expected": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "title": "Expected" + }, + "kind": { + "const": "string", + "default": "string", + "title": "Kind", + "type": "string" + }, + "operator": { + "default": "equals", + "enum": [ + "equals", + "not_equals", + "in", + "not_in" + ], + "title": "Operator", + "type": "string" + }, + "property": { + "maxLength": 512, + "minLength": 1, + "pattern": "^[a-z0-9][a-z0-9_-]{0,63}(?:\\.[a-z0-9][a-z0-9_-]{0,63})*$", + "title": "Property", + "type": "string" + }, + "semantic_ref": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^(?:https://[^\\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + "title": "Semantic Ref", + "type": "string" + } + }, + "required": [ + "property", + "semantic_ref", + "expected" + ], + "title": "StringPredicate", + "type": "object" + }, + "SubjectQuantifier": { + "description": "Quantification over a proposition's finite, resolved subject set.", + "enum": [ + "all", + "any", + "at_least" + ], + "title": "SubjectQuantifier", + "type": "string" + }, + "TruthCompositionMode": { + "description": "Portable composition over assertion outcomes.", "enum": [ "all_of", - "any_of" + "any_of", + "at_least" ], - "title": "SuccessMode", + "title": "TruthCompositionMode", "type": "string" }, "Variable": { @@ -16804,10 +17642,19 @@ "type": "string" }, "steps": { - "additionalProperties": { - "$ref": "#/$defs/WorkflowStep" - }, "minProperties": 1, + "patternProperties": { + "^[a-z0-9]": { + "$ref": "#/$defs/WorkflowStep" + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + } + }, "title": "Steps", "type": "object" }, @@ -16888,13 +17735,13 @@ }, "WorkflowPredicate": { "additionalProperties": false, - "description": "Branch predicate over observable state, objectives, and prior step state.\n\nPer ADR-073 the OCR scoring references (``metrics`` / ``evaluations`` /\n``tlos`` / ``goals``) were removed; a predicate branches on observable\n``conditions``, declared ``objectives``, and prior workflow ``steps``.", + "description": "Typed branch predicate over assertions, objectives, and prior step state.", "properties": { - "conditions": { + "assertions": { "items": { "type": "string" }, - "title": "Conditions", + "title": "Assertions", "type": "array" }, "objectives": { @@ -16933,9 +17780,9 @@ "title": "Cases", "type": "array" }, - "compensate-with": { + "compensate_with": { "default": "", - "title": "Compensate-With", + "title": "Compensate With", "type": "string" }, "default": { @@ -16958,7 +17805,7 @@ "title": "Join", "type": "string" }, - "max-attempts": { + "max_attempts": { "anyOf": [ { "type": "integer" @@ -16971,7 +17818,7 @@ } ], "default": null, - "title": "Max-Attempts" + "title": "Max Attempts" }, "next": { "default": "", @@ -16983,19 +17830,19 @@ "title": "Objective", "type": "string" }, - "on-exhausted": { + "on_exhausted": { "default": "", - "title": "On-Exhausted", + "title": "On Exhausted", "type": "string" }, - "on-failure": { + "on_failure": { "default": "", - "title": "On-Failure", + "title": "On Failure", "type": "string" }, - "on-success": { + "on_success": { "default": "", - "title": "On-Success", + "title": "On Success", "type": "string" }, "then": { @@ -17043,7 +17890,7 @@ "additionalProperties": false, "description": "Predicate reference to previously observed workflow step state.", "properties": { - "min-attempts": { + "min_attempts": { "anyOf": [ { "type": "integer" @@ -17056,7 +17903,7 @@ } ], "default": null, - "title": "Min-Attempts" + "title": "Min Attempts" }, "outcomes": { "items": { @@ -17143,12 +17990,21 @@ "$id": "https://aces.dev/schemas/sdl-authoring-input-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "Top-level scenario specification.\n\nA YAML document with up to 23 named sections. Only ``name``\nis required. All sections are optional dicts keyed by\nuser-defined identifiers.", + "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.", "properties": { "accounts": { "additionalProperties": { "$ref": "#/$defs/Account" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Accounts", "type": "object" }, @@ -17156,6 +18012,15 @@ "additionalProperties": { "$ref": "#/$defs/ParticipantActionContract" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Action Contracts", "type": "object" }, @@ -17163,13 +18028,47 @@ "additionalProperties": { "$ref": "#/$defs/Agent" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Agents", "type": "object" }, + "assertions": { + "additionalProperties": { + "$ref": "#/$defs/Assertion" + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "title": "Assertions", + "type": "object" + }, "behavior_specifications": { "additionalProperties": { "$ref": "#/$defs/ParticipantBehaviorSpecification" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Behavior Specifications", "type": "object" }, @@ -17177,6 +18076,15 @@ "additionalProperties": { "$ref": "#/$defs/Condition" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Conditions", "type": "object" }, @@ -17184,6 +18092,15 @@ "additionalProperties": { "$ref": "#/$defs/Content" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Content", "type": "object" }, @@ -17196,6 +18113,15 @@ "additionalProperties": { "$ref": "#/$defs/Entity" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Entities", "type": "object" }, @@ -17203,6 +18129,15 @@ "additionalProperties": { "$ref": "#/$defs/Event" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Events", "type": "object" }, @@ -17210,6 +18145,15 @@ "additionalProperties": { "$ref": "#/$defs/EvidenceRequirement" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Evidence Requirements", "type": "object" }, @@ -17217,12 +18161,39 @@ "additionalProperties": { "$ref": "#/$defs/Feature" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Features", "type": "object" }, "forwarding_agents": { "items": { - "$ref": "#/$defs/RuntimeForwardingAgent" + "allOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgent" + }, + { + "properties": { + "forwarding_agent_id": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + } + }, + "type": "object" + } + ] }, "title": "Forwarding Agents", "type": "array" @@ -17238,6 +18209,15 @@ "additionalProperties": { "$ref": "#/$defs/InfraNode" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Infrastructure", "type": "object" }, @@ -17245,6 +18225,15 @@ "additionalProperties": { "$ref": "#/$defs/Inject" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Injects", "type": "object" }, @@ -17260,6 +18249,12 @@ "default": null }, "name": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", "title": "Name", "type": "string" }, @@ -17267,6 +18262,15 @@ "additionalProperties": { "$ref": "#/$defs/Node" }, + "propertyNames": { + "maxLength": 35, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Nodes", "type": "object" }, @@ -17274,6 +18278,15 @@ "additionalProperties": { "$ref": "#/$defs/Objective" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Objectives", "type": "object" }, @@ -17281,6 +18294,15 @@ "additionalProperties": { "$ref": "#/$defs/ParticipantObservationBoundary" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Observation Boundaries", "type": "object" }, @@ -17288,13 +18310,47 @@ "additionalProperties": { "$ref": "#/$defs/OutcomeInterpretationRule" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Outcome Interpretation Rules", "type": "object" }, + "propositions": { + "additionalProperties": { + "$ref": "#/$defs/Proposition" + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "title": "Propositions", + "type": "object" + }, "relationships": { "additionalProperties": { "$ref": "#/$defs/Relationship" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Relationships", "type": "object" }, @@ -17302,6 +18358,15 @@ "additionalProperties": { "$ref": "#/$defs/Script" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Scripts", "type": "object" }, @@ -17309,16 +18374,34 @@ "additionalProperties": { "$ref": "#/$defs/Story" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Stories", "type": "object" }, "variables": { "additionalProperties": false, "patternProperties": { - "^[A-Za-z_][A-Za-z0-9_-]*$": { + "^[a-z0-9]": { "$ref": "#/$defs/Variable" } }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Variables", "type": "object" }, @@ -17331,6 +18414,15 @@ "additionalProperties": { "$ref": "#/$defs/Vulnerability" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Vulnerabilities", "type": "object" }, @@ -17338,6 +18430,15 @@ "additionalProperties": { "$ref": "#/$defs/Workflow" }, + "propertyNames": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, "title": "Workflows", "type": "object" } @@ -17345,6 +18446,9 @@ "required": [ "name" ], - "title": "Scenario", - "type": "object" + "title": "SDL Normalized Authoring Object v1", + "type": "object", + "x-aces-document-phase": "normalized-authoring-object", + "x-aces-source-profile": "sdl-yaml/v1", + "x-aces-validates-raw-source": false } diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index abe93d366..b4b5414a4 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -3370,6 +3370,404 @@ "title": "ParticipantTimeManagementContextModel", "type": "object" }, + "PropositionAssertionPolarity": { + "enum": [ + "positive", + "negative" + ], + "title": "PropositionAssertionPolarity", + "type": "string" + }, + "PropositionEvaluationBasis": { + "enum": [ + "declared_state", + "observed_state" + ], + "title": "PropositionEvaluationBasis", + "type": "string" + }, + "PropositionIndeterminacyReason": { + "enum": [ + "missing_evidence", + "stale_evidence", + "partial_evidence", + "conflicting_evidence", + "redacted_evidence", + "lossy_evidence", + "probe_failure" + ], + "title": "PropositionIndeterminacyReason", + "type": "string" + }, + "PropositionLossDisclosureModel": { + "additionalProperties": false, + "properties": { + "kind": { + "$ref": "#/$defs/PropositionLossKind" + }, + "within_admissible_bound": { + "title": "Within Admissible Bound", + "type": "boolean" + } + }, + "required": [ + "kind", + "within_admissible_bound" + ], + "title": "PropositionLossDisclosureModel", + "type": "object" + }, + "PropositionLossKind": { + "enum": [ + "missing", + "stale", + "partial", + "conflicting", + "redacted", + "lossy", + "probe_failure" + ], + "title": "PropositionLossKind", + "type": "string" + }, + "PropositionProbeBindingModel": { + "additionalProperties": false, + "description": "Provenance for one capability-bound proposition realization.", + "properties": { + "artifact_digest": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Artifact Digest", + "type": "string" + }, + "backend_manifest_ref": { + "minLength": 1, + "title": "Backend Manifest Ref", + "type": "string" + }, + "binding_id": { + "minLength": 1, + "title": "Binding Id", + "type": "string" + }, + "capability_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Capability Refs", + "type": "array" + }, + "implementation_id": { + "minLength": 1, + "title": "Implementation Id", + "type": "string" + }, + "implementation_version": { + "minLength": 1, + "title": "Implementation Version", + "type": "string" + }, + "proposition_address": { + "minLength": 1, + "title": "Proposition Address", + "type": "string" + } + }, + "required": [ + "binding_id", + "implementation_id", + "implementation_version", + "artifact_digest", + "backend_manifest_ref", + "proposition_address", + "capability_refs" + ], + "title": "PropositionProbeBindingModel", + "type": "object" + }, + "PropositionTemporalContextModel": { + "additionalProperties": false, + "description": "Governed boundary and clock context used for one evaluation.", + "properties": { + "boundary_ref": { + "minLength": 1, + "title": "Boundary Ref", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "time_domain": { + "minLength": 1, + "title": "Time Domain", + "type": "string" + } + }, + "required": [ + "boundary_ref", + "time_domain", + "clock_authority" + ], + "title": "PropositionTemporalContextModel", + "type": "object" + }, + "PropositionTruthOutcome": { + "description": "Portable proposition-evaluation outcome domain.\n\n``unsupported`` is a realization disposition included in the portable\nenvelope, not a logical truth value.", + "enum": [ + "true", + "false", + "unknown", + "unsupported" + ], + "title": "PropositionTruthOutcome", + "type": "string" + }, + "PropositionTruthResultModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "evaluation_basis": { + "const": "observed_state" + }, + "proposition_outcome": { + "enum": [ + "true", + "false" + ] + } + }, + "required": [ + "evaluation_basis", + "proposition_outcome" + ] + }, + "then": { + "properties": { + "evidence_refs": { + "minItems": 1 + }, + "probe_binding": { + "not": { + "type": "null" + } + }, + "temporal_context": { + "not": { + "type": "null" + } + } + } + } + }, + { + "else": { + "properties": { + "indeterminacy_reason": { + "type": "null" + } + } + }, + "if": { + "properties": { + "proposition_outcome": { + "const": "unknown" + } + }, + "required": [ + "proposition_outcome" + ] + }, + "then": { + "properties": { + "indeterminacy_reason": { + "not": { + "type": "null" + } + } + } + } + }, + { + "else": { + "properties": { + "unsupported_capability_refs": { + "maxItems": 0 + } + } + }, + "if": { + "properties": { + "proposition_outcome": { + "const": "unsupported" + } + }, + "required": [ + "proposition_outcome" + ] + }, + "then": { + "properties": { + "unsupported_capability_refs": { + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "proposition_outcome": { + "enum": [ + "true", + "false" + ] + } + }, + "required": [ + "proposition_outcome" + ] + }, + "then": { + "properties": { + "loss_disclosures": { + "items": { + "$ref": "#/$defs/PropositionLossDisclosureModel", + "properties": { + "within_admissible_bound": { + "const": true + } + }, + "required": [ + "within_admissible_bound" + ] + } + } + } + } + } + ], + "description": "Evidence-bearing truth result separate from evaluator lifecycle state.", + "properties": { + "assertion_address": { + "minLength": 1, + "title": "Assertion Address", + "type": "string" + }, + "assertion_outcome": { + "$ref": "#/$defs/PropositionTruthOutcome" + }, + "assertion_polarity": { + "$ref": "#/$defs/PropositionAssertionPolarity" + }, + "declared_artifact_digest": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declared Artifact Digest" + }, + "evaluation_basis": { + "$ref": "#/$defs/PropositionEvaluationBasis" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "indeterminacy_reason": { + "anyOf": [ + { + "$ref": "#/$defs/PropositionIndeterminacyReason" + }, + { + "type": "null" + } + ], + "default": null + }, + "loss_disclosures": { + "items": { + "$ref": "#/$defs/PropositionLossDisclosureModel" + }, + "title": "Loss Disclosures", + "type": "array" + }, + "probe_binding": { + "anyOf": [ + { + "$ref": "#/$defs/PropositionProbeBindingModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "proposition_address": { + "minLength": 1, + "title": "Proposition Address", + "type": "string" + }, + "proposition_outcome": { + "$ref": "#/$defs/PropositionTruthOutcome" + }, + "result_id": { + "minLength": 1, + "title": "Result Id", + "type": "string" + }, + "schema_version": { + "const": "proposition-truth-result/v1", + "default": "proposition-truth-result/v1", + "title": "Schema Version", + "type": "string" + }, + "temporal_context": { + "anyOf": [ + { + "$ref": "#/$defs/PropositionTemporalContextModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "unsupported_capability_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Unsupported Capability Refs", + "type": "array" + } + }, + "required": [ + "result_id", + "proposition_address", + "assertion_address", + "assertion_polarity", + "proposition_outcome", + "assertion_outcome", + "evaluation_basis" + ], + "title": "PropositionTruthResultModel", + "type": "object" + }, "RawDataIntegrityModel": { "additionalProperties": false, "description": "Hash, size, and truncation facts for raw data behind a runtime claim.", @@ -3443,6 +3841,46 @@ "title": "RawDataIntegrityModel", "type": "object" }, + "RealizationEnvelopeIdentityModel": { + "additionalProperties": false, + "description": "Immutable realization-envelope identity carried across runtime contracts.", + "properties": { + "configuration_digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Configuration Digest", + "type": "string" + }, + "contract_id": { + "const": "realization-envelope-v1", + "default": "realization-envelope-v1", + "title": "Contract Id", + "type": "string" + }, + "digest": { + "pattern": "^sha256:[a-f0-9]{64}$", + "title": "Digest", + "type": "string" + }, + "envelope_id": { + "minLength": 1, + "title": "Envelope Id", + "type": "string" + }, + "schema_version": { + "const": "realization-envelope/v1", + "default": "realization-envelope/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "envelope_id", + "digest", + "configuration_digest" + ], + "title": "RealizationEnvelopeIdentityModel", + "type": "object" + }, "RealizationProvenanceEntryModel": { "additionalProperties": false, "description": "SEM-218 invariant I5: provenance for one realized realization concern.\n\nDistinguishes ``author-declared`` / ``processor-derived`` / ``backend-realized``\norigins for a realized concern recorded on the snapshot's result / history\nsurfaces. Carries field-path and kind references only (never the realized\nvalue, per the SEM-218 host-exposure gate). Kept distinct from ADR-054\nlifecycle ``phase_realization`` and API-407 participant feature support.", @@ -3489,6 +3927,12 @@ "additionalProperties": false, "properties": { "address": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "title": "Address", "type": "string" }, @@ -3498,6 +3942,12 @@ }, "ordering_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Ordering Dependencies", @@ -3510,6 +3960,12 @@ }, "refresh_dependencies": { "items": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", "type": "string" }, "title": "Refresh Dependencies", @@ -3946,8 +4402,20 @@ "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.", "properties": { "entries": { - "additionalProperties": { - "$ref": "#/$defs/SnapshotEntryModel" + "additionalProperties": false, + "patternProperties": { + "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$": { + "$ref": "#/$defs/SnapshotEntryModel" + } + }, + "propertyNames": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", + "type": "string" }, "title": "Entries", "type": "object" @@ -4025,6 +4493,24 @@ "title": "Participant Episode Results", "type": "object" }, + "proposition_truth_results": { + "additionalProperties": { + "$ref": "#/$defs/PropositionTruthResultModel" + }, + "title": "Proposition Truth Results", + "type": "object" + }, + "realization_envelope": { + "anyOf": [ + { + "$ref": "#/$defs/RealizationEnvelopeIdentityModel" + }, + { + "type": "null" + } + ], + "default": null + }, "realization_provenance": { "items": { "$ref": "#/$defs/RealizationProvenanceEntryModel" diff --git a/docs/aces/inventory/scn010-expressivity-gap-analysis.md b/docs/aces/inventory/scn010-expressivity-gap-analysis.md index e477c586b..b612281d1 100644 --- a/docs/aces/inventory/scn010-expressivity-gap-analysis.md +++ b/docs/aces/inventory/scn010-expressivity-gap-analysis.md @@ -1,8 +1,6 @@ # SCN-010 Expressivity Gap Analysis — Final Architect-Review Report ## ACES SDL runtime surface coverage for the 16 remaining APTL containers ---- - ## 1. Executive summary The 16 remaining SCN-010 containers fall into seven functional groups — a search/index cluster tier (`wazuh.indexer`, `thehive-es`, `shuffle-opensearch`), an analytics dashboard (`wazuh.dashboard`), log-shipping/intel-sync sidecars (`wazuh-sidecar-db`, `wazuh-sidecar-suricata`, `misp-suricata-sync`) plus the standalone Suricata they govern, a threat-intelligence platform (`misp`), a Redis/MariaDB data-store-and-sync tier (`misp-redis`, `misp-db`), an IR/case-management platform over a wide-column store (`thehive`, `thehive-cassandra`), a SOAR tier (`shuffle-frontend`, `shuffle-orborus`, `shuffle-backend`), and an analyzer/responder engine (`cortex`). Holding each container to the **wazuh.manager parity bar** — wazuh.manager (a SIEM) earned a fully domain-typed `security_monitoring_managers` family rather than being folded into generic `filesystem`/`listeners`/`software` surfaces — surfaces a consistent failure mode: every one of these containers has *defining logical state* that ACES can only shallow-encode today (a Redis cache as a relational `RuntimeDatabaseService`, a search cluster as a relational engine, a MISP as content counts, a SOAR as a data volume, an embedded RBAC store as OS `/etc` identity). The cohesive response is **extension-first at the least structural cost that still clears parity**: collapse the recurring *shapes* into two discriminator-guarded spines plus three orthogonal shared primitives and one forwarder family, each spine **guarded by a `require_profile_for_` after-validator** so the abstraction provably cannot silently shallow-encode a defining fact, with inter-node access detail expressed as typed relationship subtypes, and a single executable cross-family invariant lint making "#439 one small invariant set" testable rather than aspirational. diff --git a/docs/conf.py b/docs/conf.py index 085d2a760..71bb8456f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,21 @@ +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as _distribution_version + # -- Project information ------------------------------------------------------- project = "ACES SDL" copyright = "2026, Brad Edwards" author = "Brad Edwards" -release = "0.1.0" + +# The docs build version derives from the installed `aces-sdl` distribution +# metadata (the release-please-owned source of truth), not a hand-maintained +# literal (GOV-901). The honest PEP 440 sentinel `0.0.0+unknown` is used when +# the distribution is not installed, so the docs never imply a false release. +try: + release = _distribution_version("aces-sdl") +except PackageNotFoundError: + release = "0.0.0+unknown" +version = release.split("+", 1)[0] # -- General configuration ----------------------------------------------------- diff --git a/docs/conformance/libvirt-qemu.provisioning-only.report.json b/docs/conformance/libvirt-qemu.provisioning-only.report.json index 1ce8a7db3..770d31133 100644 --- a/docs/conformance/libvirt-qemu.provisioning-only.report.json +++ b/docs/conformance/libvirt-qemu.provisioning-only.report.json @@ -234,21 +234,21 @@ ] }, { - "name": "live-manifest", + "name": "target-manifest", "contract_name": "backend-manifest-v2", "valid": true, "passed": true, "diagnostic_codes": [] }, { - "name": "live-provisioning", + "name": "target-provisioning", "contract_name": "operation-status-v1", "valid": true, "passed": true, "diagnostic_codes": [] }, { - "name": "live-snapshot", + "name": "target-snapshot", "contract_name": "runtime-snapshot-v1", "valid": true, "passed": true, diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index 1f75263dc..b1290e71c 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -118,6 +118,13 @@ adr-070-realization-envelope-semantics adr-071-reusable-asset-trust-and-integrity-policy adr-072-validation-and-admission-profiles adr-073-scoring-reward-language-scope +adr-074-experiment-authoring-input-contract-boundary +adr-075-ecosystem-versioning-deprecation-and-migration-governance +adr-076-portable-sdl-identifiers-and-canonical-addresses +adr-077-associated-artifact-manifest-boundary +adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence +adr-079-backend-neutral-proposition-and-truth-semantics +adr-080-revision-pinned-sdl-lineage-and-provenance-ledger ``` | ADR | Title | Status | Date | @@ -196,3 +203,10 @@ adr-073-scoring-reward-language-scope | [071](adr-071-reusable-asset-trust-and-integrity-policy.md) | Reusable Asset Trust and Integrity Policy | accepted | 2026-07-05 | | [072](adr-072-validation-and-admission-profiles.md) | Validation and Admission Profiles | proposed | 2026-07-05 | | [073](adr-073-scoring-reward-language-scope.md) | Scoring and Reward Language Scope in the SDL | accepted | 2026-07-05 | +| [074](adr-074-experiment-authoring-input-contract-boundary.md) | Experiment Authoring-Input Contract Boundary | accepted | 2026-07-08 | +| [075](adr-075-ecosystem-versioning-deprecation-and-migration-governance.md) | Ecosystem Versioning, Deprecation, and Migration Governance | proposed | 2026-07-11 | +| [076](adr-076-portable-sdl-identifiers-and-canonical-addresses.md) | Portable SDL Identifiers and Canonical Addresses | accepted | 2026-07-11 | +| [077](adr-077-associated-artifact-manifest-boundary.md) | Associated Artifact Manifest Boundary | accepted | 2026-07-12 | +| [078](adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md) | Closed SDL Phase Contracts and Portable Derivation Evidence | accepted | 2026-07-12 | +| [079](adr-079-backend-neutral-proposition-and-truth-semantics.md) | Backend-Neutral Proposition and Truth Semantics | accepted | 2026-07-12 | +| [080](adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md) | Revision-Pinned SDL Lineage And Provenance Ledger | accepted | 2026-07-12 | diff --git a/docs/decisions/adrs/adr-001-scenario-description-language.md b/docs/decisions/adrs/adr-001-scenario-description-language.md index e3d350c95..1274aaa57 100644 --- a/docs/decisions/adrs/adr-001-scenario-description-language.md +++ b/docs/decisions/adrs/adr-001-scenario-description-language.md @@ -108,3 +108,9 @@ None by design. The repository accepts SDL documents only: - If the repository fails to keep authored participant intent distinct from participant implementation and runtime apparatus, future agent-support work could leak execution-stack concerns back into the SDL surface + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-11 | #721 | Replaced the original implicit case-insensitive parser convention with the normative `sdl-yaml/v1` source profile: YAML 1.2.2 Core resolution, exact `snake_case` structural fields, strict default rejection, and explicit diagnosed migration for legacy case/kebab spellings and disjoint `<<` merges. Distinguished raw YAML from the normalized authoring-object schema and added the `aces-sdl-semantic/v1` RFC 8785 identity profile. The accepted body above remains the historical 2026-03-29 decision; its parser-spelling statements are not the current syntax contract. | diff --git a/docs/decisions/adrs/adr-002-declarative-sdl-objectives.md b/docs/decisions/adrs/adr-002-declarative-sdl-objectives.md index 909f30587..42b5a7c2d 100644 --- a/docs/decisions/adrs/adr-002-declarative-sdl-objectives.md +++ b/docs/decisions/adrs/adr-002-declarative-sdl-objectives.md @@ -42,7 +42,8 @@ Each objective may declare: - exactly one actor: `agent` or `entity` - optional `actions` - optional `targets` -- required `success` criteria referencing declared `conditions`, `metrics`, `evaluations`, `tlos`, or `goals` +- required `success` criteria composing declared invariant or postcondition + assertions over backend-neutral propositions (ADR-079) - optional `window` constraints over `stories`, `scripts`, and `events` - optional `depends_on` links forming an acyclic ordering relation between objectives @@ -53,7 +54,10 @@ This section is intentionally declarative. It expresses: - when the objective matters - how success should be interpreted -It does **not** encode backend-specific validation probes such as Wazuh queries, command execution, file checks, polling loops, or session orchestration. Those remain runtime concerns and continue to live outside the SDL. +It does **not** encode backend-specific validation probes such as Wazuh queries, +command execution, file checks, polling loops, or session orchestration in +objective meaning. Legacy `conditions` carry probe implementation mechanics; +they are not propositions and cannot be referenced as objective success. It also does **not** identify the concrete participant implementation that will realize an authored `agent` or `entity` role in a given run. That remains a @@ -94,3 +98,4 @@ This ADR refines ADR-001's SDL boundary by making declarative objectives part of | Date | Commit/PR | Summary | |------|-----------|---------| | 2026-07-05 | #682 | Per [ADR-073](adr-073-scoring-reward-language-scope.md), narrowed the objective-success clause: `objectives.success` references observable state (`conditions`) only. The OCR scoring pipeline (`metrics` / `evaluations` / `tlos` / `goals`) this ADR preserved was removed from the SDL; graded scoring and reward now live in the experiment/evaluator plane (ADR-055/064/069). | +| 2026-07-12 | #725 | Per [ADR-079](adr-079-backend-neutral-proposition-and-truth-semantics.md), corrected the condition/proposition conflation: objective success now composes invariant or postcondition assertions over typed propositions; executable conditions are probe realizations only. | diff --git a/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md b/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md index a8cbbdafb..ae702eb37 100644 --- a/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md +++ b/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md @@ -83,26 +83,34 @@ checker walks the AST of every changed `.py` file under each rule's `scope_root` and emits `layering-rule-violation` for any `import` or `from … import` of the forbidden top-level package or its submodules. -### 2. 600-line cap on non-test source files +### 2. 500-line cap on non-test source files No non-test, non-generated `.py` file under `implementations/python/packages/` -shall exceed 600 lines, except for paths listed in +shall exceed 500 lines, except for paths listed in `tools/policy/oversized_allowlist.yaml`. +The 500-line value matches SonarCloud's S104 threshold, so a file either +passes both the repo policy and the Sonar quality gate or fails both; there +is no longer a 500–599 band that is repo-compliant but blocks the +`dev → main` integration PR. (The cap was originally 600; see Amendments.) + The allowlist is the inverse of the goal: each entry is technical debt to -be drained. Each child PR of #3 splits its file and removes the entry. -The tracker closes when the allowlist is empty. +be drained. A split PR splits its file and removes the entry. The +modularity tracker closes when the allowlist is empty. -The fixed reference set — the 14 paths over the cap when this ADR landed — -is a *code constant*, `_ADR015_INITIAL_OVERSIZED_FILES` in +The fixed reference set — the paths over the cap when the current cap was +adopted — is a *code constant*, `_ADR015_INITIAL_OVERSIZED_FILES` in `tools/policy/repo_policy.py`, **not** config in `adr_policy.yaml`. Keeping the locked set in PR-editable config would let one PR satisfy "the allowlist only shrinks" by editing the allowlist and the locked set together; pinning -it in the policy module means adding a 15th entry requires a diff to the +it in the policy module means adding an entry requires a diff to the checker code, which PR review scrutinises as policy rather than as data noise. The allowlist must be a subset of that constant — no new entry may be -added; a file over the cap that wasn't one of the initial set must be split, -not allow-listed. Enforcement: +added; a file over the cap that wasn't one of the reference set must be +split, not allow-listed. Lowering the cap re-baselines both the constant and +the allowlist to the files over the new cap at that time (a code-constant +diff, PR-scrutinised the same way); the shrink-only rule then continues from +the new baseline. Enforcement: - `_check_oversized` — emits `oversized-source-file` for an over-cap, non-allowlisted changed file. @@ -170,14 +178,14 @@ defensively. That would be a separate ADR. - The dependency between `aces_sdl` and `aces_processor` is a DAG, enforced by CI. - Each subsequent file split inherits the layering invariant for free. -- The cap creates a forcing function: a file that wants to grow past 600 +- The cap creates a forcing function: a file that wants to grow past 500 lines must justify the split or the allowlist entry. - The allowlist is self-documenting debt; its size measures remaining modularity work. ### Negative -- Fourteen subsequent PRs are needed to drain the allowlist. +- A subsequent PR is needed to drain each allowlist entry. - `docs/api/processor-semantics.rst` is rescoped to planner only; a new `docs/api/sdl-semantics.rst` documents the moved modules. Bookmarks to the processor-semantics page still resolve, to a smaller page. @@ -188,6 +196,12 @@ defensively. That would be a separate ADR. re-introduce a layering bug the current rule doesn't capture. Mitigation: each new package introduction should consider whether it needs its own `layering_rules` entry. -- The 600-line cap is conservative; some cohesive modules may hit it. The +- The 500-line cap is conservative; some cohesive modules may hit it. The allowlist gives an explicit, visible escape hatch. The cap can be tuned later if it proves too aggressive. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-12 | #561 | Lowered the non-test source-file cap from 600 to 500 lines to align with SonarCloud's S104 threshold, closing the 500–599 band where a file was repo-compliant but failed the Sonar quality gate at the `dev → main` integration PR (#545). Re-baselined `_ADR015_INITIAL_OVERSIZED_FILES` and `tools/policy/oversized_allowlist.yaml` to the files over the 500-line cap as of this amendment; the shrink-only rule continues from that new baseline. | diff --git a/docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md b/docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md index 285dadf10..d3081ffc3 100644 --- a/docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md +++ b/docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md @@ -13,7 +13,8 @@ accepted [ADR-009](adr-009-normative-artifact-authority-and-repository-structure.md) decided that the ecosystem's authority boundary separates normative artifacts (prose under `specs/`, schemas under `contracts/schemas/`, fixtures under -`contracts/fixtures/`, profiles under `contracts/profiles/`, and the shared +`contracts/fixtures/`, profiles under `contracts/profiles/`, configuration-bound +realization envelopes under `contracts/realization-envelopes/`, and the shared concept-authority artifacts under `contracts/concept-authority/`) from reference implementations (`implementations/`), explanatory docs (`docs/`), worked examples (`examples/`), research material (`research/`), and tooling @@ -44,10 +45,11 @@ decision: - `schemas` under `contracts/schemas/` - `fixtures` under `contracts/fixtures/` - `profiles` under `contracts/profiles/` + - `realization-envelopes` under `contracts/realization-envelopes/` - `concept-authority` under `contracts/concept-authority/` It also enumerates each non-normative root (`implementations/`, `docs/`, - `examples/`, `research/`, `notes/`, `tools/`, `changelog.d/`), the + `examples/`, `research/`, `notes/`, `tools/`), the legacy top-level directories ADR-009 transitioned out (`schemas`, `conformance`, `src`), and a `schema_authority` block that pins the normative schema root, the publication manifest path, and the codegen @@ -114,3 +116,11 @@ decision: ADR-009 must update `CANONICAL_AUTHORITY_ROOT_IDS` in `tools/check_authority_boundary.py` alongside the YAML edit. This is the same pattern ADR-018's gate uses, and it surfaces in test coverage. + +## Amendments + +| Date | Commit/PR/Issue | Summary | +|------|-----------------|---------| +| 2026-07-11 | #100 | Added `contracts/realization-envelopes/` as the normative authority for configuration-bound backend realization disclosures. | +| 2026-07-12 | issue #726 | Added `contracts/provenance/` as the normative `provenance` authority for revision-pinned SDL lineage, derivation, citation identity, and third-party notice dispositions. | +| 2026-07-12 | issue #240 | Removed `changelog.d/` from the enumerated non-normative roots: release-please (#684) replaced the towncrier fragment workflow, so the fragment directory is no longer part of the authority boundary (GOV-901). | diff --git a/docs/decisions/adrs/adr-055-experiment-core-contract-boundary.md b/docs/decisions/adrs/adr-055-experiment-core-contract-boundary.md index 1b59b7927..ccc32f477 100644 --- a/docs/decisions/adrs/adr-055-experiment-core-contract-boundary.md +++ b/docs/decisions/adrs/adr-055-experiment-core-contract-boundary.md @@ -293,3 +293,4 @@ and redaction patterns. | Date | Commit/PR | Summary | |------|-----------|---------| | 2026-06-12 | #482 | Recorded that the ADR's decision date is 2026-05-26 and it landed with experiment-core PR #422 on 2026-06-05. | +| 2026-07-08 | #675 | ADR-074 realizes the "explicit draft/authoring surface" anticipated in this ADR's Risks section as `experiment-authoring-input-v1`, a separate pre-run input contract; the archival contracts published here are unchanged. | diff --git a/docs/decisions/adrs/adr-069-cage-2-replication-architecture.md b/docs/decisions/adrs/adr-069-cage-2-replication-architecture.md index aea16c32b..e23731edd 100644 --- a/docs/decisions/adrs/adr-069-cage-2-replication-architecture.md +++ b/docs/decisions/adrs/adr-069-cage-2-replication-architecture.md @@ -286,3 +286,9 @@ adapter-driven replication. - Cross-repo work may drift if downstream issues do not link back to ACES requirements and design records. The workflow requires linked issues, PRs, and evidence readback. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-08 | #675 | ADR-074 adds `experiment-authoring-input-v1`, a pre-run experiment authoring surface, giving REP-003 a home to declare the execution-control facts (turn order, step count, termination, red-agent variants, seeds, stochastic controls) this ADR requires for execution-control equivalence. It complements — does not replace — the SDL scenario authoring path this ADR routes CAGE-2 through, and adds no CAGE-specific schema. | diff --git a/docs/decisions/adrs/adr-070-realization-envelope-semantics.md b/docs/decisions/adrs/adr-070-realization-envelope-semantics.md index b204e5d52..42df3519a 100644 --- a/docs/decisions/adrs/adr-070-realization-envelope-semantics.md +++ b/docs/decisions/adrs/adr-070-realization-envelope-semantics.md @@ -133,19 +133,23 @@ inside a typed domain. Exact means the domain is a singleton. Closed-world scope means no unspecified realizable dimensions under that scope are portable members of the set. -### 5. Backend manifest carriage is a schema-evolution question +### 5. Backend manifest carriage uses configuration-bound identity -Current `backend-manifest-v2` can disclose coarse support through -`realization_support`. It cannot express value-level sets, scoped closure, or a -portable subsumption relation. +`backend-manifest-v2.realization_support` discloses coarse support but cannot by +itself express value-level sets, scoped closure, or a portable subsumption +relation. -The selected carriage direction is a future manifest evolution that can either: +Issue #100 implements the selected carriage direction as a reference to a +published envelope artifact by contract id, envelope id, version, canonical +content digest, and secret-free material-configuration digest. The same identity +is carried by provisioning plans and runtime snapshots. -- embed a small envelope expression directly; or -- reference a published envelope artifact by contract id, digest, and version. +The published artifact embeds the shared expression and closed typed backend +realization, transformation, and observation-strength disclosures. Backend +manifest payloads carry only its immutable identity. -Both modes use the same expression contract. Neither overloads the current -`constraints: dict[str, str]` prose map as the final semantics. +This does not overload the current `constraints: dict[str, str]` prose map and +does not create a backend-local set relation. ### 6. Closed envelopes require negative conformance diff --git a/docs/decisions/adrs/adr-073-scoring-reward-language-scope.md b/docs/decisions/adrs/adr-073-scoring-reward-language-scope.md index 6a6545e7b..dfffd4c4e 100644 --- a/docs/decisions/adrs/adr-073-scoring-reward-language-scope.md +++ b/docs/decisions/adrs/adr-073-scoring-reward-language-scope.md @@ -89,8 +89,9 @@ The load-bearing findings: cross-reference validator** anywhere in `aces_sdl/validator/`. It names a CybORG reward class that runs outside participant perception and binds to nothing in ACES. It is the weakest surface in the language. -- `conditions` are observable state facts and `objectives` are participant - intent; both are read within the horizon and pass the discriminator. +- Typed propositions/assertions are observable state claims and `objectives` + are participant intent. Executable `conditions` are probe realizations, not + state facts (ADR-079). - The one leak between the two sides is `objectives.success`, which today can be satisfied by the scoring pipeline instead of by observable state. @@ -122,21 +123,22 @@ The recommendation is therefore to **deprecate and remove** these five surfaces from the SDL authoring language, its published schemas, the reference model, and the example corpus. -### 2. `objectives` and `conditions` stay in the horizon +### 2. Objectives and observable propositions stay in the horizon -`conditions` (observable state) and `objectives` (participant intent) are -authored scenario meaning and remain first-class SDL surfaces. Removing the -grading pipeline must not weaken what a scenario can assert about its own -observable state (ADR-020's reproducibility warning). +Typed propositions/assertions (observable state claims) and `objectives` +(participant intent) are authored scenario meaning and remain first-class SDL +surfaces. `conditions` remain only as explicitly proposition-bound probe +implementations. Removing the grading pipeline must not weaken what a scenario +can assert about its own observable state (ADR-020's reproducibility warning). ### 3. Objective success references observable state, not a score -`objectives.success` is narrowed to reference `conditions` (observable state), -not `metrics` / `evaluations` / `tlos` / `goals`. This directly answers issue -#671 question 2: success criteria should be expressed against observable state, -which is in-horizon, rather than a score-shaped grading pipeline, which is not. -On acceptance this narrows the success clause established by ADR-002 §Decision, -and the implementing issue records an ADR-002 amendment. +`objectives.success` references invariant or postcondition assertions over +typed propositions, not `conditions`, `metrics`, `evaluations`, `tlos`, or +`goals`. This preserves the issue #671 boundary: success is expressed against +observable state, which is in-horizon, rather than either an executable probe +or a score-shaped grading pipeline. ADR-079 defines the proposition and result +semantics. ### 4. Graded scoring and reward live only in the experiment/evaluator plane @@ -156,10 +158,10 @@ Removal is staged, not abrupt (answering issue #671 question 4): - **Deprecate first.** Mark the five surfaces deprecated in the schema and the SDL sections documentation with a pointer to the experiment/evaluator plane - and to `conditions`-based objective success. Emit a deprecation diagnostic + and to proposition/assertion-based objective success. Emit a deprecation diagnostic when a scenario uses them. - **Migrate the four study-style scenarios.** For each, re-express objective - success against `conditions`; move any genuinely-graded evaluation into an + success against typed propositions/assertions; move any genuinely-graded evaluation into an `experiment-*` artifact where the study intends scientific comparison; drop `reward_calculator` (it binds to nothing). The six `techvault-*` scenarios need no change. @@ -179,8 +181,10 @@ Removal is staged, not abrupt (answering issue #671 question 4): 1. **Do the scoring sections and `reward_calculator` belong in ACES?** Not in the SDL. They are vestigial given the experiment-vs-data-use boundary; their concerns belong to the experiment/evaluator plane. -2. **Should objective success reference conditions rather than a score?** Yes — - objective success references observable state (`conditions`). +2. **Should objective success reference conditions rather than a score?** It + should reference observable state rather than a score, but ADR-079 corrects + the original category error: typed propositions/assertions state observable + truth; executable conditions only realize probes. 3. **If scoring belongs somewhere, is it the SDL or an experiment/evaluator contract?** The experiment/evaluator contract boundary (ADR-055/064/069), and only in the SDL when a participant consumes the signal in-run. @@ -235,8 +239,9 @@ ratification. The ADR is proposed; implementation is spawned on acceptance. - One authority for scoring/evaluation/reward: the experiment/evaluator plane. The SDL stops carrying a duplicate, weaker copy. -- `objectives.success` becomes reproducible and in-horizon (observable state - only), closing the leak between authored meaning and grading. +- `objectives.success` becomes inspectable and in-horizon (typed observable + propositions only), closing both the grading leak and the probe/meaning + conflation. - The language shrinks by five surfaces, one of which (`reward_calculator`) was unvalidated and unbound. - SEM-206 and `Brad-Edwards/aptl#606` get a decision to follow instead of a @@ -267,3 +272,4 @@ ratification. The ADR is proposed; implementation is spawned on acceptance. | Date | Commit/PR | Summary | |------|-----------|---------| | 2026-07-06 | #682 | Accepted (proposed → accepted) and realized under SEM-206: the SDL scoring/reward surfaces (`metrics`/`evaluations`/`tlos`/`goals` and `agents.reward_calculator`) were removed, `objectives.success` narrowed to `conditions`, ADR-002's objective-success clause amended, and the published SDL schemas updated with change-ledger entries. The staged deprecation window in §5 was collapsed into one change: all in-repo consumers were migrated together, with the downstream `Brad-Edwards/aptl#606` tracked to follow. | +| 2026-07-12 | #725 | Per [ADR-079](adr-079-backend-neutral-proposition-and-truth-semantics.md), corrected the earlier classification of executable conditions as observable facts. Objective success now composes assertions over typed propositions; scoring/reward remains in the experiment/evaluator plane. | diff --git a/docs/decisions/adrs/adr-074-experiment-authoring-input-contract-boundary.md b/docs/decisions/adrs/adr-074-experiment-authoring-input-contract-boundary.md new file mode 100644 index 000000000..43b650a57 --- /dev/null +++ b/docs/decisions/adrs/adr-074-experiment-authoring-input-contract-boundary.md @@ -0,0 +1,191 @@ +# ADR-074: Experiment Authoring-Input Contract Boundary + +## Status + +accepted + +## Date + +2026-07-08 + +## Classification + +Classification: FM2 +Required artifacts: ADR, formal-spec update, published schema, fixtures, +worked examples, MCP authoring tools, discovery/validation test, amendments to +ADR-055 and ADR-069 +Waivers: No runtime execution, scheduling, persistence, HTTP API, or analysis +engine is introduced by this issue. The new contract is an authoring/input +surface that is consumed by later run/orchestration work, not an executor. + +## Context + +Issue #675 observes an asymmetry in ACES authoring surfaces. SDL scenarios are +a first-class *authoring* surface: authored `examples/scenarios/*.sdl.yaml` +files, a published `sdl-authoring-input-v1` schema, MCP `sdl_validate` / +`sdl_scaffold` tools, and a discovery test that validates every worked example +against the published schema. + +The experiment-core contracts published under ADR-055, ADR-064, and ADR-065 +(`experiment-run-v1`, `experiment-study-v1`, `experiment-apparatus-context-v1`, +`experiment-task-v1`, capture-spec, evidence, derived-measure) are the +opposite. Every one is framed as an **archival provenance OUTPUT emitted by a +run** — `ExperimentRunModel` is literally "Archival provenance record for one +execution." There is no authoring path — file or MCP tool — to *specify* an +experiment (run count, seeds, red-variant selection, turn order, step count, +termination, condition assignments) as an **input** artifact before execution. +The only in-repo instances are conformance fixtures. + +A full CAGE-2 specification is scenario **plus** experiment. ADR-069 routes the +CAGE-2 scenario half through the existing SDL authoring surface, but the +experiment half — REP-003's "turn order, fixed step counts, episode +termination, red-agent variants, randomization, seeds, and stochastic controls" +(ADR-069 §2, and the execution-control equivalence tier in §7) — has no pre-run +authoring home. "Fully specify the CAGE-2 experiment" is therefore not a +supported workflow today. + +ADR-055 anticipated this exact need but deliberately deferred it. Its §5 states +the design "does not add scheduling, execution, persistence, HTTP APIs, +analysis engines, or new SDL authoring syntax," while "later implementation +work may consume these contracts." Its Risks section is more specific: "If +later APIs need draft or partial experiment records, they must introduce an +explicit draft lifecycle surface rather than weakening these archival +contracts." The EXP design-criteria note (Principle 1) independently warns +against "a single `experiment` object that means scenario, task, run, and study +depending on which fields are present," and (Principle 2) notes that "planned +apparatus and observed apparatus evidence must both be representable." + +## Decision + +Publish a new, separate authoring-input contract `experiment-authoring-input-v1` +(model `ExperimentSpecModel`) that specifies an experiment before it executes. +It is the input counterpart to the archival experiment-core outputs, exactly as +`sdl-authoring-input-v1` is the authored counterpart to `instantiated-scenario-v1`. + +### 1. It is a separate input contract, not a mutation of the archival family + +The archival contracts (`experiment-run-v1`, `experiment-study-v1`, +`experiment-apparatus-context-v1`) are unchanged. The authoring input is a new +closed-world `ContractModel` generated into `contracts/schemas/experiment-core/` +and recorded in the schema-publication manifest, following ADR-055's directive +to introduce an explicit draft/authoring surface rather than weaken the archival +records, and ADR-061's schema-evolution discipline. + +### 2. It references the separated concepts rather than re-declaring them + +Per the separation principle, the spec is a lean design that binds already- +separated concepts: + +- `task_ref` references a separately authored `experiment-task-v1` (which + carries scenario + protocol + metric definitions); the spec does not + duplicate task meaning. +- `intended_scenario_ref` optionally pins the intended scenario snapshot. +- `apparatus_intent` reuses the input-shaped `ExperimentApparatusConstraintModel` + (allowed processors/backends, required manifests/capabilities). It is the + *planned* apparatus, distinct from the run-scoped *observed* + `ExperimentApparatusContextModel`. +- `capture_spec_refs` reference `experiment-capture-spec-v1` artifacts. +- `factors`, `validity_notes`, and `artifact_refs` reuse the existing + experiment-core value models. + +### 3. The run plan carries the pre-run experimental design + +`run_plan` composes reuse and the genuinely-new authoring fields: + +- `stochastic_controls` (reused) declare seeds and randomization. +- `allocation` (reused `ExperimentRunAllocationPlanModel`) declares + condition-based run counts, replication, and condition assignments; or, for a + simple design, `target_run_count` declares a flat count. Exactly one of the + two is present. +- `episode_control` (new) declares turn order, logical step count, and episode + termination — the execution-control facts ADR-069 §7 requires. +- `red_variant_selections` (new, keyed by variant id) select red-agent variants. +- `clock_intent` (reused) declares the intended time domain. + +Identifier-bearing repeated children are keyed object maps (ADR-055 rule). +Cross-map constraints that portable JSON Schema cannot express — exactly-one run +count source, red-variant map-key equality, and blocking factors resolving to +declared factors — are declared as `x-aces-invariants` and enforced by the ACES +model validators, consistent with ADR-055's semantic-invariant profile. + +### 4. Authoring surface parity with SDL + +The contract ships an authored-file convention (`examples/experiments/*.exp.yaml`), +a thin Pydantic loader (`aces_contracts.experiment_spec.load_experiment_spec`; +no SDL-style parser is needed because the document has no shorthand or +cross-file resolution), MCP authoring tools (`experiment_scaffold`, +`experiment_validate`, `experiment_get_example`), and a discovery test that +validates every worked `.exp.yaml` against the published schema — mirroring the +SDL authoring surface and satisfying AUT-801's agent-facing authoring mandate. + +### 5. Runtime and orchestration remain out of scope + +Like ADR-055, this decision publishes a contract, a formal spec, tooling, and +examples. It does not schedule, execute, persist, or evaluate experiments. A +future orchestration surface consumes an authored spec to produce the archival +`experiment-run-v1` / `experiment-study-v1` records. + +## Guardrails + +- Do not treat the authoring input as a run, study, or apparatus-context + record; it is pre-run design, not provenance. +- Do not weaken the archival experiment-core contracts to carry draft or + partial data — that is exactly what this surface exists to avoid. +- Do not duplicate task, scenario, or capture-spec meaning inside the spec; + reference the authored artifacts. +- Do not hand-edit `contracts/schemas/`; change the contract source and + regenerate. +- Do not add a parallel experiment-authoring DSL or SDL section; the spec is a + single nested contract document. + +## Consequences + +### Positive + +- An experiment can be authored and validated before execution, closing the + asymmetry with SDL scenarios and giving REP-003 a home to declare CAGE-2 + execution-control facts pre-run. +- The archival contracts and their scientific boundary are untouched. +- The authoring surface reuses the existing published schema, fixture, + conformance, and MCP machinery rather than inventing new mechanisms. + +### Negative + +- Authors now maintain a task artifact and an experiment-spec artifact when a + task is used in a designed experiment. +- The spec references artifacts (task, capture-spec) by id; cross-artifact + existence is not resolved by the loader and is left to downstream + orchestration. + +### Risks + +- If later orchestration code treats the spec as free-form metadata, the + input/output boundary could blur back into the archival records. Conformance + and review must keep the surfaces distinct. +- The reused `ExperimentRunAllocationPlanModel` annotates its semantic + invariant against `experiment-study-v1`; when embedded in the authoring input + the annotation still names study. This is documented in the formal spec and is + harmless because the model validator runs regardless. + +## Alternatives Considered + +### Add authoring fields to the archival run/study contracts + +Rejected. ADR-055 explicitly forbids weakening the archival contracts to carry +draft/partial data and directs a separate draft lifecycle surface instead. + +### Add a CAGE-specific experiment schema for REP-003 + +Rejected. ADR-069 rejects CAGE-specific schemas; the authoring surface is a +general experiment-core contract that REP-003 uses, not a CAGE fork. + +### Author experiments as a new SDL section + +Rejected. ADR-055's guardrails forbid putting experiment concepts into SDL as a +new root section, and an experiment is not scenario meaning. + +### Defer authoring to a future runtime/API milestone + +Rejected. The issue asks specifically for an authoring surface analogous to SDL; +the contract + tooling can and should exist independently of execution, exactly +as the SDL authoring surface predates full runtime realization. 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 new file mode 100644 index 000000000..35ab8d74f --- /dev/null +++ b/docs/decisions/adrs/adr-075-ecosystem-versioning-deprecation-and-migration-governance.md @@ -0,0 +1,129 @@ +# ADR-075: Ecosystem Versioning, Deprecation, and Migration Governance + +## Status + +proposed + +## Date + +2026-07-11 + +## Classification + +Classification: FM2 +Required artifacts: ADR, normative specification, repository-policy and docs verification +Waivers: none + +## Context + +ACES now has several independently versioned surfaces: + +- the Python distribution and Git tags; +- published JSON Schema contract lineages; +- wire discriminators in closed contract DTOs; +- SDL scenarios and reusable modules; +- processor, backend, participant, and apparatus declarations; +- experiment tasks, runs, studies, and evidence artifacts; and +- ADR and Ground Control workflow statuses. + +These surfaces use similar words, such as version, compatibility, +deprecation, stability, migration, and lifecycle, but they do not all carry the +same semantics. ADR-061 already governs published JSON Schema evolution, while +ADR-010 governs compatibility-only Python import wrappers. Those decisions are +necessary but not sufficient for GOV-901, GOV-902, and GOV-903 because the +ecosystem needs one cross-surface policy that explains how versioning, +deprecation, and migration claims are made without collapsing every surface +into a single package-version model. + +The current repository also has a practical tension: release-please owns the +package release process, the package is still pre-1.0, and all checked-in JSON +Schemas are currently `draft` even when their filenames carry `v1` or `v2` +lineage suffixes. A reader can otherwise infer stronger compatibility +guarantees than the repository can honestly provide. + +## Decision + +Adopt a two-part governance structure: + +1. This ADR records the architectural decision and rationale. +2. `specs/evolution/versioning-deprecation-and-migration.md` is the normative + operational policy for versioning, compatibility, deprecation, removal, and + migration across ecosystem surfaces. + +The policy has these core rules. + +Version identifiers are surface-local. Package SemVer, Git tags, contract +lineage suffixes, wire discriminators, module versions, apparatus versions, +domain artifact versions, ADR status, and requirement status must not be +treated as interchangeable. + +Compatibility claims must name their direction, producer, consumer, surface, +and dimension. The recognized directions are backward, forward, and full +compatibility. The recognized dimensions are structural acceptance, semantic +equivalence, behavioral compatibility, and operational interoperability. + +Stability, deprecation, and removal are separate lifecycle concepts. ADR-061 +`draft` or `stable` classification governs how a schema may evolve; it does +not say whether a surface is deprecated or removed. ADR status and Ground +Control requirement status remain governance workflow state, not artifact +lifecycle records. + +Published JSON Schemas continue to follow ADR-061. This decision narrows the +meaning of "additive" at the ecosystem boundary: an optional schema property or +enum value can be structurally additive to the schema lineage, but it is not an +end-to-end compatibility guarantee unless the relevant installed readers are +shown to accept it or the policy explicitly scopes the claim to schema +structure only. + +Deprecations must be explicit records. A deprecation record identifies the +exact surface, first release or contract lineage carrying the notice, +replacement, migration reference, expected notice window or removal eligibility +rule, verification evidence, and any security exception. + +Migrations are surface-specific. Human migration notes belong under the +existing documentation boundary. Automated migrations are added only when the +transformation is deterministic, idempotent, preserves source data, reports +ambiguous or lossy cases, and fails closed. + +Adapters stay at their owning boundary. The policy does not create a universal +version registry, migration service, runtime endpoint, persistence layer, or +cross-package exception hierarchy. + +## Alternatives Considered + +Extend ADR-061 to govern every ecosystem surface. Rejected: ADR-061 is the +right authority for published JSON Schemas, but package releases, SDL modules, +apparatus manifests, and experiment artifacts have different authorities and +compatibility relations. + +Use Python package SemVer as the single ecosystem version. Rejected: the +package is a release vehicle, not the identity of each contract lineage, +scenario module, processor/backend declaration, or experiment artifact. + +Create a central runtime versioning or migration service. Rejected: the current +need is repository governance, and each executable surface already has an +owning validator, registry, checker, or adapter boundary. A central service +would blur ownership and invite best-effort coercion. + +Treat deprecation as a generic warning. Rejected: lifecycle notice must remain +separate from semantic validity. Deprecated-but-supported use is non-fatal; +removed or unsupported input fails through the owning surface's existing error +envelope. + +## Consequences + +The ecosystem gets one vocabulary for versioning, compatibility, deprecation, +and migration without weakening existing authorities such as ADR-061, +release-please, the schema publication manifest, or module registry checks. + +Future surface families have a clear extension seam: add a row to the +normative surface-class matrix and update that surface's owning checker or +documentation rather than adding a universal runtime abstraction. + +Some existing documentation remains more informal than the new policy. Follow-up +work may need to align release, migration, SDL, API, and conformance prose with +the normative spec. + +Because this ADR is proposed, teams can still refine the surface matrix before +acceptance. Accepted amendments to already accepted ADRs remain governed by +ADR-059; this ADR does not silently edit ADR-061 or any earlier decision. diff --git a/docs/decisions/adrs/adr-076-portable-sdl-identifiers-and-canonical-addresses.md b/docs/decisions/adrs/adr-076-portable-sdl-identifiers-and-canonical-addresses.md new file mode 100644 index 000000000..963805fbe --- /dev/null +++ b/docs/decisions/adrs/adr-076-portable-sdl-identifiers-and-canonical-addresses.md @@ -0,0 +1,646 @@ +# ADR-076: Portable SDL Identifiers and Canonical Addresses + +## Status + +accepted + +## Date + +2026-07-11 + +## Classification + +Classification: FM2 +Required artifacts: ADR, normative SDL specification, published schema updates, +source-ranged migration diagnostics, cross-stage identity tests, and targeted +property/differential tests +Waivers: No new runtime service, persistence layer, authentication mechanism, +or general-purpose identifier registry is introduced. + +## Context + +SDL references already fail closed when a bare name is ambiguous, module +composition rewrites imported symbols into namespaces, and the processor emits +canonical runtime addresses. The lexical inputs to those mechanisms are not yet +coherent. + +Ordinary section keys may be empty or contain whitespace, delimiters, mixed +case, or arbitrary Unicode. Dots currently mean several different things: +section qualification, a nested entity path, the workflow/step boundary, an +authored node name, and a generated module namespace. Runtime-family ids use +the shared `require_symbol` helper, but that helper only rejects empty values +and variable placeholders. Compiler addresses then join unchecked identity +parts with dots. + +This creates two classes of failure. First, a valid declaration may be +impossible to name portably. Second, two different declaration paths can render +as the same string, especially after nested module composition. Existing +indexes often collect strings into sets or dictionaries; if two declarations +already rendered to the same string, those containers erase the evidence of +the collision. + +The repository already has the right cross-cutting owners: the SDL section and +reference catalogs, the source-marked YAML loader, published SDL schemas, +`SemanticValidator`'s declaration index, the module symbol/composition layer, +the runtime-family registry, the objective-window semantic IR, the processor +compiler, and the language-service diagnostic envelope. The identifier contract +must strengthen those owners rather than create parallel schemas, reference +resolvers, exception types, or workflow-specific naming rules. + +## Decision + +### 1. Define one portable local-identifier grammar + +A symbol-defining local identifier is an ASCII string matching: + +```text +portable-id = id-start *63id-char +id-start = %x61-7A / DIGIT +id-char = id-start / "-" / "_" +``` + +Equivalently, it is a full-string match for +`^[a-z0-9][a-z0-9_-]{0,63}$`. Implementations must use full-match semantics; +they must not rely on a regex engine's `$` anchor alone, because some engines +also match `$` immediately before a trailing line terminator. The JSON Schema +form combines `minLength`/`maxLength`, a valid first-character constraint, and +rejection of every character outside `[a-z0-9_-]` so the same contract rejects +trailing newlines, NULs, and other controls without depending on engine-specific +anchor behavior. + +The spelling is exact. SDL does not trim, lowercase, case-fold, Unicode-normalize, +escape, or otherwise repair an identifier. Uppercase, non-ASCII, whitespace, +control characters, `.`, `/`, `:`, and variable placeholders are invalid in a +local identifier. The existing 35-character node-id limit remains a stricter +node-specific constraint on the authored local node segment; composition-added +namespace segments do not consume that local limit. + +The grammar is a string grammar. Because `sdl-yaml/v1` applies YAML 1.2 Core +scalar resolution before model construction, an all-digit authored key must be +quoted to remain a string; decoders must not coerce a numeric key into an id. + +The grammar applies by semantic role, not by field spelling. It covers: + +- `Scenario.name`; +- declaration keys in map-valued SDL sections, including variables; +- nested declaration segments such as entity children, node-local roles, + workflow steps, participant action preconditions/effects, view transitions, + and temporal/disclosure records; +- explicitly addressable nested names such as named services, ACL rules, and + content items; +- the scenario-level `forwarding_agents[*].forwarding_agent_id` list identity; +- every primary and addressable child id in the node-runtime family registry; +- family-local stable ids validated through `require_symbol`, after confirming + that the field is an ACES-local record key rather than provider/native data; +- any later field that becomes a declaration key or canonical-address segment. + +It does not apply merely because a field is named `name` or ends in `_id`. +Display labels, descriptions, usernames, DNS names, URLs, file paths, LDAP DNs, +environment-variable names, versions, contract ids, CVEs, package coordinates, +provider/native ids, external ids, and opaque evidence/provenance refs retain +their owning contracts. If a value must be both human-facing data and an +addressable symbol, its symbol role is the portable id; display or provider data +must remain a separate field rather than weakening the id grammar. + +Declaration identity and reference values remain distinct. A reference-valued +field may continue to accept a full-value variable placeholder where its owning +contract already permits one; instantiation may select a declared target, but it +must never rename or create the target. Variable declarations, module parameter +names, import-parameter keys, and +`scenario-instantiation-request-v1.parameters` keys use the same portable-id +grammar. Parameter *values* retain their owning field contracts and never enter +an address segment. + +Applying the lexical grammar does not make every stable local id a generic +relationship/objective target. Canonical addressability and targetability are +separate catalogued properties: a node-local role can have a stable internal +address without becoming a valid generic target. +`Scenario.name`, `module.id`, and import namespaces identify document and +composition boundaries; they do not become in-scenario generic targets merely +because they use the same segment grammar. +The semantic-role audit must account for every ACES-local declaration identity, +including list-valued and deeply nested records; it must not infer declaration +status from a field suffix or stop at the existing generic-reference targets. +Each in-scenario declaration identity receives one owner-relative canonical +address, while the existing owning catalog records whether that address is +externally targetable. + +### 2. Give modules explicit, segment-safe identity + +`module.id` uses exactly `portable-id "/" portable-id` for +`publisher/name`. Every import declares an explicit `namespace`, and that +namespace is one `portable-id`; filenames, source paths, OCI repository names, +or module display text are never silently converted into namespaces. + +An imported unit that participates as a module declares its module descriptor; +the registry must not manufacture `module.id` from a filename or source path. +`module.exports` keys are existing section-catalog literals, not user ids, and +each exported member must resolve to an actual declaration of that section. +Nested export members such as entity paths are validated as typed declaration +paths, not blindly with the local-id regex and not by arbitrary `getattr` lookup. +The list-valued `forwarding_agents` section participates by its +`forwarding_agent_id`: composition rewrites that id, preserves the list value +shape, and detects collisions by canonical address before concatenating lists. +It must not be dropped because the existing map-section composition loop does +not enumerate it, converted into a public map-shaped SDL section, or exempted +from exports and private-prefix handling. + +Nested imports retain namespace structure as a tuple of segments. Composition +may render that tuple as a dotted prefix, but only the composition layer may +create such generated qualified identities. The generated `__private` segment +is reserved to composition and is never valid author input. Raw/normalized SDL +declarations therefore contain local ids, while expanded SDL may contain +validated generated namespace paths. A local-id validator must not be applied +blindly to trusted expanded keys, and expanded-key acceptance must not become a +way for raw callers to bypass local-id validation. + +The existing `Scenario` / `ExpandedScenario` distinction is the phase and trust +boundary. Public YAML, normalized JSON, and direct model construction validate +`Scenario` local declarations. Only the composition path may construct an +internal `ExpandedScenario`, after validating every imported `Scenario`, module +descriptor, import namespace, and namespace tuple. There is no caller-settable +`expanded=true`, validation-context flag, or permissive fallback that authorizes +dotted declarations. `ExpandedScenario` is not a second public authoring +contract. + +Instantiation preserves the identity phase of its input. A flat authored +scenario produces local keys; a composition-expanded scenario produces the same +validated generated namespace paths. The published instantiated-scenario +contract may therefore describe generated qualified keys, but `parse_sdl` must +never use that derived-document shape to admit dotted raw declarations. +Rebuilding the concrete model must preserve the namespace tuple and prior +collision proof rather than treating the rendered dotted key as trusted by +itself. + +### 3. Treat dots as address syntax, not identifier content + +Canonical declaration addresses are built from typed path parts. Examples are: + +```text +
. +entities. +forwarding_agents. +nodes..roles. +nodes..services. +infrastructure..acls. +content..items. +nodes..runtime..[.....] +workflows..steps. +``` + +`` is a composition-generated namespace path followed by one +local id. Fixed words such as `runtime`, `services`, `acls`, `items`, `steps`, +and registered runtime collection names are grammar literals in their typed +positions. They are not globally banned as local ids; typed construction and +canonical collision detection provide the boundary. Generated `__*` control +segments remain reserved. + +Bare references and compact `.` window references +are lookup aliases, not additional canonical identities. A qualified reference +resolves by exact canonical-address lookup. Resolution must not guess by first +match, declaration order, source locality, delimiter partitioning, or a +longest-node-name heuristic. + +Every declaration is entered into the canonical-address index with its kind and +source provenance before aliases are deduplicated. Two distinct declarations +that render to the same canonical address are a fatal collision, even if a set +or dictionary would otherwise collapse them. This covers collisions between +root and imported declarations, nested entities and namespace paths, and +namespace paths that resemble nested collection paths. + +Processor runtime addresses remain derived artifacts, distinct from SDL +authoring addresses. Existing compiler address builders must consume validated, +structured identity parts, preserve namespace tuples, and reject duplicate +derived addresses before constructing resource dictionaries. Planner, +snapshot, and persistence layers consume those canonical strings; they do not +reinterpret or repair SDL identifiers. + +The compiler boundary must enforce the identity and duplicate-address invariants +even when a caller intentionally skips unrelated semantic checks. The planner +must not merge runtime-family dictionaries with last-write-wins assignment. +Existing planning, backend-result, runtime-snapshot, and persistence contracts +must preserve address equality across every redundant representation: a +snapshot map key equals its embedded `SnapshotEntry.address`, and an operation +or changed-address value denotes the same compiler-owned address used by its +resource or snapshot transition. A mismatch is a contract failure at the +existing `PlanOperationModel` / `RuntimeSnapshotEnvelopeModel` / +`OperationStatusModel`, +`aces_runtime.backend_calls`, or `aces_runtime.control_plane_store` boundary; it +is never repaired by choosing one copy or by parsing the address as an SDL +reference. + +### 4. Enforce the contract at every existing ingress + +The hand-governed schemas under `contracts/schemas/sdl/` remain the normative +machine-readable authority. Identifier-bearing map keys use scoped +`propertyNames` constraints and identifier-bearing fields use the same pattern. +Literal/native maps and reference-keyed maps must not acquire those constraints. +This includes `scenario-instantiation-request-v1.parameters`; its values remain +unconstrained JSON values at that boundary. The Python `schema_bundle()` must +continue to generate identical schemas. + +Schema constraints are phase-specific. The authoring-input schema applies the +local-id grammar to authored declaration keys. The instantiated-scenario schema +must separately describe composition-generated qualified top-level keys while +continuing to constrain local nested segments. Reusing the authoring +`propertyNames` fragment blindly for instantiated output would reject valid +expansion; reusing the instantiated fragment for authoring would admit forged +dotted identities. Generated schemas, the hand-governed copies, and the schema +publication manifest remain synchronized through their existing checks and +change-ledger rules. + +`MappingScope.LITERAL` only means "do not normalize this key as a structural +field"; it is not an identity classification. Source validation must distinguish +declaration keys, reference-keyed maps (for example script event bindings), and +native/data maps (for example labels, facts, driver options, and extensions). +Only the first receives the local-id rule. Reference-keyed maps use their owning +reference grammar, and native/data maps preserve their existing key contract. + +The same rule is enforced at these existing boundaries: + +- the source-marked YAML/key preflight, for precise key ranges and strict + `sdl-yaml/v1` behavior; +- Pydantic model construction, so direct Python and normalized-JSON callers do + not bypass source parsing; +- import-shape validation before resolution, and descriptor/lock validation at + the existing registry boundary before the values are used for composition or + filesystem output; +- module expansion and the semantic declaration index, for generated-address + collisions and exact reference resolution; +- instantiation, which rebuilds the concrete model and re-runs semantic checks; +- language-service completions, references, edits, formatting, and diagnostics; + and +- compiler/canonical-serialization boundaries, which may consume identifiers + but never normalize them. + +Per-document `SDLParserLimits` enforcement is not an aggregate import-graph +budget. A single request-scoped composition budget, carried through the existing +`SDLSourceParseOptions` and registry/composition recursion, must additionally +bound import depth and document count, aggregate decoded bytes and constructed +object/YAML nodes (which conservatively bounds declaration count), +namespace depth, and rendered canonical-address size. Exhaustion fails before +further expansion or compilation through the existing bounded import/parse +diagnostic path. This extends the source-limit seam; it is not a new limits +subsystem, an environment-selected language profile, or a backend setting. + +The existing `VARIABLE_NAME_PATTERN`, runtime `require_symbol`, and the duplicate +stable-id helper in `runtime_ssh_server` must converge on this contract instead +of remaining competing regex/validator families. Every stable-id helper caller +is audited by semantic role before the shared helper is tightened; an +incorrectly classified provider/native field must return to its owning validator +rather than weakening the portable grammar. Existing object-name, path, DNS, +environment-name, and provider/native validators remain distinct because those +values are data, not declarations. +Runtime-family discovery continues to come from +`RUNTIME_SERVICE_FAMILIES`; section and reference discovery continues to come +from the existing SDL catalogs and indexes. `tools/check_sdl_catalog_parity.py` +remains the whole-repository drift gate. The list-valued scenario-level +`forwarding_agents` identity surface remains catalogued explicitly and must not +disappear behind map-only key handling. The language service and MCP inspection +surfaces may retain partial-document presentation logic, but valid-document +definitions and references must come from, or be differentially checked against, +the same declaration index. Existing inventories are reconciled or parity-tested +at their current ownership boundaries; no additional section list, runtime +family list, reference-edge registry, or best-effort semantic resolver is +introduced solely for identifier validation. + +### 5. Fail closed with the existing diagnostic and security boundaries + +Lexically invalid declarations are structural errors. Canonical-address +collisions and reference ambiguity are static-semantic errors. They use the +existing `SDLParseError` / `SDLParseDiagnostic` and `SDLValidationError` +envelopes; no new exception hierarchy is added. Where source marks exist, a +diagnostic identifies the RFC 6901 path and offending declaration-token range, +whether that declaration is a mapping key or a scalar id field. A collision +diagnostic identifies both declarations and their source provenance. +The existing diagnostic records may gain optional related-source/provenance +fields for cross-file collisions; that is an envelope extension, not a parallel +error type. The authoring envelope must not be conflated with +`aces_contracts.diagnostics.Diagnostic`, whose owner is planner/runtime +reporting. `SDLInstantiationError` continues to wrap failures discovered while +rebuilding and revalidating the concrete model, and `ScenarioValidationError` +remains only the existing scenario-loader compatibility adapter. Import/model +validation must not leak a raw Pydantic `ValidationError` through parser or +language-service entry points. + +Migration is explicit and non-lossy. Diagnostics may suggest a conforming id, +but ordinary parsing and `SDLMigrationPolicy.ACCEPT` do not silently trim, +case-fold, normalize Unicode, replace dots, or rewrite references. Any future +rename operation must update a declaration and every resolved reference +atomically, prove that the rename is injective, and fail on ambiguous or lossy +input. + +Identifiers are public structural metadata, never a secret-bearing channel. +Diagnostics and logs may name a validated id. An invalid authored key is +untrusted input: any rendered spelling must be escaped and length-bounded, while +the structured source range remains the primary locator. Neither form may +include adjacent values, the whole document, parameter maps, trust material, +credentials, or tracebacks. Restricting valid identifiers to bounded ASCII +prevents control-character/log injection and prevents module ids or namespaces +from becoming path traversal or shell syntax; it does not make raw invalid-key +rendering safe. + +Module trust, digest/signature verification, registry allowlists, source-size +limits, archive extraction containment, and local-import path containment remain +mandatory and unchanged. Authored namespaces and local module descriptors are +validated before import resolution. A remote descriptor can only be validated +after its bounded, digest-bound config is fetched; it must be validated before +signature acceptance, archive extraction, composition, lockfile emission, or +output/cache path construction. Module ids already flow into signature payloads, +OCI annotations, lock records, and OCI-layout directory names, so those consumers +must receive the validated value and must not re-sanitize it independently. +Validation of `module.id` does not make adjacent `version`, `source`, or +`root_file` values safe path components; their owning version, URL, archive, and +containment checks remain independently mandatory, and an output boundary must +validate or encode every component it places in a filesystem path. +Identifiers are not secret or environment-binding values, and the language +grammar is not selected through an environment variable, deployment setting, or +backend config. Existing OCI and host-tool realization seams retain fixed +list-form argv (never `shell=True`), bounded execution, runtime/tool allowlists, +and native-output redaction. The existing TechVault initramfs seam is narrower +but different: it places a derived hostname into a generated guest `/bin/sh` +script and must retain its dedicated `_shell_quote` boundary; no additional +identifier interpolation or general shell-command builder is introduced there. + +Language-service/MCP symbol, pointer, and prefix arguments and control-plane +compiled-address path parameters remain caller-controlled even after the authored +grammar is tightened. They must be length-bounded at their existing entrypoint +limit/config seam, escaped when rendered, and resolved by exact index/snapshot +lookup. Identifier or migration results may return declaration names and source +ranges, but never adjacent parameter values or document fragments. The runtime +control plane continues to apply `ControlPlaneSecurityConfig` authentication, +authorization, and request guards; it does not parse a URL path value as an SDL +authoring reference or echo an unbounded invalid value in an error envelope. + +Plan submission is a separate runtime trust boundary. `PlanOperationModel` and +its provisioning/orchestration/evaluation envelopes are authenticated wire +input, not proof that an address came from this compiler. Their existing +`ContractModel` / FastAPI conversion boundary is the incumbent shape gate, but +it currently provides closure rather than proof of canonical address +provenance. It must be strengthened, without adding parallel DTOs, to reject +non-canonical or overlong operation addresses, duplicate operation addresses, +dependency or startup-order entries that do not resolve in the admitted plan / +snapshot set, and address / domain / resource-type incoherence before +execution. Backend `ApplyResult` values and locally persisted snapshots are +rechecked independently; neither is trusted merely because an earlier boundary +validated the plan. + +The reusable downstream seam is one bounded compiled-address contract in the +existing `aces_contracts.planning` boundary, consumed by processor plan models, +wire DTOs, runtime state, backend-result gates, and persistence loaders. The +processor's existing `_address` construction helpers remain the renderer and +must emit values accepted by that contract. The address-size bound is a named, +schema-visible contract constant shared by those consumers, not an environment +setting or a collection of endpoint-local limits. This is distinct from the +local SDL identifier validator: it validates compiler address form and size, +not authoring references or provider names. + +Default 4xx rendering, `str(ValueError)` conflict responses, and caught backend +exception text are not currently a sufficient disclosure boundary. Paths that +can include caller- or backend-controlled address text must translate failures +through the existing bounded runtime diagnostic and redacted API envelopes and +must not disclose whole plans, payload values, credentials, tracebacks, or +unbounded address data. + +Provider host/resource names continue through their existing driver-specific +mapping and validation. The portable SDL grammar rejects leading option markers, +path separators, whitespace, and control characters, but it is not a Docker, +libvirt, DNS, hostname, or cloud-provider naming contract. A provider name is a +derived value and never replaces the canonical SDL identity. Bounded provider +names must be deterministically collision-resistant for the complete canonical +address, for example a sanitized readable prefix plus a digest suffix; plain +truncation is not an identity-preserving mapping. + +### 5.1 Formal obligations + +Let `P` be the set of portable local identifiers, `N = P*` the finite namespace +paths generated by composition, and `Q = N x P` the qualified symbols. Let +`D(S)` be the declarations of a validated expanded scenario `S`, with each +declaration retaining its semantic kind and owner path. The typed canonical +address renderer `A_S : D(S) -> String` must satisfy document-scoped +injectivity: + +```text +forall d1, d2 in D(S): A_S(d1) = A_S(d2) implies d1 = d2 +``` + +This is proved operationally by retaining every declaration and its provenance +until collision checking completes; inserting rendered names into a set or map +before that check is not a proof. The renderer is not claimed to be globally +injective over untyped strings. Its domain includes the declaration kind, +owner-relative path, registered fixed segments, namespace tuple, and local id. + +Instantiation parameters are identity-independent. If `instantiate(S, p)` and +`instantiate(S, q)` both succeed for parameter environments `p` and `q`, their +declaration-address domains are equal. Parameter values may select references +or data but cannot create, delete, or rename declarations: + +```text +dom(A_instantiate(S, p)) = dom(A_instantiate(S, q)) = dom(A_S) +``` + +Every resolved reference must have cardinality exactly one in the typed +declaration index. Zero matches is unresolved; more than one is ambiguous. +Resolution order, source proximity, and delimiter partitioning do not alter that +cardinality. + +Across processor, plan, backend, and snapshot stages, canonical addresses form a +commuting identity carrier: redundant representations must compare equal to the +compiler-emitted address, and a transition may mention only admitted addresses. +Provider naming is a projection from that complete address into a provider's +bounded name domain. It is never the inverse identity map and therefore retains +a deterministic digest of the complete source address. + +### 5.2 Standards lineage and limits + +The grammar notation follows RFC 5234 ABNF conventions. The segment/delimiter +separation is influenced by RFC 3986, but a canonical SDL address is not a URI +and does not inherit URI resolution, normalization, percent-encoding, or +equivalence semantics. RFC 6901 defines diagnostic JSON Pointer locations; a +pointer is not an SDL declaration identity or reference. RFC 8785 defines the +canonical JSON serialization used for document semantic identity; it does not +define SDL identifier equality or lineage. + +Unicode Standard Annex #15 and Unicode Technical Standard #39 inform the +decision to avoid silent normalization and confusable-sensitive identifier +repair. ACES does not claim conformance to a Unicode identifier profile: SDL +structural identifiers deliberately use bounded lowercase ASCII, while display +and data fields retain their owning Unicode contracts. + +This identifier decision does not claim or define Park bisimulation, labelled +transition-system equivalence, observational equivalence, or multi-agent +behavioral equivalence. Those concepts apply to the behavior and observation +semantics of scenarios, not to the lexical identity carrier established here. +Any future bisimulation-based scenario equivalence must state its transition +system, labels, observations, and equivalence relation independently; stable +injective addresses are supporting structure, not evidence of behavioral +equivalence. + +Canonical SDL authoring semantic identity remains RFC 8785 over the validated +expanded authoring model. +Display/data strings retain their authored Unicode code points; only identifiers +are restricted to ASCII. The control-plane authentication/authorization gates +and atomic local snapshot store remain downstream consumers of compiled +addresses and gain no new authoring ingress. Compiled addresses may appear as +snapshot/audit keys and workflow URL path parameters, but runtime DTOs, backend +validators, and persistence must treat them as typed compiler output rather than +re-parsing or repairing authoring references. Downstream planner and backend code +may use the compiled address grammar for deterministic ordering or provider-name +derivation only at their existing, explicit seams. + +### 6. Preserve the extension seams already present + +Namespace depth is represented as a sequence (`namespace_path` already exists +in the objective/reference semantic IR), not inferred by repeatedly splitting a +rendered string. Composition, declaration indexing, semantic IR, and compiler +address construction carry that sequence until one canonical renderer produces +the external string. A new top-level section extends the existing +section/reference catalogs; a new runtime family extends +`RUNTIME_SERVICE_FAMILIES`; both then use the same local-id and address rules +automatically. + +A future internationalized identifier grammar, escaping syntax, or different +case policy is a new language/contract decision with an explicit migration and +schema-version assessment. It is not an in-place widening of this regex. +The grammar and its length bound are therefore versioned language seams, not +runtime configuration parameters. + +On acceptance, the normative SDL document model, section/reference/runtime +catalogs, diagnostics, variable/instantiation contract, composition guidance, +and parser guide must be reconciled in one change. In particular, the current +dotted-node and longest-match rules and the current implicit local-import +namespace behavior are superseded. Accepted ADRs remain immutable; conflicting +guidance in ADR-003 or ADR-053 is changed only through this explicit superseding +decision and corresponding normative documentation, not by editing their +accepted records in place. + +## Guardrails + +- Do not validate every `name`, `id`, or mapping key as an SDL symbol; classify + the field's semantic role first. +- Do not implement the schema rule with a `$`-anchored pattern alone; prove + parity for final line terminators, controls, and maximum length across YAML, + Pydantic, and the published JSON Schemas. +- Do not treat `_mapping_scopes.MappingScope.LITERAL` as proof that a key is a + declaration, or as proof that it is native data. +- Do not keep dotted authored node ids as a special case. +- Do not derive namespaces from paths or sanitize invalid ids into valid ones. +- Do not synthesize module identity from a descriptor-less import path, or treat + module export section names as arbitrary attributes. +- Do not treat one validated path component as validation of an output path that + also contains version, source, archive-member, or provider-native data. +- Do not expose a public trusted/expanded switch or accept authored dotted keys + through `ExpandedScenario` as a convenience path. +- Do not make identifier grammar, normalization, or maximum length selectable + through environment, backend, parser, or deployment configuration. +- Do not exempt synthetic wrapper/scaffold identifiers used by MCP, language + service, tests, or fixtures; those callers enter through the same authoring + contract and must use conforming local ids. +- Do not treat per-document parser limits as protection against an aggregate + nested-import expansion; carry one bounded composition budget through the + complete import graph. +- Do not parse qualified refs independently in each section with `split`, + `partition`, or `rsplit`; resolve through the canonical declaration index. +- Do not let a set/dict insertion serve as collision detection after provenance + has already been erased. +- Do not apply the authoring key regex to composition-generated instantiated + keys, or use the instantiated key shape as an authoring bypass. +- Do not let compiler semantic-validation options bypass identity/collision + checks, merge planner resources with last-write-wins behavior, or accept a + snapshot whose map key differs from its embedded address. +- Do not treat authentication, a recognized address prefix, or Pydantic shape + validation alone as proof that a plan, backend result, or persisted snapshot + carries coherent compiler-owned addresses. +- Do not omit or map-convert `forwarding_agents` during composition merely + because the incumbent composition registry is map-section-oriented; compose + the list by its declared identity and preserve its published list shape. +- Do not add schema-only, Pydantic-only, or semantic-validator-only enforcement; + raw YAML, normalized JSON, direct model construction, module expansion, and + compiled output must agree. +- Do not add a second exception hierarchy, migration policy, section catalog, + runtime-family registry, or workflow-step resolver. +- Do not change display labels, provider-native ids, paths, URLs, DNS names, + environment names, or external contract identifiers to make them resemble + SDL symbols. +- Do not use provider-safe runtime names as canonical SDL identities or assume + the portable-id grammar satisfies every provider's length and hostname rules. +- Do not truncate provider names without retaining a deterministic digest of the + complete canonical address. +- Do not surface raw Pydantic error renderings when they can include adjacent + values; translate failures into the existing bounded diagnostic envelopes. +- Do not write an invalid key verbatim to logs or diagnostics; escape and bound + its presentation and use the source range as the locator. +- Do not echo unbounded language-tool query strings, URL path parameters, + document fragments, or instantiation values in identifier diagnostics. +- Do not expose raw backend exception text or complete invalid plan/payload + values through 4xx responses, runtime diagnostics, logs, or audit events. +- Do not interpolate identifiers into new shell command strings; retain the + dedicated quoting boundary on the existing generated guest-init script. + +## Non-Goals + +- Escaping arbitrary legacy identifiers inside the current dotted syntax. +- Automatic or best-effort renaming of existing scenarios. +- Case-insensitive or Unicode-normalized identity. +- New variable types, substitution syntax, binding precedence, or parameter + value semantics; this decision only prevents values from changing declaration + identity. +- Changing bare-reference ambiguity semantics or allowing implicit target + creation. +- A general redesign of the source/normalized/expanded/instantiated document + phases beyond the identity provenance needed to keep qualified segments safe. +- Redesigning module trust, OCI transport, lockfiles, runtime authentication, + persistence, or backend protocols. +- Making compiled runtime addresses interchangeable with SDL authoring + references. +- Promoting every local stable id to generic relationship/objective targetability. +- Changing environment-variable, provider/native, external, display, path, URL, + DNS, or other data-field grammars. +- Adding a universal identity service or value-object hierarchy. + +## Alternatives Considered + +### Keep arbitrary ids and use longest-match parsing + +Rejected. Meaning would depend on the declarations currently in the symbol +table, and newly imported declarations could change how an existing string is +parsed. It also leaves compiler and persistence addresses structurally opaque. + +### Escape dots and other delimiters + +Rejected for the current language. Escaping would have to agree across YAML, +JSON Schema, module rewriting, workflow-step refs, compiler addresses, +language-service tools, logs, and persisted snapshots. A small portable segment +grammar is easier to implement consistently and audit. + +### Normalize case or Unicode + +Rejected. Normalization is lossy, can merge previously distinct declarations, +and creates cross-language/version dependencies. Lowercase ASCII ids plus +unrestricted Unicode display/data fields make the boundary explicit. + +### Apply one regex to every `name`, `_id`, and map key + +Rejected. Many such fields are labels, provider data, external identifiers, +native option maps, or references rather than SDL declarations. Suffix- or +shape-based validation would conflate concepts and break legitimate data. + +## Consequences + +Every valid SDL declaration has exactly one portable canonical address, and any +collision is reported before composition, compilation, or persistence can +silently overwrite it. Reference tooling and runtime compilation can share one +identity contract without losing the existing section-, workflow-, or +runtime-family-specific semantics. + +The change is intentionally strict and therefore migration-bearing. Existing +dotted nodes, mixed-case/file-like targetable names, descriptor-less imported +fragments, implicit local-import namespaces, and other nonconforming declarations +must be migrated explicitly. +Published SDL schemas are currently `draft`, but every tightening still follows +ADR-061 and the schema-publication manifest/change-ledger rules. + +The grammar does not make every dotted string self-describing. Canonical +addresses remain typed paths, and collision-free meaning comes from structured +construction plus exact declaration-index lookup. This avoids both a global +reserved-word list and a new general-purpose address parser. diff --git a/docs/decisions/adrs/adr-077-associated-artifact-manifest-boundary.md b/docs/decisions/adrs/adr-077-associated-artifact-manifest-boundary.md new file mode 100644 index 000000000..f7cc2ff9c --- /dev/null +++ b/docs/decisions/adrs/adr-077-associated-artifact-manifest-boundary.md @@ -0,0 +1,107 @@ +# ADR-077: Associated Artifact Manifest Boundary + +## Status + +accepted + +## Date + +2026-07-12 + +## Classification + +Classification: FM1 +Required artifacts: ADR, normative spec, schema, fixtures, contract tests +Waivers: none + +## Context + +ACES gives validated, expanded SDL a canonical semantic digest and gives +experiment artifacts checksum-bearing references. Neither surface defines one +portable set of non-semantic artifacts attached to a scenario, sealed scenario +snapshot, or experiment artifact. Downstream packaging and ingestion tools +therefore cannot distinguish a verified companion set from a caller-asserted +package digest without inventing an asset model outside ACES. + +The missing model must preserve existing boundaries. Documentation, operator +material, evaluator assets, reports, profiles, and similar bytes are not SDL +meaning. A live directory is not an atomic snapshot. An artifact checksum is +not a manifest identity, and neither a checksum nor a manifest digest proves +authenticity. Attachment at one scope must not silently imply attachment at +another scope. + +## Decision + +Publish `associated-artifact-manifest-v1` as a closed, standalone contract. One +manifest attaches one exact keyed artifact-reference set to one explicit +parent. Its scope is either: + +- `scenario`, with a `scenario` or `scenario-snapshot` parent; or +- `experiment`, with a task, authoring input, apparatus context, run, or study + parent. + +The contract reuses the experiment-core typed-reference, checksum, and artifact +descriptor shapes. It adds the missing `authoring-input` reference kind and +shared companion roles without creating a scenario-only descriptor or a +universal `TrustedAsset` payload. Generic scenario parents remain id-only; +scenario snapshots may carry the existing version and semantic-digest binding. +Experiment parents do not gain self-asserted payload digests. + +The logical manifest id/version is separate from the derived +`associated-artifact-set/v1` identity. Set identity is lowercase SHA-256 over +RFC 8785 canonical bytes containing the profile id, scope, exact parent +reference, and exact keyed artifact-reference set. It excludes the set digest +itself and all filesystem, archive, OCI, filename, materialization, and export +layout metadata. Changing the parent or any artifact reference changes set +identity; changing only a downstream layout does not. + +Full conformance requires the cross-artifact validator to match the concrete +parent, derive the set digest, and recompute every artifact checksum and size +from exactly one explicitly supplied, bounded byte reader. The validator does +not fetch locators, walk directories, unpack archives, follow symlinks, or read +ambient configuration. A digest, path, URI, or prior validation flag is not a +byte binding. + +Add `associated_artifact_set` as a distinct reusable-asset family under +ADR-071. Its set digest supplies `integrity_digest`; every payload supplies +`artifact_checksum`. The parent retains its existing identity and integrity +mechanism. Authenticity remains an independent signature/trust-policy decision. + +Attachments never inherit between scenario, snapshot, task, authoring input, +apparatus context, run, and study. Attaching the same bytes to another parent +requires another manifest and therefore another set digest. + +## Consequences + +### Positive + +- Producers and consumers share one portable parent-plus-artifact-set contract + without importing scenario-pack filesystem layout into ACES. +- Caller-asserted package digests cannot establish conformance or trust. +- Scenario meaning, parent identity, set identity, raw checksums, and + authenticity remain independently testable claims. +- The typed parent matcher, versioned canonicalization profile, shared role + vocabulary, and injected byte-reader seam admit future families and transports + without changing SDL semantics. + +### Negative + +- Full validation requires callers to stage and supply every referenced + payload, even in offline workflows. +- Adding parent families or canonicalization revisions requires a contract + revision and focused compatibility evidence. + +### Risks + +- Consumers may validate schema shape but skip the required semantic byte gate; + the published `x-aces-invariants` annotation and conformance diagnostics make + that limitation explicit. +- Mutable locators can drift; immutable staging and use-time verification remain + consumer responsibilities. + +## Non-goals + +This decision does not define scenario-pack directories, manifest filenames, +archive or OCI layout, release tiers, catalog metadata, acquisition, storage, +entitlement, launchability, a registry, new cryptography, or an API/persistence +service. It does not change SDL syntax or `canonical_sdl_digest()`. diff --git a/docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md b/docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md new file mode 100644 index 000000000..c18322dbc --- /dev/null +++ b/docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md @@ -0,0 +1,264 @@ +# ADR-078: Closed SDL Phase Contracts and Portable Derivation Evidence + +## Status + +accepted + +## Date + +2026-07-12 + +## Classification + +Classification: FM2 + +Required artifacts: ADR, normative phase invariants, distinct typed phase +representations, published schemas and fixtures, cross-phase conversion tests, +and serialized-artifact differential tests. + +Waivers: no theorem prover or static typestate checker is required. The Python +reference implementation enforces these contracts at runtime and does not claim +that arbitrary Python code cannot use unsafe construction APIs. + +## Context + +ACES previously represented an instantiated scenario by subclassing the +authoring model. The generated `instantiated-scenario-v1` schema therefore +retained `variables`, `imports`, and `module`, even though the normative SDL +prose said variable definitions do not survive instantiation. Resolution facts +needed by the processor lived in Python private attributes. A deserialized +artifact could lose those facts, and a caller could ask the compiler to skip +semantic validation. + +The phase name consequently promised more than the type and schema guaranteed. +That is unacceptable for an exchange artifact used in reproducible experiments: +an independent producer must be able to tell which fields belong to a phase, +an independent consumer must be able to reject phase-inappropriate fields, and +the processor must not obtain different meaning from in-process private state. + +## Decision + +### 1. Use disjoint closed representations + +Let `C` be the common executable scenario sections. ACES defines these phase +shapes: + +```text +NormalizedAuthoring = C + {module, imports, variables} +ExpandedAuthoring = C + {variables, expansion_provenance} +Instantiated = C + {instantiation_provenance} +Snapshot = {profile, scenario: Instantiated} +``` + +The sets are exact. A field not shown for a phase is forbidden, including an +empty `variables: {}`, empty `imports: []`, or `module: null` shell. + +`Scenario` is the normalized authoring representation. It accepts only local +declaration identities. `ExpandedScenario` is an internal, trusted result of +module resolution and namespace rewriting. It may carry generated qualified +identities and remaining root variables, but it has no `module` or `imports` +member. `InstantiatedScenario` is not a subtype of `Scenario`; it has no +authoring-machinery member or compatibility property. The private structural +binder may construct bound content but cannot mint the public instantiated +type. + +`ExpandedScenario` is not published as an exchange schema. It carries +resolver-produced trust context and exists only between composition and final +instantiation. The two public derived contracts are +`instantiated-scenario-v1` and +`instantiated-scenario-snapshot-v1`. + +### 2. Make phase transitions partial and fail closed + +The supported public path is: + +```text +sdl-yaml/v1 + -> normalized Scenario + -> trusted ExpandedScenario (when imports exist) + -> InstantiatedScenario + -> optional canonical snapshot +``` + +Each arrow is a partial function: invalid input has no output artifact. +`instantiate_scenario()` always validates its authoring input, binds once, +reconstructs the closed instantiated representation, validates provenance +consistency and absence of substitution tokens, and reruns semantic validation. +There is no public `validate_semantics=False` option. + +Direct or deserialized instantiated artifacts use +`admit_instantiated_scenario()`. Compiler entry points apply that same semantic +admission before lowering. Canonical snapshot creation also admits the artifact. +Pydantic's `model_construct()` remains an explicitly unsafe framework escape +hatch and is not an ACES ingestion API. + +### 3. Carry replay-relevant derivation evidence on the wire + +Every instantiated artifact contains a required, closed +`instantiation_provenance` value with: + +- the profile-labelled digest of the semantically validated expanded authoring + object; +- the selected instantiation profile, when one was selected; +- root parameter bindings, each with a segment-valued identity, origin + (`provided` or `default`), and scalar JSON value; +- resolved imports in declared preorder, with namespace segments, requested + selector, resolved module identity, checkout-independent source identity, + content/manifest/export digests when applicable, signer id when applicable, + and module-local bindings; +- the narrow capability constraints needed after substitution for + `nodes..os` and `infrastructure..count`, addressed by RFC 6901 JSON + Pointer and qualified parameter identity; and +- portable explicitness records needed to preserve SEM-218 realization meaning + across serialization. + +The provenance records are closed and immutable value objects in the reference +API. Binding values are limited to the SDL variable domain: JSON string, +integer, finite number, or boolean. Equality follows JSON semantics: +booleans are distinct from numbers, while numerically equal JSON numbers are +equal. Every capability parameter must resolve to exactly one root or imported +binding, its selected value must belong to the retained domain, and its pointer +must resolve to the same concrete field value. Explicitness parameter +identities must likewise resolve. + +Resolved source identities never contain absolute host/cache paths or registry +credentials. The artifact does not retain trust-policy contents, headers, +credentials, cache locations, raw signatures, or source documents. Signer id is +an attribution fact from an already completed trust check, not a signature. + +The compiler lowers each portable capability fact to one typed +`CompiledCapabilityConstraint` addressed to a compiled resource. The planner +does not reconstruct variable declarations or coordinate a variable-spec map +with a second field-reference map. + +`instantiation_provenance` is artifact metadata, not a backend-realizable SDL +dimension. Realization-envelope membership projects fields annotated +`x-aces-realization-dimension: false` out of closed-world child enumeration. +The field remains required, validated, and included in snapshot identity; the +projection only prevents a backend capability envelope from claiming that it +must realize the derivation record itself. + +This evidence provides a replay recipe and verification anchors. It does not +make a replay self-contained: source availability, the referenced module bytes, +and an independently selected trust policy are still required to repeat import +resolution. A provenance record is a claim carried by an artifact, not proof +that a named process actually ran. + +### 4. Separate authored identity from instantiated snapshot identity + +`aces-sdl-semantic/v1` continues to identify the validated expanded authoring +meaning. Its envelope and bytes are not redefined by this decision. The legacy +`module_variable_specs` and `module_node_variable_refs` members in that profile +remain compatibility projections derived from typed expansion evidence; they +are not live instantiated fields. + +`aces-sdl-instantiated-snapshot/v1` is a separate envelope containing an +explicit profile label and exactly one admitted `InstantiatedScenario`. It is +serialized with RFC 8785 JCS and digested with SHA-256. A provenance change +therefore changes the snapshot digest even if the concrete scenario sections +are unchanged. + +The snapshot is an identity for bytes and meaning under this profile. It is not +a claim of behavioral equivalence between scenarios, apparatuses, participants, +or runs. In particular, digest equality is not Park-Milner bisimilarity. Any +future behavioral-equivalence claim must define the labelled transition system, +observable actions, hidden actions, and bisimulation relation it uses. + +### 5. State the schema and semantic boundaries separately + +The Draft 2020-12 schemas enforce closed object shapes, required profile and +provenance members, identifier grammars, scalar constraints, and absence of +`${name}` in any instantiated string. Model validation additionally enforces +provenance collection invariants. Semantic admission enforces cross-reference, +graph, and pointer-to-concrete-value relations that JSON Schema does not express. + +Therefore: + +```text +admitted(x) = schema_valid(x) + and provenance_consistent(x) + and semantic_valid(x) +``` + +Schema validity alone is necessary but not sufficient for compiler admission. +Documentation and diagnostics must not collapse these layers into a claim that +the JSON Schema proves all SDL semantics. + +### 6. Keep sensitive values off secondary response surfaces + +Parameter values necessarily appear where substitution places them and in the +replay binding record. Tools must not duplicate them into summaries, +diagnostics, logs, or operation metadata. Binding failures report bounded +locations and error classes without rendering supplied values or allowed-value +domains. MCP authoring/operation responses report binding and import counts, +not a second raw parameter map. + +## Lineage And Limits + +The separation of operations by object state is informed by Strom and Yemini's +[typestate](https://doi.org/10.1109/TSE.1986.6312929) work. ACES adopts the +design lesson of distinct state-indexed operation surfaces, not its compile-time +guarantee: Python/Pydantic validation here is dynamic, and the transition types +are not a dependent-type proof. + +The provenance vocabulary is conceptually aligned with the W3C +[PROV Data Model](https://www.w3.org/TR/prov-dm/): scenarios are entity-like +artifacts, composition/instantiation are activity-like transformations, and +bindings/digests describe derivation context. ACES does not serialize PROV-N, +PROV-XML, or PROV-O, does not carry the complete PROV relation set, and makes no +W3C PROV conformance claim. + +Closed exchange shapes use +[JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12/json-schema-core). +Deterministic snapshot bytes use the Informational +[RFC 8785 JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html). +Neither source defines SDL semantics, proves provenance truth, or establishes +behavioral equivalence. + +The distinction from behavioral equivalence follows the original line of work +from Park's +[Concurrency and Automata on Infinite Sequences](https://wrap.warwick.ac.uk/47224/). +This ADR deliberately establishes artifact phase identity only; operational and +multi-agent observational semantics remain separate work. + +## Consequences + +Positive: + +- a schema-valid instantiated payload cannot retain authoring machinery or a + substitution token; +- serialization no longer drops compiler-relevant capability or explicitness + meaning; +- module resolution evidence is checkout-independent and ordered; +- direct construction cannot bypass compiler semantic admission; and +- authored and instantiated canonical identities cannot be confused. + +Costs and compatibility: + +- `instantiated-scenario-v1` is tightened while it remains draft; payloads + created under the former permissive shape must be reinstantiated; +- every instantiated artifact is larger because derivation evidence is + explicit; +- semantic validation runs at public instantiation and direct-artifact + admission, including compiler and snapshot boundaries; and +- consumers of former Python private fields must migrate to typed provenance. + +## Verification + +The FM2 evidence consists of: + +- `specs/formal/sdl-phases/README.md`; +- `contracts/schemas/sdl/instantiated-scenario-v1.json` and + `instantiated-scenario-snapshot-v1.json`; +- cross-phase valid/invalid fixtures under `contracts/fixtures/sdl/`; +- `implementations/python/packages/aces_sdl/phase_contracts.py`; +- `implementations/python/tests/test_sdl_phase_contracts.py`; +- `implementations/python/tests/test_instantiated_scenario_schema.py`; and +- compiler/MCP regression tests proving serialized equivalence and bounded + disclosure. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-12 | #724 | Clarified that required instantiation provenance remains validated artifact identity metadata but is excluded from realization-envelope child-dimension enumeration. | diff --git a/docs/decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md b/docs/decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md new file mode 100644 index 000000000..f2c7a4438 --- /dev/null +++ b/docs/decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md @@ -0,0 +1,185 @@ +# ADR-079: Backend-Neutral Proposition and Truth Semantics + +## Status + +accepted + +## Date + +2026-07-12 + +## Classification + +Classification: FM2 + +Required artifacts: closed authoring models, pure truth tables, semantic +validation, compiled resources, capability declarations, portable result +contracts, published schemas and fixtures, cross-backend fixtures, and explicit +migration diagnostics. + +Waivers: ACES does not claim a complete temporal logic, a theorem-proved +evaluator, or behavioral equivalence between backends. Runtime checks exercise +the finite algebra and contract invariants implemented here. + +## Context + +ACES inherited `Condition` as an executable command-plus-schedule or package +source. ADR-002 and ADR-073 later described conditions as observable state and +made `objectives.*.success.conditions` the meaning of success. Those statements +were inconsistent with the actual model: command text, exit status, polling +interval, and package identity specify a probe implementation, not a +proposition about the scenario world. + +That conflation prevents independent review and reproducible comparison. Two +backends can run different probes under the same condition name without a +shared statement of what is expected, and a successful process can be mistaken +for a true world-state claim. It also collapses false, insufficient evidence, +probe failure, and absent backend capability. + +## Decision + +### 1. Separate four contracts + +ACES separates: + +1. A **proposition**: a named, inert claim over a finite set of canonical SDL + subjects. It declares whether its facts come from admitted declared state or + observed state, a closed typed predicate, explicit quantification, and any + authored evidence requirements. +2. An **assertion**: one use of a proposition as a precondition, invariant, or + postcondition, with positive or negative polarity. +3. A **probe binding**: a capability-bound realization of a proposition. A + legacy `Condition.command` or `Condition.source` may supply implementation + mechanics only when it explicitly names the proposition it realizes. +4. A **truth result**: an evidence-bearing evaluation record that preserves + proposition and assertion identity, polarity, realization provenance, + governed boundary/clock context, evidence references, and loss disclosure. + +None of these is a score, reward, participant perception, evaluator lifecycle +status, or command result. `EvaluationResultState.passed` remains a lifecycle +surface and cannot substitute for a proposition truth result. + +### 2. Use a closed typed predicate language + +The initial predicate families are presence, Boolean, string/membership, and +numeric comparison. Each family admits only operators meaningful for its +operand type. Numeric predicates name a unit and its semantic reference. + +Every predicate carries both a readable local property name and a stable HTTPS +or URN semantic reference. The reference is the cross-system identity; prose +description is explanatory, not a substitute for it. The language admits no +shell, Python, callback, regular-expression policy, JSONPath, JMESPath, or +backend-native query expression. + +Subjects are explicit canonical SDL references and therefore form a finite set +after instantiation. Quantification is `all`, `any`, or `at_least`; an +`at_least` threshold cannot exceed the subject count. This issue does not add +unbounded domain quantification or dynamic discovery semantics. +Numeric operands must be finite; NaN and infinities are not portable comparison +values. + +### 3. Define the portable outcome algebra precisely + +The semantic truth values are `true`, `false`, and `unknown`. This is consistent +with finite-observation runtime-verification practice in which a partial trace +may be inconclusive. `unsupported` is not a fourth logical truth value: it is a +realization disposition stating that the selected apparatus cannot evaluate +the proposition with the required predicate, evidence, or time guarantees. +ACES includes it in the portable outcome enum so an adapter cannot collapse +capability absence into unknown or false. + +Negation swaps only true and false. It preserves unknown and unsupported. + +For `all_of`, false is decisive; all true yields true; otherwise unsupported +dominates unknown. For `any_of`, true is decisive; all false yields false; +otherwise unsupported dominates unknown. For `at_least(k)`, at least `k` true +values yields true; fewer than `k` possible true values after counting unknown +and unsupported yields false; otherwise the result is unsupported when any +required undecided operand is unsupported, and unknown otherwise. Empty +composition and invalid thresholds are rejected. + +The same tables aggregate per-subject predicate outcomes: proposition `all` +maps to `all_of`, `any` maps to `any_of`, and `at_least(k)` maps directly to +`at_least(k)`. + +Conflicting evidence is `unknown` with a typed reason. ACES does not claim to +implement Belnap's four-valued paraconsistent logic. Partial, redacted, or lossy +evidence may decide true or false only when every disclosed loss remains within +an admitted bound. Ordinary absence of an observation is unknown; it is not +evidence of falsity. + +### 4. Keep time and evidence claims governed + +Preconditions apply at a governed start boundary, postconditions at a governed +completion boundary, and invariants over the owning governed window. Objective +window references determine orchestration scope and refresh dependencies; they +do not establish a clock. + +An observed decided result must name a boundary, time domain, and clock +authority. This decision does not promote participant-specific time carriers +into a general SDL clock or invent backend-local wall-clock semantics. A +backend unable to supply the required governed temporal context reports +unsupported. A sampled observation does not prove an interval invariant unless +a later governed observation contract defines and satisfies that coverage +claim. + +Authored evidence requirements remain capture intent. A decided observed result +must cite captured evidence and a digest-identified probe binding. Provenance +supports audit and replay; it does not prove that the referenced process ran or +that its evidence is truthful. + +Runtime admission also requires each result to reference the admitted +proposition and assertion entries whose basis, proposition link, and polarity +match the result. Envelope validity alone cannot authorize a detached truth +claim. + +### 5. Replace ambiguous consumers and fail migration closed + +Objective success composes invariant and postcondition assertion references. +Event triggers, workflow predicates, and participant starting state consume +precondition assertion references. `nodes.*.conditions` remains probe +placement; it does not become a state claim. + +The removed `success.conditions`, event/workflow condition predicates, and +`starting_conditions` fields receive bounded migration errors. A migrator may +carry command/source mechanics into a probe binding, but it cannot infer a +property, expected value, polarity, quantifier, evidence adequacy, or temporal +meaning from command text or package identity. Existing identifiers may be +retained only when the author supplies the missing proposition/assertion +meaning explicitly. + +### 6. Bound equivalence claims + +Two results from different probe bindings are **semantically claim-equivalent** +when their proposition, assertion, polarity, outcome, basis, governed temporal +context, loss disclosure, and unsupported capability set agree. Their evidence +and binding provenance remain distinct. + +This equality is not observational equivalence of the backends. ACES does not +claim Park-Milner bisimulation without a declared labelled transition system, +observable and hidden actions, and a bisimulation relation. Result equality is +also not evidence equality, replay equivalence, or apparatus equivalence. + +## Consequences + +Authors can review objective truth from SDL without executing arbitrary code, +and backends can disclose inability to realize a claim without falsifying it. +The cost is a deliberately breaking migration from ambiguous condition +references and more explicit evidence/capability metadata. + +The initial property vocabulary remains extensible through stable semantic +references. ACES does not yet publish a complete cyber-observable ontology; +semantic references make that limitation visible and permit later governed +profiles without changing the predicate algebra. + +## References + +- Andreas Bauer, Martin Leucker, and Christian Schallhart, "Runtime + Verification for LTL and TLTL," *ACM Transactions on Software Engineering and + Methodology* 20(4), 2011. +- Ron Koymans, "Specifying Real-Time Properties with Metric Temporal Logic," + *Real-Time Systems* 2, 1990. +- David Park, "Concurrency and Automata on Infinite Sequences," LNCS 104, + 1981. +- W3C, *PROV-DM: The PROV Data Model*, Recommendation, 2013. + diff --git a/docs/decisions/adrs/adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md b/docs/decisions/adrs/adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md new file mode 100644 index 000000000..fefe3b1c7 --- /dev/null +++ b/docs/decisions/adrs/adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md @@ -0,0 +1,86 @@ +# ADR-080: Revision-Pinned SDL Lineage And Provenance Ledger + +## Status + +accepted + +## Date + +2026-07-12 + +## Classification + +Classification: FM1 + +Required artifacts: an explicit invariant list, closed contract models, +published JSON Schema, exact bidirectional catalog coverage, deterministic +offline integrity checks, and unit/integration tests for failure modes and +distribution-visible notices. + +Waivers: live DOI, source-host, and documentation availability checks are a +bounded audit rather than a CI invariant. The committed evidence preserves the +review result without making network availability part of repository truth. + +## Context + +ACES openly acknowledges that it is not a clean-room language, but narrative +lineage tables did not distinguish conceptual influence, translated code, +example similarity, and compatibility. They also did not pin the OCR revision, +prove coverage of the live language catalogs, or record a resolved notice +disposition. That ambiguity is unacceptable for academic review and downstream +distribution. + +## Decision + +1. `contracts/provenance/sdl-lineage-ledger-v1.json` is the normative lineage, + derivation, citation-identity, and notice-disposition record. ADR-019 + registers `contracts/provenance/` as a distinct authority root. +2. Every current top-level authoring field, node runtime family, concept + family, and reference model has a namespaced subject record whose authority + coordinate resolves to its existing canonical artifact. Coverage is exact + and bidirectional; matching counts are insufficient. +3. Claims separate syntax, semantics, artifact/code derivation, and examples. + `adopted_syntax`, `adopted_semantics`, `adapted`, and `aces_native` classify + provenance. `current`, `removed`, and `planned` classify disposition. + Compatibility is a separate directional statement. +4. External sources carry immutable identity. Git sources require a full + commit and pinned artifact boundary; standards require an edition and + maintaining body; publications keep title, authors, year, container, DOI, + and URL as independently checkable fields. +5. Artifact/code claims require a resolved third-party disposition. The OCR + v0.21.2 derivation is pinned to + `fe83e8281fc4b954967fbaa5a0d099007ddcb06c`. Because initial ACES history + explicitly describes translated OCR structures, ACES conservatively ships + the upstream MIT notice in `THIRD_PARTY_NOTICES.md` and includes that notice + in source and wheel distributions. +6. `tools/check_sdl_lineage.py` validates the ledger offline using existing + catalogs and schemas. Live DOI, GitHub, and documentation lookups occur only + during a bounded audit whose evidence is committed under + `docs/research/lineage/`; CI never treats network availability as truth. +7. `docs/explain/sdl/precedents.md` and `lineage.md` are explanatory views. + They may summarize the ledger but may not establish a competing source or + classification registry. Ambiguous current claims such as "direct port" + are prohibited. + +## Consequences + +### Positive + +- Reviewers can distinguish influence from copying and compatibility. +- Catalog growth without a provenance record fails deterministically. +- Bibliographic identity mismatches and unresolved derived-code notice status + fail before release. +- ACES-native additions are visible rather than falsely attributed to an + older source family. + +### Negative + +- Language-family additions must update one more normative contract. +- The initial ledger is deliberately conservative about OCR notice inclusion; + narrowing that disposition requires new evidence and an explicit amendment. + +### Limits + +The ledger is an evidence-backed engineering record, not legal advice, a +clean-room certification, a proof of behavioral equivalence, or a claim that +all external links remain available forever. It does not change SDL semantics. diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 6e59d1ebb..993700902 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -13,13 +13,20 @@ adrs: - id: ADR-001 path: docs/decisions/adrs/adr-001-scenario-description-language.md pin: 29d53078da3314320bca34148440e8ad80a0deb6fc0471177197d2ab90043389 + amendments: + - date: 2026-07-11 + ref: "#721" + summary: "Replaced implicit case-insensitive field parsing with strict sdl-yaml/v1, explicit diagnosed migration, an honest normalized-authoring-object schema boundary, and RFC 8785 semantic identity." - id: ADR-002 path: docs/decisions/adrs/adr-002-declarative-sdl-objectives.md - pin: 7550c868306aa4f6ff7a1522796b71ae8797aa0015d7faec37c8f7011e60c536 + pin: 70db360e393dd0a02b0bed7aaa668a68ed6961eda0f9246905a536b9c6cba82c amendments: - date: 2026-07-05 ref: "#682" summary: "Per ADR-073, narrowed the objective-success clause to reference observable state (conditions) only; the OCR scoring pipeline (metrics/evaluations/tlos/goals) was removed from the SDL and graded scoring/reward moved to the experiment/evaluator plane (ADR-055/064/069)." + - date: 2026-07-12 + ref: "#725" + summary: "Per ADR-079, corrected the condition/proposition conflation: objective success composes assertions over typed propositions; conditions are probe realizations only." - id: ADR-003 path: docs/decisions/adrs/adr-003-workflows-targetable-subobjects-and-enum-variables.md pin: 0039bea0de4e4d5d08ccf300a7fb7516a0ea01977f38f1e2e111409a13e108df @@ -59,7 +66,11 @@ adrs: pin: 940ce80b1f1d94e3efb4a9f56cb9147d09c9a9dc1453a4977c7a5e194238d708 - id: ADR-015 path: docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md - pin: 1e6000c3405e84765a993d326a8d97822e7b28a6714463bcd4d1550951f20ec7 + pin: bc5038164e535c4d506962e27cee07e720f5c8a77cb17d018dedb3dba927936f + amendments: + - date: 2026-07-12 + ref: "#561" + summary: "Lowered the non-test source-file cap from 600 to 500 lines to align with SonarCloud S104; re-baselined _ADR015_INITIAL_OVERSIZED_FILES and the oversized allowlist to the files over 500 lines at amendment time." - id: ADR-016 path: docs/decisions/adrs/adr-016-semantic-layer-scope-and-coverage-model.md pin: ff9a71be7f6d427b88ff26317da36e14b6cc5943418cd941df88dca29033449b @@ -71,7 +82,15 @@ adrs: pin: 168195279ea2e3dafb12d3d308f750af2804bc09700efa3dc258a31cb05e8d2f - id: ADR-019 path: docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md - pin: 66b4ea3eb2482d6f8855462f4921c9cd7853ddb4c9adfaa21f311a07422f248b + pin: 872985c045405d0333c80b8b8df718bdc243c0ea778e6c8fdc6d6b0d49a15d75 + amendments: + - ref: "#100" + - date: 2026-07-12 + ref: "issue #726" + summary: "Registered contracts/provenance as the normative authority for revision-pinned SDL lineage, derivation, citation identity, and notice dispositions." + - date: 2026-07-12 + ref: "issue #240" + summary: "Removed changelog.d/ from the non-normative roots after release-please replaced the towncrier workflow (GOV-901)." - id: ADR-020 path: docs/decisions/adrs/adr-020-declarative-participant-framing-boundaries.md pin: 4a5d146fc0ffd6da97af94e9f2d257a893de0ab7f63c709339d27f33178609d0 @@ -242,6 +261,9 @@ adrs: - date: 2026-06-12 ref: "#482" summary: "Recorded that the ADR's decision date is 2026-05-26 and it landed with experiment-core PR #422 on 2026-06-05." + - date: 2026-07-08 + ref: "#675" + summary: "ADR-074 realizes the explicit draft/authoring surface anticipated in the Risks section as experiment-authoring-input-v1; the archival contracts are unchanged." - id: ADR-056 path: docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md pin: 3aa602de53f7607cf531b4a9da3f58192eb87ad21ddd92846fd2ae2a7526a4c3 @@ -282,13 +304,42 @@ adrs: - id: ADR-069 path: docs/decisions/adrs/adr-069-cage-2-replication-architecture.md pin: 305334e5558fb88d1f84317209f0e64d182714ba8e99cdeb5f6781bf3ee384f5 + amendments: + - date: 2026-07-08 + ref: "#675" + summary: "ADR-074 adds experiment-authoring-input-v1, giving REP-003 a pre-run home for execution-control facts; it complements the SDL scenario authoring path and adds no CAGE-specific schema." - id: ADR-071 path: docs/decisions/adrs/adr-071-reusable-asset-trust-and-integrity-policy.md pin: d0f0d15945a91e97870d794986d7c7453cea7fade5c74c673d271ceaf05a6b7e - id: ADR-073 path: docs/decisions/adrs/adr-073-scoring-reward-language-scope.md - pin: 5c0f6a0fe961bd9fc3f0b268ee99e1bdf6e6b576fbbe470dd3969a543dc07356 + pin: b48e0d4184556ec1da17e58d4ea74197e383fed8bbf70d8069eaa2d978fbb893 amendments: - date: 2026-07-06 ref: "#682" summary: "Accepted (proposed → accepted) and realized under SEM-206: removed the SDL scoring/reward surfaces, narrowed objectives.success to conditions, amended ADR-002, and updated the published SDL schemas." + - date: 2026-07-12 + ref: "#725" + summary: "Per ADR-079, corrected executable conditions from observable facts to probe realizations and moved objective truth to typed propositions/assertions." + - id: ADR-074 + path: docs/decisions/adrs/adr-074-experiment-authoring-input-contract-boundary.md + pin: 74dd29e3e5f3a2b7bdf836bd40908c0840c51f46a1a89efbb191e6ce7f620ce5 + - id: ADR-076 + path: docs/decisions/adrs/adr-076-portable-sdl-identifiers-and-canonical-addresses.md + pin: cd28329002c1befb1920f0037c07517adbe3811d15e4f559455d05b893c69203 + - id: ADR-077 + path: docs/decisions/adrs/adr-077-associated-artifact-manifest-boundary.md + pin: bbbcb25d36c0b81b4b19198321ae2578e433bc238465397367f5e7dcf3407138 + - id: ADR-078 + path: docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md + pin: d8e2630e332568681d5cf4df5f63971fc5a909c656d380b785e75c016a3744cb + amendments: + - date: 2026-07-12 + ref: "#724" + summary: "Clarified that required instantiation provenance remains validated artifact identity metadata but is excluded from realization-envelope child-dimension enumeration." + - id: ADR-079 + path: docs/decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md + pin: a16e021301c9b411c8efe54ac4933819a50bd10a27d13741cc7ce78d6bffbe14 + - id: ADR-080 + path: docs/decisions/adrs/adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md + pin: e5b44525b5c2bfcef5516e3661c6d3d3a6cb816c10d4c4bb05d720554cea6663 diff --git a/docs/decisions/index.md b/docs/decisions/index.md new file mode 100644 index 000000000..b19d3da3f --- /dev/null +++ b/docs/decisions/index.md @@ -0,0 +1,24 @@ +# Architecture Decisions & Design Notes + +Accepted Architecture Decision Records (ADRs) and the design and preflight notes +that informed the implementation. The ADRs are the durable, governed decisions; +the design and preflight notes are the working analyses that preceded them. + +```{toctree} +:maxdepth: 1 +:caption: Architecture Decision Records +:glob: + +adrs/README +adrs/adr-* +``` + +```{toctree} +:maxdepth: 1 +:caption: Design & Preflight Notes +:glob: + +cage-* +issue-* +sem-* +``` diff --git a/docs/decisions/issue-100-asr-519-libvirt-realization-envelope-preflight.md b/docs/decisions/issue-100-asr-519-libvirt-realization-envelope-preflight.md new file mode 100644 index 000000000..29207322c --- /dev/null +++ b/docs/decisions/issue-100-asr-519-libvirt-realization-envelope-preflight.md @@ -0,0 +1,288 @@ +# Issue 100 / ASR-519 Libvirt Realization Envelope Preflight + +Date: 2026-07-11 + +Requirement: ASR-519. + +This note records architecture guardrails for publishing configuration-bound +libvirt realization envelopes. It is guidance only: it does not publish an +envelope, change a contract, select a target, alter planning/apply behavior, or +define an implementation plan. + +## Binding Sources + +- ADR-070 and `specs/formal/realization/envelope-semantics.md` own the + `RealizationEnvelopeModel` language and the shared `member()`, `subsumes()`, + `witness()`, and `generate_negative_probes()` semantics. +- `docs/decisions/issue-667-realization-envelope-preflight.md` and + `docs/decisions/issue-668-envelope-relation-preflight.md` prohibit a second + capability language or backend-local set relation. +- `docs/decisions/issue-602-libvirt-backend-manifest-preflight.md` and + `docs/decisions/issue-605-libvirt-envelope-diagnostics-preflight.md` own the + coarse manifest-capability and concrete plan-term gates. Those gates remain + necessary but are not value-level realization envelopes. +- `create_libvirt_manifest()`, `create_libvirt_components()`, + `create_libvirt_target()`, `_driver_config()`, `LibvirtProvisioner`, + `LibvirtDeploymentDriver`, and `TechVaultNativeLibvirtDriver` are the current + configuration and execution seams. +- `BackendManifest`, `backend_manifest_payload()`, `ExecutionPlan`, + `ProvisioningPlan`, `RuntimeSnapshot`, `RuntimeManager`, + `RuntimeControlPlane`, and `_call_backend_apply()` are the existing carriage, + provenance, persistence, and fail-closed execution path. +- ADR-009, ADR-019, ADR-061, `ContractModel`, `schema_bundle()`, + `contracts/schema-publication-manifest.json`, and `aces_contracts.corpus` own + publication and packaged-corpus authority. + +## Architecture Decisions And Guardrails + +- Publish exactly one authoritative `realization-envelope/v1` artifact for + each constructible **material** libvirt configuration. The initial modes are + generic qcow2/cloud-init and the generated TechVault appliance. They must not + share an envelope where their behavior differs. +- Keep `RealizationEnvelopeModel` as the denotational scenario-set expression + inside the published artifact. Planning must call the shared relation: + concrete instantiated scenarios use `member()`; a future requested envelope + uses `subsumes(offered, requested)`. Do not duplicate path/domain logic in the + libvirt package, planner, manifest validator, or conformance runner. +- The issue's required realization and observation disclosures are not all SDL + value domains. Architecture, placement mechanism, transformation policy, ACL + enforcement, and observation strength are backend-behavior claims. The + published envelope artifact must be one closed carrier containing the + existing set expression plus closed, typed per-concern disclosures under the + same identity. This is the schema-governed carrier anticipated by ADR-070 R7, + not a second set language. Do not misrepresent disclosures as navigable SDL + paths or create a libvirt-only envelope schema. +- Keep three taxonomies distinct: envelope posture (`open`, `constrained`, + `exact`), SEM-218 authored explicitness/provenance, and evidence source or + observation strength (`authored`, `planned`, `driver-reported`, + `daemon-observed`, `guest-observed`). The last is not + `ExplicitnessProvenance` and must not be inferred from snapshot presence. +- Give the selected envelope one immutable identity value containing contract + id, envelope id, envelope schema version, and canonical `sha256:` content + digest. Bind it to the normalized material configuration identity: driver + mode plus a digest of an allowlisted, secret-free semantic configuration + projection. Reuse the repository's existing ref/digest validation conventions; + do not compare mutable object identity, paths, summaries, or prose. +- Canonical digest input is the closed JSON contract payload with stable key + ordering and the self-digest field excluded. The implementation must have one + digest helper and one identity validator. A digest of raw configuration is + forbidden: credentials and other low-entropy secrets remain vulnerable to + offline guessing even when hashed. +- Normalize target configuration once at the factory boundary and pass the same + closed value to manifest selection, component construction, and driver + construction. Unknown fields, invalid combinations, and a driver whose + declared mode does not match the selected envelope fail target construction. + Do not infer mode by `isinstance`, callable name, successful driver behavior, + or host probing; injected test/live drivers must declare the mode explicitly. +- Classify configuration fields deliberately. Driver mode, image/source policy, + architecture, resource bounds, network policy, seed/appliance behavior, and + any realization-affecting toggle are material. Connection handles, URI + credentials, workspace paths, name prefixes, cleanup, timeouts, and injected + connector objects are operational/private unless they change the claim; if + one does change realizability, it must be represented by a safe governed ref + or construction must fail. `participant_runtime=True` changes a separate + runtime surface and must not silently change the provisioning envelope. +- Carry the selected identity through the existing contracts. The backend + manifest advertises the selected envelope; the planner copies that identity + to the provisioning plan; the libvirt provisioner is constructed with the + same expected identity; and a successful runtime snapshot records it in a + typed field. Do not use `constraints`, plan operation payloads, + `RuntimeSnapshot.metadata`, `ApplyResult.details`, tags, or audit blobs. +- `ExecutionPlan.manifest` equality and `RuntimeManager._provenance_diagnostics()` + remain a defense, but are not sufficient because published + `ProvisioningPlan` values can enter through `RuntimeControlPlane` directly. + `LibvirtProvisioner.validate()` and `apply()` must both reject missing, stale, + mismatched, malformed, or unverifiable identity before interpretation, + snapshot reconciliation, `driver.realize()`, or `driver.destroy()`. +- A non-empty baseline snapshot bound to another envelope/configuration is also + a mismatch. Changing material target configuration requires a new plan under + the new identity; it must not silently relabel existing state. +- Keep `ProvisionerCapabilities` and `realization_support` as coarse incumbent + gates. Mechanically check both directions that matter: every selected + envelope concern is permitted by the manifest's coarse kind/capability + surface, and every governed term claimed by the coarse manifest occurs in at + least one selectable envelope. The selected envelope, not the union + capability set, is the value-level admission authority. +- Keep one canonical declaration of selectable envelope content and derive or + validate manifest projections from it. Do not preserve + `LIBVIRT_PROVISIONER_CAPABILITIES`, a new envelope allowlist, and driver-local + term tables as three independent truths. +- Every non-`UNCHANGED` plan operation must remain accounted for by existing + portable surfaces: its address is changed/observed-realized, or an addressed + typed diagnostic marks it unsupported/failed. A successful operation may not + disappear from `changed_addresses` or the snapshot merely because the driver + aggregates work at domain level. + +## Truthful Initial Mode Boundaries + +- Generic qcow2/cloud-init is x86_64 in current XML. Image `Source.version` is + currently dropped, host image existence is not proven during planning, and a + successful libvirt call is not guest boot or guest configuration evidence. + The envelope must narrow image/OS claims or require an explicit governed image + policy; it must not advertise all OS families merely because dialect code can + emit commands. +- Generic resource translation currently rounds/clamps RAM, supplies default + RAM/CPU values, and normalizes service protocol. Those are transformations, + not exact realization. They must be admitted and disclosed for constrained or + open concerns, or rejected before driver IO; exact declarations may never be + silently transformed. +- Generic `file` placement has a direct `write_files` mechanism. Dataset and + directory placement currently write descriptors/commands rather than proving + the named data or directory contents exist. Likewise, some OS dialects record + feature/mail descriptors instead of realizing the native concern. Envelope + claims must distinguish descriptor disclosure from concern realization. +- Generic ACL translation is fail-closed through `realize_node_acls()` and + libvirt nwfilter generation, but a successful define is daemon/substrate + evidence only. Guest-facing enforcement strength requires separate evidence. +- TechVault appliance mode always boots a generated x86_64 Linux/BusyBox + initramfs, ignores the scenario image as the boot artifact, clamps memory to + 64-128 MiB and vCPUs to 1-2, may synthesize a health service, and does not + consume the generic cloud-init account/content/feature or nwfilter surfaces. + Its envelope must reject or explicitly disclose each difference. It must not + inherit the generic mode's broad provisioner claims. +- `DomainHandle`, `NetworkHandle`, and normal `RuntimeSnapshot` entries are + driver-reported/planned substrate facts. Neither mode may label them + `daemon-observed` without native readback or `guest-observed` without a + concern-specific guest probe. Later issues may strengthen observation through + the envelope's per-concern seam without changing envelope identity semantics. + +## Required Cross-Cutting Reuse + +- SDL ingress: `parse_sdl()` / `parse_sdl_file()`, closed `SDLModel` shapes, + `instantiate_scenario()`, `SemanticValidator`, and existing parse, + instantiation, and semantic errors. No libvirt-specific SDL field is needed. +- Envelope semantics: `RealizationEnvelopeModel`, `member()`, `subsumes()`, + `witness()`, and `generate_negative_probes()` from the shared contract and SDL + semantic packages. +- Contract publication: hand-governed schemas, `ContractModel`, + `schema_bundle()`, valid/invalid fixtures, publication-manifest `last_change` + hashes, `x-aces-invariants`, generated-schema parity, and the packaged corpus + resolver. Published envelope instances are a corpus family, not backend + profiles or fixtures. Keep `specs/authority/authority-boundary.yaml`, + `BACKEND_SUPPORTED_CONTRACT_IDS`, schema publication, and packaged wheel/sdist + contents synchronized; do not make the provisioning-only profile require the + new contract unless that is intentionally a universal backend obligation. +- Manifest authority: `BackendManifest`, `BackendManifestV2Model`, + `backend_manifest_payload()`, supported-contract validation, capability-gap + checks, controlled vocabularies, and canonical concept bindings. +- Planning/runtime: `_validate_manifest()`, + `realization_support_diagnostics()`, `realization_disclosure()`, + `ExecutionPlan`, `RuntimeManager`, `RuntimeControlPlane`, + `_call_backend_diagnostics()`, `_call_backend_apply()`, snapshot contract + diagnostics, and target-shape validation. +- Error/observability: existing `Diagnostic`, `Severity`, `OperationReceipt`, + `OperationStatus`, conformance case/report, audit, and `changed_addresses` + surfaces. Do not add a libvirt exception hierarchy or a logging-only result. +- Persistence: `RuntimeSnapshot`, `RuntimeSnapshotEnvelopeModel`, + `ControlPlaneStore`, and its established typed serialization. Existing atomic + run-artifact writers remain the only optional report writer. +- Testing: realization relation/property tests; planner, runtime-manager, + control-plane/API/store, manifest publication, libvirt realization, + provisioner, registry, conformance, and recording-driver tests. Negative + identity/config/unsupported mutations must assert the recording driver saw no + call and the baseline snapshot stayed byte-for-byte equivalent. + +## Security And Whole-Path Gates + +- Config shape gate: CLI/operations inputs must become the normalized closed + target config before any manifest or driver is built. There is no incumbent + environment-binding path for libvirt; do not add one implicitly. A + credential-bearing connection URI must not be accepted through a CLI option, + because command arguments are OS-visible; use a non-secret URI plus an + injected connection/credential handle or an explicit future secret-input + surface. +- Secret gate: connection credentials, connector objects, SSH keys, cloud-init + bodies, host paths, environment dumps, and raw config never enter envelope + artifacts, digests, plans, snapshots, fixtures, diagnostics, audit details, or + reports. Connection URIs remain private driver input and must not be logged. +- Contract/manifest gate: envelope, manifest, plan, and snapshot shapes pass + their closed Pydantic models, published schemas, contract-id allowlists, + vocabulary validation, concept binding validation, and semantic invariants. +- Planner gate: ordinary manifest checks, SEM-218 support checks, and shared + envelope membership/subsumption all run before a valid execution plan exists. + No one check substitutes for another. +- Target/apply gate: target component shape, exact envelope/config identity, + libvirt capability diagnostics, backend `ApplyResult` shape, snapshot + contract/transition validation, and SEM-218 runtime disclosure all remain + fail-closed. Identity failure precedes every native side effect. +- HTTP/auth gate: no new route is needed. Provisioning submissions retain strict + bearer/proxy identity verification, backend/operator role authorization, + target scoping, request-size limits, idempotency fingerprints, audit events, + and closed `ProvisioningPlanModel` parsing. The current bearer-token branch + returns before the proxy branch's `identity.target_name` check; target scope + must be enforced after either authentication mechanism before a + configuration-bound plan is accepted. +- Error-envelope gate: failures use stable typed diagnostics naming ids, + digests, relation kind, concern, and safe paths. They do not echo envelope + values, raw plan/config payloads, native XML/object reprs, argv, stdout/stderr, + stack traces, or credentials; unexpected HTTP failures keep the existing + redacted 500 response. New validation failures must not reach + `_backend_call_failed()`, whose current inclusion of `str(exc)` is not a safe + carrier for configuration or connection errors; use typed redacted + diagnostics, and harden that boundary if it is touched. +- Host/OS gate: envelope selection and validation are pure and hermetic. They do + not import libvirt, query a daemon, inspect images, invoke subprocesses, or + broaden claims from host discovery. Native access stays lazy behind the + existing driver; fixed argv/list-form command and ownership/rollback guards + remain intact. Envelope ids are grammar-checked before corpus path + construction, resolve only below the packaged corpus root, and never select a + caller-supplied or remote path. +- Persistence gate: rejected work writes no snapshot, operation success, + realization provenance, or native-state ledger. Successful state carries the + typed envelope/config identity; private host state remains outside portable + persistence. + +## Extensibility Seam + +The required seam is one normalized material target configuration selecting one +versioned envelope artifact and immutable identity. A future remote libvirt +policy, alternate image family, architecture, storage/network policy, or new +appliance mode adds a normalized configuration variant and envelope artifact; +it does not edit the shared relation, planner rules, control-plane route, +diagnostic hierarchy, or persistence mechanism. Observation may strengthen per +concern from driver-reported to daemon- or guest-observed only when the selected +configuration wires the corresponding probe and changes the envelope digest. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating missing envelope identity as universal support or accepting a legacy + fallback after a successful driver call; +- treating the backend name, driver class, connection URI, manifest summary, or + a successful libvirt define as configuration-bound envelope identity; +- hashing raw secrets or serializing a callable/connection/object repr as + configuration identity; +- passing credential-bearing libvirt URIs in process argv, diagnostics, audit, + or report output; +- carrying identity only on `ExecutionPlan`, only in the manifest, or only in + snapshot `metadata` while direct provisioning-plan submission remains open; +- using `realization_support.constraints` or + `ProvisionerCapabilities.constraints` as the machine-readable envelope; +- conflating SDL membership with backend transformation or observation claims; +- copying the shared relation, plan payload extraction, manifest renderer, + schema registry, vocabulary tables, config normalization, or digest logic; +- allowing the generic and TechVault drivers to select the same envelope by + accident, or inferring the envelope from an injected driver; +- retaining broad OS/content/account/feature/ACL claims that only produce a + descriptor, planned snapshot echo, or substrate handle; +- relabeling planned data as daemon- or guest-observed evidence; +- assuming bearer authentication enforces target scope before the shared + authorization check is corrected; +- weakening the default hermetic suite by requiring libvirt/QEMU, privileges, + host images, network access, or secrets. + +## Non-Goals And Boundaries + +- No implementation of issue #100 or downstream issues #714-#717. +- No new SDL syntax, backend profile, capability language, solver, HTTP route, + persistence service, native-state repository, or libvirt-specific exception + hierarchy. +- No guest-observed certification, real-daemon report, TechVault end-to-end + honesty certification, or final reference-scenario certification; this issue + may only publish the observation strength it actually proves. +- No claim that one witness proves subsumption, negative refusal, or backend + honesty. Closed-envelope conformance still requires generated negative probes + and unchanged native state. +- No compatibility path that treats absent, stale, mismatched, or unverifiable + envelope/configuration identity as acceptable. diff --git a/docs/decisions/issue-601-techvault-live-verification.md b/docs/decisions/issue-601-techvault-live-verification.md index 97a3a0a9a..cf8b4f29d 100644 --- a/docs/decisions/issue-601-techvault-live-verification.md +++ b/docs/decisions/issue-601-techvault-live-verification.md @@ -1,218 +1,107 @@ -# Issue 601 TechVault Native Libvirt Verification +# Issue 601 TechVault Live Verification — Corrected Claim Boundary -This note records the live TechVault checks used for issue 601 after the -libvirt backend was corrected to prove a second independent substrate. Earlier -ACES/libvirt live-gate attempts in this PR delegated TechVault startup to APTL -Compose; those attempts are superseded and are not used as acceptance evidence. +This historical verification note is corrected by issue 714 / ASR-519. The +earlier issue-601 results must not be cited as proof that TechVault guest images, +cloud-init content, accounts, features, ACLs, named services, or SOC applications +were realized by the libvirt backend. -## Baseline APTL gate +## What the earlier run established -Command run from `/home/atomik/src/aptl` on 2026-06-27: +The runs established that ACES could create a separate libvirt/QEMU substrate +containing generated initramfs domains and networks. That is a useful substrate +check, but it is narrower than scenario realization. -```bash -uv run aptl lab validate-live --yes --run-id aces-601-libvirt-techvault-live-20260627 -``` - -Result: PASS. - -Baseline summary: - -- Scenario: `scenarios/techvault-operational.sdl.yaml` -- Selected profiles: `wazuh`, `victim`, `kali`, `enterprise`, `soc`, - `fileshare`, `dns`, `otel` -- ACES-realized nodes: 30 -- Running `aptl-*` containers after the gate: 30 -- Networks: `aptl_aptl-dmz`, `aptl_aptl-internal`, - `aptl_aptl-redteam`, `aptl_aptl-security` -- Manual readback found 10 active Wazuh agents, Suricata traffic/alert events, - 0 kernel drops, 49,954 loaded rules, and 0 failed rules. - -This baseline is the operational comparison point only; it is not the libvirt -backend proof. - -## Native libvirt substrate - -The accepted ACES/libvirt path is now native: - -- `aces libvirt techvault validate-live` creates libvirt networks and QEMU - domains from the ACES provisioning plan. -- Domains boot generated BusyBox initramfs appliances through libvirt/QEMU. -- The live gate no longer imports APTL, starts Docker Compose, or probes Docker - containers. -- Clean boot removes prior `aces-techvault-*` libvirt domains/networks before - realizing the next scenario, so a full TechVault run can be followed by a - reduced variant without carrying over the old topology. - -Local host setup for the proof: - -```bash -sudo apt-get install -y qemu-system-x86 libvirt-daemon-system \ - libvirt-clients python3-libvirt iputils-ping -``` - -Because `libvirt-python` remains optional and lazy for normal CI, the local -manual run exposed only the system libvirt binding to the project venv: - -```bash -mkdir -p /tmp/aces-libvirt-python -ln -s /usr/lib/python3/dist-packages/libvirt.py /tmp/aces-libvirt-python/libvirt.py -ln -s /usr/lib/python3/dist-packages/libvirtmod.cpython-312-x86_64-linux-gnu.so \ - /tmp/aces-libvirt-python/libvirtmod.cpython-312-x86_64-linux-gnu.so -``` +Several former claims exceeded the available evidence: -The old APTL Docker lab was stopped before native libvirt runs because its -bridges already occupied the authored TechVault `172.20.x.0/24` CIDRs. +- planned domain and network data was copied into the native report without an + independent source label; +- generic listeners were treated as realization of named services; +- SOC state was inferred from domain names rather than observed inside guests; +- requested memory and CPU values could be clamped; +- a successful native handle was treated as sufficient without exact daemon + readback; and +- prefix-wide cleanup was treated as a safe clean boot without per-resource + ownership proof. -## Native reduced variants +Consequently, the former counts for services, Wazuh agents, Suricata rules, +case-management applications, and scenario variants are withdrawn as libvirt +realization evidence. Historical endpoint reachability proved only that a +generated listener answered at an address; it did not prove the declared +application or service was installed and operating. -These variants mirror the APTL curated scenario shapes and are ordinary SDL -inputs to the libvirt backend, not name-based presets. +The APTL live-gate results remain evidence about the APTL substrate only. They do +not transfer to libvirt. -### Observability core +## Current bounded native mode -```bash -sudo env PYTHONPATH=/tmp/aces-libvirt-python PATH=$PATH \ - /home/atomik/.local/bin/uv run --project implementations/python --frozen \ - aces libvirt techvault validate-live \ - --scenario /home/atomik/src/aces5/examples/scenarios/techvault-observability-core.sdl.yaml \ - --output-dir /tmp/aces-libvirt-native \ - --run-id native-observability-20260627T0820Z \ - --yes --boot-timeout-seconds 90 --appliance-memory-mib 64 -``` - -Result: PASS. +The TechVault appliance mode now accepts only concerns it can apply exactly and +verify through bounded libvirt daemon readback. A successful run accounts for: -Manifest: - -```text -/tmp/aces-libvirt-native/runs/native-observability-20260627T0820Z/live-gate/manifest.json -``` +- native domain and network existence; +- exact architecture and generated-initramfs attachment policy; +- exact memory and virtual CPU values; +- exact network CIDR, gateway, internal/NAT policy, and domain attachments; and +- a realization binding covering the published envelope, driver configuration, + connection and naming configuration digests, and boot-artifact digests. -Surface: +The following concerns are explicitly unsupported in this mode and block the +operation before native resource creation: -- Domains: `aptl-grafana-otel`, `aptl-otel-collector`, `aptl-tempo` -- Networks: `security-net` -- Service listeners: 4 -- Substrate: `libvirt-qemu-initramfs` - -### Attacker target - -```bash -sudo env PYTHONPATH=/tmp/aces-libvirt-python PATH=$PATH \ - /home/atomik/.local/bin/uv run --project implementations/python --frozen \ - aces libvirt techvault validate-live \ - --scenario /home/atomik/src/aces5/examples/scenarios/techvault-attacker-target.sdl.yaml \ - --output-dir /tmp/aces-libvirt-native \ - --run-id native-attacker-target-20260627T0825Z \ - --yes --boot-timeout-seconds 120 --appliance-memory-mib 64 -``` +- concrete guest images; +- cloud-init content, content placements, accounts, and feature bindings; +- declared guest services; +- network ACLs; +- unbound metadata or silently normalized names; and +- updates or compound delete transactions without a verified restore path. -Result: PASS. +Guest readiness and SOC/application state remain `not-observed`. Concern-specific +guest probes are separate work; generic ping or TCP reachability is not promoted +to realization evidence. -Surface: +## Current live-gate interpretation -- Domains: `aptl-grafana-otel`, `aptl-otel-collector`, `aptl-tempo`, `kali`, - `kali-capture`, `victim`, `wazuh-indexer`, `wazuh-manager` -- Networks: `internal-net`, `redteam-net`, `security-net` -- Wazuh readback: `victim`, `wazuh-manager` +The live gate may pass only for a bounded scenario whose admitted substrate is +successfully created and read back. The repository's operational TechVault and +curated variants declare unsupported guest concerns, so they now fail with typed +diagnostics instead of producing a partial-success manifest. -### Defensive minimum after full TechVault - -The defensive-minimum variant was run after the full 30-domain scenario with -clean boot enabled, proving the backend recomposes the live surface instead of -over-starting the full topology. +For an admitted bounded scenario, the operator command is: ```bash -sudo env PYTHONPATH=/tmp/aces-libvirt-python PATH=$PATH \ - /home/atomik/.local/bin/uv run --project implementations/python --frozen \ - aces libvirt techvault validate-live \ - --scenario /home/atomik/src/aces5/examples/scenarios/techvault-defensive-min.sdl.yaml \ - --output-dir /tmp/aces-libvirt-native \ - --run-id native-defensive-min-final-20260627T0855Z \ - --yes --boot-timeout-seconds 120 --appliance-memory-mib 64 +aces libvirt techvault validate-live \ + --scenario path/to/bounded.sdl.yaml \ + --project-dir . \ + --run-id bounded-native-check \ + --yes ``` -Result: PASS. - -Live libvirt state after the run: - -- Running domains: `aces-techvault-aptl-grafana-otel`, - `aces-techvault-aptl-otel-collector`, `aces-techvault-aptl-tempo`, - `aces-techvault-wazuh-dashboard`, `aces-techvault-wazuh-indexer`, - `aces-techvault-wazuh-manager` -- Active native network: `aces-techvault-security-net` -- No full-TechVault domains remained from the preceding run. - -## Native full TechVault +There is no memory-clamping, boot-timeout, or prefix-cleanup switch. Resource +values come from the governed plan. Connection URIs carrying user information or +passwords are rejected. -Final command run from `/home/atomik/src/aces5` on 2026-06-27: +The manifest keeps sources separate: -```bash -sudo env PYTHONPATH=/tmp/aces-libvirt-python PATH=$PATH \ - /home/atomik/.local/bin/uv run --project implementations/python --frozen \ - aces libvirt techvault validate-live \ - --scenario /home/atomik/src/aces5/examples/scenarios/techvault-operational.sdl.yaml \ - --output-dir /tmp/aces-libvirt-native \ - --run-id native-operational-final-20260627T0850Z \ - --yes --boot-timeout-seconds 240 --appliance-memory-mib 64 -``` +| Section | Permitted basis | +|---|---| +| `authored` | scenario reference | +| `planned` | compiler/runtime plan | +| `driver_reported` | driver operation result | +| `daemon_observed` | bounded libvirt XML and active-state readback | +| `guest_observed` | `not-observed` | -Result: PASS. +Planned topology remains labelled `planned`; daemon observations do not imply +guest services, SOC state, or application behavior. -Manifest: - -```text -/tmp/aces-libvirt-native/runs/native-operational-final-20260627T0850Z/live-gate/manifest.json -``` +## Recovery and cleanup -Surface: - -- Domains: 30, matching `examples/scenarios/techvault-operational.sdl.yaml` -- Networks: `dmz-net`, `internal-net`, `redteam-net`, `security-net` -- Declared service listeners: 36 -- Substrate: `libvirt-qemu-initramfs` - -SOC readback: - -- Case-management surface present: TheHive, MISP, Cortex, Shuffle -- Suricata readback: present, 49,954 rules loaded, 0 failed rules, 0 kernel - drops -- Wazuh active-agent readback: `ad`, `db`, `dns`, `fileshare`, `suricata`, - `victim`, `wazuh-manager`, `webapp`, `workstation` - -Manual endpoint probes against the live native domains: - -| Node | IP | Port | Result | -|---|---:|---:|---| -| `wazuh-manager` | `172.20.0.29` | 55000 | OK | -| `thehive` | `172.20.0.24` | 9000 | OK | -| `misp` | `172.20.0.15` | 443 | OK | -| `cortex` | `172.20.0.13` | 9001 | OK | -| `shuffle-frontend` | `172.20.0.20` | 80 | OK | -| `shuffle-backend` | `172.20.0.19` | 5001 | OK | -| `suricata` | `172.20.0.23` | 80 | OK | -| `webapp` | `172.20.1.14` | 8080 | OK | -| `kali` | `172.20.4.10` | 22 | OK | -| `victim` | `172.20.2.16` | 22 | OK | - -## Regression coverage - -Native coverage now includes: - -- `test_libvirt_backend_techvault_integration.py`: the full TechVault SDL - drives 30 node domains and four networks through runtime planning and - provisioning. -- `test_libvirt_backend_techvault_native.py`: full TechVault and all four - reduced variants realize distinct native libvirt surfaces; live manifest - evidence is native and contains no Docker/APTL probe surface; clean boot - removes prior libvirt resources. -- `test_libvirt_backend_cli.py`: CLI wiring passes connection, memory, and - boot-timeout controls to the native live gate. +Creation failures trigger ownership-checked cleanup of resources created by the +current operation. Success is withheld unless absence can be verified. Uncertain +or failed cleanup produces a residual-state diagnostic and retains run-local boot +artifacts for investigation. The backend does not enumerate and delete resources +solely by name prefix. ## Scope statement -This is a reference-backend operational proof, not an equivalence proof with -APTL. The libvirt backend boots native QEMU appliance domains and validates the -ACES-composed topology, network reachability, declared service listeners, and -SOC surface/readback. It does not claim byte-identical guest images, application -data, or upstream Wazuh/MISP/TheHive internals from the APTL Docker stack. +The corrected proof is a bounded native-substrate realization proof. It is not an +APTL equivalence proof, an application deployment proof, a service-readiness proof, +or a SOC detection-quality proof. diff --git a/docs/decisions/issue-615-libvirt-paper-evidence-preflight.md b/docs/decisions/issue-615-libvirt-paper-evidence-preflight.md index 104a15713..acb8c230f 100644 --- a/docs/decisions/issue-615-libvirt-paper-evidence-preflight.md +++ b/docs/decisions/issue-615-libvirt-paper-evidence-preflight.md @@ -13,6 +13,16 @@ participant/evidence scenario. It is guidance only: it does not implement the artifact, add schemas, change runtime behavior, or define an implementation plan. +## ASR-519 correction + +Issue 714 narrows this preflight's native evidence assumptions. A generated +TechVault domain or reachable listener is not guest, service, or SOC evidence. +Current native evidence is limited to exact, bounded libvirt daemon readback of +the admitted VM/network substrate. Wazuh/SOC channels in the paper artifact are +structural evaluator declarations, not native readback. Generic readiness, +negative-reachability, and name-derived SOC claims are withdrawn pending +concern-specific guest observation work. + ## Binding Sources - `docs/decisions/issue-598-paper-reference-scenario-preflight.md`, @@ -34,7 +44,8 @@ plan. - `docs/decisions/issue-601-techvault-live-verification.md`, `aces_operations.techvault_live`, and `aces_backend_libvirt.techvault_native` own the native libvirt live-gate - substrate, realized surface, readiness checks, and SOC readback helpers. + substrate and bounded daemon-observed surface. Guest readiness and SOC state + are explicitly `not-observed`. - `contracts/schemas/backend-manifest/backend-manifest-v2.json`, `BackendManifestV2Model`, and `backend_manifest_payload()` own backend manifest/capability shape. @@ -60,14 +71,14 @@ plan. capability/profile/conformance result; libvirt realization provenance; realized topology and network attachment matrix; participant action proof from `LibvirtParticipantRuntime`; terminal participant observation envelope - or behavior-history equivalent; evaluator-only Wazuh/SOC readback or a typed - translated readback record with explicit limitation; negative reachability + or behavior-history equivalent; evaluator-only declarations of Wazuh/SOC + evidence channels with explicit limitation; structural negative-boundary checks for internal DB and Wazuh/evaluator surfaces; evaluator outcome and limitation records; and redaction/provenance metadata. - Wazuh/SOC evidence must remain evaluator-only evidence. If the native libvirt - proof uses generated appliance readback or translated native readback rather - than full upstream Wazuh internals, the artifact must state that limitation - next to the evidence and in the run/evaluator limitation surface. + proof has no concern-specific guest observation, the artifact must state + `not-observed`; it must not synthesize translated native readback from domain + names, declared services, or generic reachability. - The participant proof must enter through `RuntimeControlPlane` and `LibvirtParticipantRuntime`, reusing the issue-614 action-admission and behavior-history machinery. Do not replace the participant proof with a @@ -88,8 +99,7 @@ Reuse these repo surfaces before adding anything new: - Libvirt live-gate surface: `validate_techvault_live()`, `TechVaultLiveConfig`, `TechVaultLiveReport`, `TechVaultNativeLibvirtDriver`, - `NativeLibvirtProbe`, `expected_surface()`, `native_soc_readback()`, and the - existing safe `run_id` filesystem-label check. + `expected_surface()`, and the existing safe `run_id` filesystem-label check. - Libvirt runtime/provisioning surface: `create_libvirt_target()`, `create_libvirt_manifest()`, `create_libvirt_components()`, `LibvirtProvisioner`, @@ -152,7 +162,7 @@ Reuse these repo surfaces before adding anything new: rejected. - Participant visibility gate: participant-visible content is limited to the compiled observation boundary and participant implementation exposure - policy. Wazuh/SOC readback, internal DB reachability, policy internals, + policy. Wazuh/SOC channel declarations, policy internals, evaluator limitations, libvirt native details, and negative checks must stay outside `visible_refs` and `disclosed_refs` unless an existing governed boundary explicitly permits disclosure. @@ -172,10 +182,10 @@ Reuse these repo surfaces before adding anything new: connection URIs with secrets, credentials, private keys, unredacted tool transcripts, backend-native inspect payloads, raw Wazuh rule bodies, raw prompts, hidden answers, environment dumps, process argv, or full tracebacks. -- OS-level exposure gate: live probes must keep secrets out of argv and - diagnostics. Reuse fixed argv, no `shell=True`, bounded timeouts, controlled - working directories, and bounded sanitized diagnostics as in the existing - libvirt live-gate helpers. +- OS-level exposure gate: future concern-specific guest probes must keep + secrets out of argv and diagnostics. Generic ping/TCP checks are not + realization evidence. Use fixed argv, no `shell=True`, bounded timeouts, + controlled working directories, and bounded sanitized diagnostics. - Persistence gate: use the existing run archive directory and atomic JSON writing pattern where durable control-plane state is needed. Do not add a libvirt evidence database, participant store, audit log, schema registry, or @@ -197,10 +207,10 @@ runtime/evidence contract inputs, parameterized by: - `scenario_path`, `run_id`, `output_dir`, and optional artifact locator or sealing policy; -- backend target factory/config, including libvirt connection, native probe, - boot timeout, clean-boot policy, and participant runtime factory; -- evidence source policy, including native translated SOC readback versus - upstream Wazuh readback and the disclosure text that explains the difference; +- backend target factory/config, including libvirt connection and participant + runtime factory; +- evidence source policy separating authored, planned, driver-reported, + daemon-observed, guest-observed, and derived facts; - invariant-ledger mapping, which should reference stable ACES addresses and evidence refs rather than libvirt domain names, UUIDs, Docker ids, host paths, or APTL-private identifiers. @@ -230,8 +240,8 @@ Avoid: observation envelope; - claiming libvirt evaluator or observation capability in the backend manifest without actual capability implementation and contract-gap checks; -- treating native appliance SOC readback as upstream Wazuh detection-quality - evidence; +- fabricating native appliance SOC readback from names, listeners, topology, or + daemon-only substrate facts; - using libvirt domain UUIDs, XML, MACs, host paths, QEMU commands, Docker ids, APTL Compose names, or backend-local action labels as portable semantics; - overfitting the artifact to one run id, host, libvirt network naming policy, diff --git a/docs/decisions/issue-668-envelope-relation-preflight.md b/docs/decisions/issue-668-envelope-relation-preflight.md new file mode 100644 index 000000000..bf95298f1 --- /dev/null +++ b/docs/decisions/issue-668-envelope-relation-preflight.md @@ -0,0 +1,202 @@ +# Issue 668 Envelope Relation Preflight + +Date: 2026-07-05 + +Issue: #668. + +Requirement: none. The GitHub issue title, body, and acceptance criteria are +the contract. + +This note records architecture guardrails for implementing the realization +envelope membership, subsumption, and witness relation. It is guidance only: it +does not implement the relation, publish schemas, change manifests, alter +conformance behavior, or replace the #663 bridge. + +## Binding Sources + +- ADR-070 and `specs/formal/realization/envelope-semantics.md` own the + realization-envelope language, admitted fragment, set-relation semantics, + witness rules, and negative-conformance requirement. +- `docs/decisions/issue-667-realization-envelope-preflight.md` owns the + design boundary for envelope semantics. Issue #668 consumes that semantics; + it must not widen the language while implementing the relation. +- `specs/sdl/variables-and-instantiation.md` owns typed variables, + placeholder syntax, fail-closed instantiation, and post-instantiation + semantic revalidation. +- `specs/sdl/diagnostics.md` owns the fail-closed SDL diagnostic stages and + collect-all validation behavior. +- `specs/formal/realization/explicitness-and-realization.md` and + `aces_processor.semantics.realization` own SEM-218 exact/constrained/open + runtime realization gates. The envelope relation composes with those gates; + it does not replace or redefine them. +- `docs/explain/reference/backend-conformance.md` and + `docs/decisions/issue-663-target-conformance-provisioning-scope-preflight.md` + own target-conformance guardrails and the temporary `reference_scenario` + bridge the witness relation is meant to retire later. +- ADR-009, ADR-019, ADR-061, `specs/authority/authority-boundary.yaml`, and + `contracts/schema-publication-manifest.json` own contract authority and schema + publication discipline. + +## Architecture Decisions + +- Implement one deterministic semantic relation over the #667 envelope + expression: concrete membership, envelope subsumption, and witness generation + must share domain comparison and closure logic. Do not create separate + author-side, backend-side, and conformance-side interpretations. +- A concrete scenario is in an envelope only after ordinary SDL structural + validation, semantic validation, instantiation, and no-unresolved-variable + checks pass. Invalid SDL is never a member. +- Subsumption is set inclusion in the admitted fragment. It must reduce to + bounded domain subset checks, product/record checks, governed-reference + subset checks, and scoped closure compatibility. No silent approximation, + sampling, probabilistic answer, backend callback, or solver-only answer is + acceptable. +- Witness generation is a deterministic selector plus ordinary SDL validation. + A witness proves one executable in-envelope instance; it is not proof of + subsumption, backend honesty, or closed-world refusal. +- Negative probes for closed envelopes must use the same relation evidence and + must expect refusal through `OperationStatus` / `Diagnostic` without runtime + mutation. Backend-native exceptions or no-op successes are not portable + refusal evidence. +- Relation diagnostics must be stable and public-safe: name relation kind, + envelope id/ref, SDL path/address, domain kind, governed refs, and contract or + digest ids when needed. Do not echo raw concrete values that may be sensitive. +- Keep current coarse manifest fields in their lane. `realization_support`, + `ProvisionerCapabilities`, backend profiles, semantic profiles, validation + profiles, and experiment study membership are not substitutes for envelope + membership/subsumption. +- If implementation discovers the current unpublished envelope shape is + insufficient, update the formal spec or ADR path first. Do not smuggle new + semantics through prose `constraints` strings or conformance-only DTOs. + +## Required Incumbents + +Reuse these before adding anything new: + +- Envelope authority: + `docs/decisions/adrs/adr-070-realization-envelope-semantics.md`, + `specs/formal/realization/envelope-semantics.md`, and + `docs/research/realization-envelope/`. +- SDL parsing, variables, and validation: `parse_sdl()`, `parse_sdl_file()`, + `SDLModel(extra="forbid")`, `VARIABLE_TOKEN_RE`, `Variable`, + `VariableType`, `instantiate_scenario()`, `SDLParseError`, + `SDLValidationError`, `SDLInstantiationError`, and `SemanticValidator`. +- Existing realization semantics: + `aces_sdl.explicitness`, `CompiledRealizationRequirement`, + `realization_support_diagnostics()`, `realization_disclosure()`, + `RuntimeSnapshot.realization_provenance`, and the SEM-218 formal spec. +- Contract and authority surfaces: `ContractModel`, `schema_bundle()`, + published `contracts/schemas/`, `contracts/fixtures/`, + `contracts/profiles/`, `contracts/concept-authority/`, + `validate_controlled_vocabulary_scope_values()`, reference-model validators, + concept bindings, and schema-publication checks. +- Backend manifest path: `BackendManifest`, + `RealizationSupportDeclaration`, `ProvisionerCapabilities`, + `BackendManifestV2Model`, `backend_manifest_payload()`, + `validate_backend_supported_contract_versions()`, and capability-gap helpers. +- Processor/runtime/conformance path: `run_reference_processor()`, + `compile_scenario_runtime_model()`, `plan()`, `RuntimeTarget`, + `RuntimeControlPlane`, `_call_backend_apply()`, `_snapshot_contract_diagnostics()`, + `run_target_conformance()`, `ConformanceCaseResult`, `OperationReceipt`, + `OperationStatus`, `Diagnostic`, and `Severity`. +- Repository workflow: `.ground-control.yaml`, `.gc/plan-rules.md`, + `tools/check_repo_policy.py`, `tools/check_requirement_governance.py`, + `tools/check_generated_schemas.py`, `tools/check_schema_publication.py`, + `tools/check_json_artifacts.py`, and `tools/verify_all.py`. + +## Cross-Cutting Layers + +- SDL/config ingress: relation inputs that represent SDL must enter through + parsed `Scenario` / `InstantiatedScenario` models or a contract model, not + free-form dicts. Unknown fields, variable placeholders in keys, unresolved + variables, reference ambiguity, and semantic validation failures remain fatal. +- Domain validation: envelope domains must use the typed variable substrate and + governed reference surfaces. Numeric intervals need declared numeric type and + bounded endpoints; enum and governed-reference domains must be finite; record + domains must reject unknown extras when closed. +- Contract shape: any public envelope payload or manifest carrier must be a + closed `ContractModel` with `schema_bundle()` parity, valid/invalid fixtures, + `contracts/schema-publication-manifest.json` ledger entries, and + `x-aces-invariants` for semantic rules JSON Schema cannot express. +- Manifest authority: backend carriage must render through + `backend_manifest_payload()` and validate as a backend manifest. Support + declarations, concept bindings, supported contract versions, capability + vocabularies, and governed extension terms must keep using existing + validators. +- Planning admission: a failed relation check is admission evidence, not a + backend deployment attempt. It should surface as stable `Diagnostic` values + and must not bypass planner diagnostics, capability-gap checks, or SEM-218 + realization-support gates. +- Runtime/control-plane: generated witnesses and negative probes must execute + only through `RuntimeControlPlane` and target components that already pass + `RuntimeTarget` shape validation. `_call_backend_apply()` must remain the + fail-closed backend result boundary. +- Error envelope: public diagnostics and reports may contain ids, refs, paths, + domain kinds, relation kinds, and bounded summaries. They must not contain + credentials, bearer tokens, private keys, raw backend objects, host paths, + process argv, stdout/stderr, hidden truth, scoring state, full tracebacks, or + environment dumps. +- OS and secret exposure: witness generation must not require reading + `~/.secrets`, local daemon inventory, privileged host state, or secrets in + command-line arguments. Default verification must stay hermetic. +- Persistence/evidence: conformance remains report-oriented. If durable + artifacts are later needed, persist envelope refs, digests, relation result + summaries, witnesses, and refusal evidence through existing run-artifact or + experiment evidence surfaces, not a new relation database or cache. + +## Extensibility Seam + +The seam is a pure relation over a versioned envelope expression plus a +validated SDL instance. Keep it parameterized by: + +- relation kind: membership, subsumption, witness, or negative probe; +- scope/path: field, node, topology, app, or scenario; +- domain kind: exact, enum, boolean, numeric interval, governed reference, or + record; +- closure/posture: scoped open-world or closed-world overlays and exact, + constrained, or open posture; +- governed authority: vocabulary, reference-model, scenario registry, contract + id, digest, or concept-family ref; and +- witness policy/seed: deterministic selection without a global hard-coded + scenario. + +Future domain kinds or carriers should land by extending the formal envelope +spec, contract model/schema, fixtures, relation helper, and tests together. +They should not require per-backend relation code, duplicate manifest +renderers, or conformance-profile branches. + +## Gotchas And Anti-Patterns + +Avoid: + +- equating `instance in envelope` with experiment-study membership, + validation/admission profile membership, backend-profile selection, or + semantic-profile applicability; +- treating `subsumes(offered, requested)` as a planner capability shortcut that + can skip manifest, capability, SEM-218, or snapshot validation; +- treating `realization_support.support_mode` as value-level envelope posture; +- letting authored variables survive into concrete membership checks; +- deriving witness values from Python dict iteration, current time, host state, + random choices without an explicit seed policy, or backend discovery; +- implementing relation failures as new exception hierarchies, raw Pydantic + prose, booleans with lost diagnostics, or backend-native errors; +- adding arbitrary predicates, unbounded regex, recursion, non-linear + arithmetic, quantification over unbounded collections, external queries, or + backend callbacks to portable envelopes; +- duplicating schema registries, fixture loaders, vocabulary tables, manifest + renderers, conformance reports, persistence stores, or validation passes; +- using `constraints` prose as the machine-checkable carrier for membership, + subsumption, closure, or witness policy; and +- leaking sensitive concrete values through diagnostics, witnesses, negative + probes, fixtures, docs, audit details, or report payloads. + +## Non-Goals + +- Implementing issue #668 in this preflight. +- Publishing a new envelope schema or backend-manifest version. +- Replacing `run_target_conformance(reference_scenario=...)` in this note. +- Redesigning SDL variables, SEM-218 explicitness, backend profiles, + validation/admission profiles, experiment run-set semantics, runtime + snapshots, control-plane security, or conformance reporting. +- Adding a solver dependency, HTTP API, persistence service, backend adapter, + controlled vocabulary, or new SDL dialect. diff --git a/docs/decisions/issue-714-asr-519-techvault-realization-disclosure-preflight.md b/docs/decisions/issue-714-asr-519-techvault-realization-disclosure-preflight.md new file mode 100644 index 000000000..4deb1c8f1 --- /dev/null +++ b/docs/decisions/issue-714-asr-519-techvault-realization-disclosure-preflight.md @@ -0,0 +1,406 @@ +# Issue 714 / ASR-519 TechVault Realization Disclosure Preflight + +Date: 2026-07-11 + +Requirement: ASR-519. + +This note records architecture guardrails for making the generated TechVault +libvirt appliance path truthful at validation, execution, observation, and +evidence boundaries. It is guidance only: it does not implement issue #714, +change a schema, alter an envelope, add a probe, or define an implementation +plan. + +## Binding Sources + +- ADR-070, `specs/formal/realization/envelope-semantics.md`, and the issue #100 + preflight own configuration-bound realization-envelope identity, the shared + concern/disposition/observation vocabulary, and exact/constrained/open set + semantics. +- `specs/formal/realization/explicitness-and-realization.md` and the issue #491 + preflight own SEM-218 non-approximation and author/processor/backend origin + provenance. Origin provenance is not observation strength. +- ADR-066 and `specs/formal/observability-evidence-plane.md` separate authored, + operational-observability, captured-evidence, and derived-analysis facts. +- ADR-021 forbids promoting an internally coherent or positive-path result into + a demonstrated realization claim without falsification evidence. +- ADR-004 and ADR-036 keep planning, runtime execution/persistence, portable + contracts, concrete backend IO, and operational artifact production in their + existing packages. +- The issue #603, #604, and #606 preflights own libvirt interpretation, + reconciliation/teardown, and backend-neutral conformance boundaries. +- `BackendRealizationEnvelopeModel`, `RealizationEnvelopeIdentityModel`, + `ProvisioningPlan`, `Realization`, `DomainSpec`, `NetworkSpec`, `DriverResult`, + `ApplyResult`, `RuntimeSnapshot`, `Diagnostic`, and `OperationStatus` are the + incumbent contract and error surfaces. + +## Architectural Diagnosis + +The current path has two independent truth gaps that must not be confused. + +1. `LibvirtProvisioner._reconcile_snapshot()` copies plan payloads into + `SnapshotEntry`. `realization_disclosure()` can therefore compare an authored + value with a planned payload echo even when the native driver ignored that + value. The snapshot is reconciliation state; payload equality is not native + observation. +2. `TechVaultNativeLibvirtDriver.last_snapshot` is derived from + `_native_matrix()`, not from libvirt or guest readback. `expected_surface()` + and `native_soc_readback()` then derive claims from that matrix. A non-empty + domain list proves, at most, that the driver reported substrate handles; it + does not prove the requested image, resources, services, accounts, content, + features, ACLs, or guest network state. + +Concrete known relaxations include resource clamping, generated-appliance image +substitution, invalid/missing network defaults, synthesized health services, +discarded `cloud_init` and ACL intent, and domain-level handles standing in for +placement-level realization. The static TechVault envelope discloses several of +these limitations, but a transformation label such as `bounded-normalization` +is disclosure, not executable authorization to weaken an exact plan value. + +Material target identity is also incomplete if behavior-changing injected +values such as the initramfs builder, kernel/image policy, `define_only`, or a +resource override can vary while only `driver_mode=techvault-appliance` selects +the fixed configuration digest. A live claim must bind the actual material +configuration, not merely the driver class name. + +## Architecture Decisions And Guardrails + +### Keep fact classes separate + +Every claim-bearing artifact must preserve these classes explicitly: + +| Fact class | Canonical source | Maximum claim without another source | +| --- | --- | --- | +| Authored | parsed and validated SDL | author requested it | +| Planned | `ProvisioningPlan`, `Realization`, `DomainSpec`, `NetworkSpec` | processor/backend intends to apply it | +| Driver-reported | `DriverResult` and bounded driver receipts | driver says an operation completed | +| Daemon-observed | post-mutation libvirt readback | libvirt reports a native definition/state | +| Guest-observed | concern-specific guest probe | guest reports the concern inside the VM | +| Derived analysis | evidence/artifact computation over named inputs | stated inference only | + +No presence test, success flag, handle, snapshot entry, compiled model, or +planned matrix may promote a fact to a stronger class. In particular, +`ExplicitnessProvenance.BACKEND_REALIZED` says who chose a value; it does not mean +`daemon-observed` or `guest-observed`. + +### Account for every non-UNCHANGED operation + +- Derive one canonical, field-addressed concern inventory from the existing + `ProvisioningPlan` -> `Realization` interpretation. Validation, driver + accounting, evidence assembly, and mutation tests must consume that inventory; + they must not maintain separate lists of fields. +- Keep the existing realization-envelope concern taxonomy. Extend that taxonomy + in place for node service declarations rather than folding services into + `feature-binding` or adding a TechVault-local vocabulary. Cloud-init is a + mechanism for account/content/feature delivery, not a separate authored + concern and not proof that any of those concerns took effect. +- Every `CREATE` or `UPDATE` concern is either realized with an allowed + transformation and a named observation source, rejected as unsupported before + native mutation, or failed with a structured diagnostic. A resource-level + `DomainHandle` cannot satisfy its nested concerns. +- `DELETE` is accounted for by ownership-checked, daemon-observed absence of the + native object and cleanup of its owned artifacts. `UNCHANGED` remains a strict + no-driver-call operation and creates no fresh observation claim. +- Descriptor production, seed attachment, and port reachability must not satisfy + the stronger content, account, feature, service-identity, or application-state + concern they describe or transport. + +### Exact values are honored or rejected + +- Treat every concrete value in a directly submitted `ProvisioningPlan` as + binding at the backend boundary. Direct control-plane submission cannot rely + on compiler-only explicitness metadata being present. +- SEM-218 exact values may not be clamped, rounded, defaulted, substituted, + normalized to a different value, or omitted. The TechVault path must honor the + exact value or reject the plan before connection, cleanup, artifact creation, + `defineXML`, `networkDefineXML`, `create`, `destroy`, or `undefine`. +- A constrained/open concern may be transformed only when the selected envelope + admits the input and output and names the transformation. Record + backend-realized origin and the actual observation strength. The current + transformation-name list is not a free-form policy language and must not be + interpreted as permission to clamp an exact request. +- If no executable, governed transformation rule exists, use exact-or-reject. + Issue #714 must not add an ad hoc TechVault transformation language merely to + preserve today's positive-path demonstrations. +- Validate the whole plan before any native mutation. Unsupported account, + content, feature, ACL, service, image, resource, or network-property concerns + must produce addressed diagnostics together; do not mutate the supported + subset and then discover the rest. + +### Observe native state independently of planned state + +- Driver handles remain completion receipts, not evidence. Successful native + creation must be followed by typed, source-labelled readback for every concern + the issue claims as realized. +- Libvirt readback may support substrate existence, ownership, configured + architecture/image attachment, vCPU/memory definition, native network + definition, and domain/network attachment claims. Record only bounded, + portable comparisons; never publish raw XML, native object reprs, paths, UUIDs, + or connection data. +- Guest-applied IP configuration, cloud-init execution, accounts, placed content, + feature behavior, ACL effect inside the guest, and named service/application + identity require concern-specific guest evidence. Until issue #715 supplies + that evidence, issue #714 must classify them as unsupported/not observed and + reject binding requests, or narrow the emitted claim to the weaker substrate + fact actually observed. +- Opening a BusyBox HTTP listener on a requested port is not realization of + Wazuh, TheHive, MISP, PostgreSQL, SSH, DNS, or another named service. Likewise, + `native_soc_readback()` values synthesized from node names are derived test + data, not native SOC readback. +- A synthesized health listener is environment-visible augmentation. Remove it + or disclose it through the existing SEM-225 augmentation carrier with its + environment/comparability effects; do not hide it as a default service. + +### Commit and rollback stay fail-closed + +- `LibvirtProvisioner` may return success only after complete concern accounting + and required readback. `_call_backend_apply()` remains the runtime contract + backstop, not the component responsible for undoing an already dishonest + native success. +- Preserve the current deep-copied baseline snapshot on every validation, + driver, readback, contract, or cleanup failure. Failed results carry no changed + addresses and never attach a new envelope or provenance claim. +- The general runtime contract currently permits some non-libvirt backends to + return partial snapshots on failure, and the control plane persists whatever + `ApplyResult.snapshot` it receives. Issue #714 imposes a stricter TechVault + provisioner invariant: it must return the baseline snapshot itself. Do not + broaden or reinterpret unrelated workflow partial-failure semantics here. +- Track pre-existing owned objects separately from objects created by this call. + Compensate in reverse dependency order and verify the result through native + readback. A failed update must restore the prior definition or report the + affected ACES addresses as residual native state; preserving only the portable + snapshot is not rollback. +- Prefix-wide `clean_existing` deletion is not ownership proof and may not run as + a preflight shortcut. Reuse deterministic ACES ownership stamps and the + absence-vs-lookup-failure rules from `LibvirtDeploymentDriver`. +- Cleanup failure is a failed operation, not a warning. Diagnostics may name + safe ACES addresses and whether residual state remains; they must not dump the + residual XML or host paths. + +### Publish truthful evidence, not a renamed plan + +- Keep the local `aces.libvirt.scenario-evidence-run/v1` artifact and its + validator as the issue's evidence carrier; do not invent a second report + schema. Separate authored, planned, driver-reported, daemon-observed, + guest-observed, and derived sections in that artifact. +- A `native-live` basis requires the corresponding observed facts. A non-empty + `last_snapshot`, `realized_addresses`, domain list, or `expected_surface()` is + insufficient. Compiled node/service/network fields remain `planned` even when + some native substrate exists. +- Embed the canonical backend manifest identity and selected realization-envelope + identity, including configuration and envelope digests. Bind observations to + the concrete driver/backend version and secret-free material configuration. +- Keep the deterministic participant runtime explicitly labelled as deterministic + control-plane execution with no live guest execution. Native provisioning does + not upgrade its participant proof. +- The TechVault live-gate manifest and downstream cross-backend corpus must + preserve the same source labels. No downstream summary may upgrade a weaker + source discarded by its input artifact. +- Continue validating the full artifact before atomic write. Failed or residual + runs may be retained as failed evidence, but never with `passed=true` or a + realized basis. + +## Required Cross-Cutting Reuse + +- SDL ingress and semantic validation: `parse_sdl()` / `parse_sdl_file()`, closed + SDL models, `instantiate_scenario()`, `SemanticValidator`, and existing parse, + instantiation, and semantic error types. +- Planning and exactness: `CompiledRealizationRequirement`, + `realization_support_diagnostics()`, `realization_disclosure()`, + `ProvisioningPlan`, `ChangeAction`, and processor dependency/delete ordering. +- Envelope authority: `BackendRealizationEnvelopeModel`, + `RealizationConcern`, `ConcernDisposition`, `TransformationKind`, + `ObservationStrength`, `load_libvirt_realization_envelope()`, and the canonical + digest helpers. Keep coarse `ProvisionerCapabilities` and + `capability_envelope_diagnostics()` as separate necessary gates. +- Libvirt interpretation and IO: `interpret_provisioning_plan()`, `Realization`, + `DomainSpec`, `NetworkSpec`, `LibvirtDriver`, `DriverResult`, the structured XML + builders, deterministic ownership UUIDs, safe absence detection, and rollback + precedents in `LibvirtDeploymentDriver`. +- Runtime execution and persistence: `RuntimeManager`, `RuntimeControlPlane`, + `_call_backend_diagnostics()`, `_call_backend_apply()`, `ApplyResult`, + `RuntimeSnapshot`, `realization_provenance`, `realization_envelope`, + `ControlPlaneStore`, and atomic local-store writes. +- Error and operational observability: `Diagnostic`, `Severity`, + `OperationReceipt`, `OperationStatus`, control-plane audit events, and stable + package-local diagnostic codes. There is no need for a TechVault exception + hierarchy or logging-only result channel. +- Evidence and conformance: `BackendManifestV2Model`, + `ExperimentRealizedFormDisclosureModel`, SEM-225 augmentation disclosures, + `run_target_conformance()`, `BackendConformanceReport`, + `validate_libvirt_evidence_run_artifact()`, `redaction_violations()`, + `run_artifact_path()`, and `atomic_write_json_artifact()`. +- Schema authority, if the existing envelope concern taxonomy or runtime carrier + changes: the hand-governed schema, `schema_bundle()`, valid/invalid fixtures, + `contracts/schema-publication-manifest.json`, ADR-061 compatibility evidence, + packaged corpus, and `specs/authority/authority-boundary.yaml`. Evolve the + existing contract; do not publish a libvirt-only duplicate. + +## Security And Whole-Path Gates + +- **SDL/plan shape:** authored input passes the existing parser, closed models, + semantic validator, compiler/planner checks, closed `ProvisioningPlanModel`, + envelope identity validation, and libvirt capability/value gates. Unknown or + unsupported fields fail before IO; no backend-local SDL parser is added. +- **Target/config shape:** reuse `_validate_config_keys()`, + `_selected_driver_mode()`, `_validate_manifest_mode()`, the envelope loader, + and canonical configuration digest. Replace clamping/coercion in direct Python + entry points with validation; Typer option bounds alone do not protect library + callers. Material builder/kernel/image/resource/define mode affects identity or + is rejected in claim-bearing mode. Operational handles and secrets stay out of + the digest. +- **Authentication/authorization:** issue #714 needs no HTTP route. If a changed + snapshot or operation field crosses HTTP, it retains + `ControlPlaneSecurityConfig.strict_defaults()`, bearer/proxy verification, + backend/operator role checks, target scope, request-size limits, idempotency + fingerprints, and audit events. The current bearer-token branch returns before + the proxy branch's `identity.target_name` check; any issue #714 HTTP exposure + must enforce target scope after either authentication mechanism rather than + relying on that incumbent bug. The local destructive CLI retains explicit + operator confirmation; neither path grants host privileges. +- **Secrets:** credential-bearing libvirt URIs are forbidden in CLI argv and are + rejected rather than logged. Use a non-secret URI plus injected connection or + credential handle. SSH keys, account material, cloud-init bodies, generated + initramfs contents, connector reprs, environment dumps, and raw configuration + never enter digests, diagnostics, snapshots, audit details, fixtures, reports, + or command output. +- **Host files/processes:** reuse the generic seed workspace's ownership, + symlink, and permission rules if sensitive guest material is ever generated. + Do not place it in the current world-readable appliance metadata/HTML path. + Keep libvirt imports lazy, use structured XML, fixed argv, no `shell=True`, + bounded timeouts, controlled working directories, and no secrets in argv or + subprocess environments. No `sudo`, setcap, ambient capability, or daemon + inventory requirement belongs in the hermetic suite. +- **Error envelopes:** plan/config failures use safe addressed `Diagnostic` + values. Native exception strings, XML, stdout/stderr, probe output, paths, + object reprs, and stack traces do not cross the boundary. The existing + `_backend_call_failed()` and live/evidence `str(exc)` paths are unsafe for + secret-bearing configuration errors; avoid raising such values into them and + harden them if touched. +- **Persistence/API serialization:** the baseline snapshot remains byte-for-byte + equivalent on failure. Existing typed realization provenance and envelope + identity must round-trip through `ControlPlaneStore`, + `RuntimeSnapshotEnvelopeModel`, and `_snapshot_model()`; note that the current + HTTP serializer omits `realization_provenance`, so any claim depending on that + ledger must close this cross-boundary loss. Do not use `RuntimeSnapshot.metadata` + or `ApplyResult.details` as a native-state/evidence ledger. +- **Evidence output:** run ids pass the existing safe-label and root-confinement + helpers; artifacts pass embedded-contract, source-separation, boundary, and + redaction validation before atomic write. Host paths, connection URIs, native + UUIDs/XML, QEMU command lines, credentials, and private keys stay forbidden. + +## Whole-Repository Surfaces In Scope + +- Canonical contracts and policy: the TechVault realization envelope, + `realization-envelope-v1`, `backend-manifest-v2`, `provisioning-plan-v1`, + `runtime-snapshot-v1`, schema publication/authority manifests, and backend + provisioning-only profile. +- Backend/runtime path: `aces_backend_libvirt.realization`, `capability_envelope`, + `driver`, `drivers.libvirt`, `techvault_appliance`, `techvault_native`, + `techvault_probe`, `provisioner`, `manifest`, `target`, + `aces_runtime.backend_calls`, control-plane execution/API serializers, and + control-plane stores. +- Evidence path: `aces_operations.techvault_live`, + `libvirt_evidence_run`, `_evidence_run_artifact`, + `_evidence_run_validation`, cross-backend corpus projection/validation, and the + libvirt CLI presentation. +- Verification path: realization-envelope, manifest, provisioner, native driver, + evidence-run, runtime snapshot/API/store, target conformance, authority, and + real-libvirt opt-in suites; repository policy and full verification remain the + final mechanical gates. + +## Conformance And Falsification Guardrails + +- Reuse the injected/recording `LibvirtDriver` and fake-connection patterns. Do + not certify by monkeypatching the gate under test, inspecting source strings, + or calling `_native_matrix()` directly. +- Mutation cases must include a driver that drops cloud-init, ignores an account, + omits content, clamps resources, substitutes an image, and fabricates a + realized handle. Unsupported TechVault concerns should be rejected before the + mutation driver is called; supported concerns must fail when independent + readback does not match the request. A plausible handle alone must never pass. +- Also exercise service synthesis/omission, ACL omission, network defaulting, + stale or mismatched envelope/configuration identity, partial create, failed + update restoration, rollback failure, and residual-state reporting. These are + distinct failure modes and must not collapse into one happy-path handle test. +- Every rejection asserts: no pre-admission native call; failed operation status; + empty `changed_addresses`; prior in-memory and persisted snapshots unchanged; + no new realization provenance/envelope claim; and either verified native + cleanup or a safe residual-state diagnostic naming the affected ACES address. +- Artifact mutations must relabel planned or driver-reported data as + daemon/guest-observed, infer realization from a non-empty domain list, replace + the bound envelope/configuration identity, or inject forbidden host/secret + material. The existing artifact validator must reject every mutation before + atomic write. +- Keep `run_target_conformance()` as the backend-neutral target/profile runner. + Libvirt-specific mutation coverage belongs at the existing driver, + provisioner, evidence-validator, and target-integration seams, not in a + libvirt branch inside `aces_conformance`. +- The default suite remains hermetic. Real-libvirt evidence is opt-in, + reproducible, source-labelled, and must verify complete cleanup even when the + realization or probe fails. + +## Extensibility Seam + +The seam is one canonical concern inventory plus a source-specific observer +passed through the existing target/driver configuration boundary. The observer +is parameterized by ACES address/field path, concern, selected envelope identity, +and observation source; it returns bounded typed facts, not raw backend objects. + +Issue #714 supplies validation and daemon-level accounting for the bounded +TechVault appliance. Issue #715 can add guest observers for the same concern +inventory without changing SDL syntax, plan interpretation, diagnostic +hierarchy, control-plane workflow, or evidence taxonomy. A future appliance, +kernel/image policy, remote libvirt configuration, or stronger probe selects a +different normalized material configuration/envelope identity rather than +editing global planner or artifact rules. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating `SnapshotEntry.payload`, `status="applied"`, `changed_addresses`, a + handle, domain existence, or a non-empty driver matrix as native observation; +- comparing an authored value with the same planned payload twice and calling + the second copy backend evidence; +- using `last_snapshot`, `last_matrix`, `realized_addresses()`, + `expected_surface()`, or name-derived SOC counts as the evidence authority; +- allowing `bounded-normalization`, `default-substitution`, or + `image-substitution` to weaken an exact request; +- silently accepting accounts/content/features/ACLs/services because they were + aggregated into a `DomainSpec` the TechVault driver then ignores; +- calling a generic HTTP listener the requested named service or calling seed + creation guest application; +- inferring guest IP state from a planned DHCP allocation or daemon network XML; +- overloading SEM-218 origin provenance with observation strength, or using the + SEM-225 augmentation carrier as a substitute for realization evidence; +- adding a second concern enum, plan payload extractor, schema registry, + validation stack, exception hierarchy, store, endpoint, or report writer; +- binding claims only to a driver class/mode while injected material behavior can + vary under the same configuration digest; +- deleting resources by prefix, treating every lookup error as absence, or + reporting rollback success without post-cleanup readback; +- relying on the general runtime's permissive partial-failure snapshot behavior + for the stricter issue #714 baseline-preservation contract; +- assuming bearer authentication currently enforces target scope; +- preserving the portable snapshot while hiding known residual native state; +- echoing raw exceptions, XML, stdout/stderr, host paths, connection URIs, + account/cloud-init material, or probe details in diagnostics or artifacts; +- weakening assertions or relabelling current tests merely to keep the existing + TechVault positive path green. + +## Non-Goals And Implementation Boundaries + +- No implementation of issue #714 in this preflight. +- No new SDL syntax, backend profile, control-plane route, persistence service, + libvirt-specific public DTO, exception hierarchy, or parallel report schema. +- No implementation of real TechVault/Wazuh/TheHive/MISP/application services, + generic cloud-init delivery, accounts, content, features, ACLs, or guest + configuration. Unsupported concerns are rejected and disclosed honestly. +- No guest-observed certification; issue #715 owns guest probes. Issue #714 may + only emit daemon/driver strengths it actually proves. +- No claim that the deterministic participant adapter executes inside live + guests, and no coupling of participant-runtime success to substrate success. +- No final real-libvirt scenario certification or backend-equivalence claim; + issues #716 and #717 own broader conformance and final evidence. +- No compatibility fallback that accepts missing concern accounting, stale or + mismatched envelope/configuration identity, or failed cleanup as success. diff --git a/docs/decisions/issue-714-asr-519-techvault-realization-disclosure.md b/docs/decisions/issue-714-asr-519-techvault-realization-disclosure.md new file mode 100644 index 000000000..5f81dc5df --- /dev/null +++ b/docs/decisions/issue-714-asr-519-techvault-realization-disclosure.md @@ -0,0 +1,81 @@ +# Issue 714 / ASR-519 TechVault Realization Disclosure + +Requirement: ASR-519 (`9ad95f1d-7c4a-4532-8a3e-50453e8286d4`). + +The TechVault appliance driver is a bounded native-substrate mode. It does not +claim to provision the full TechVault guest/application stack. Admission, +readback, persistence, and evidence publication all use the concern boundaries +below. + +## Concern accounting + +| Concern | Current disposition | Success evidence | +|---|---|---| +| topology | realized | active native object and exact native-name readback | +| architecture | realized | domain XML readback | +| image | selected generated-initramfs policy realized; concrete images rejected | exact attached kernel/initramfs paths plus artifact digests | +| resource allocation | realized exactly | domain memory/vCPU XML readback | +| network | realized exactly | network/domain XML readback for CIDR, gateway, forwarding policy, and attachments | +| content placement | unsupported | typed pre-I/O diagnostic | +| account placement | unsupported | typed pre-I/O diagnostic | +| feature binding | unsupported | typed pre-I/O diagnostic | +| service | unsupported | typed pre-I/O diagnostic | +| ACL | unsupported | typed pre-I/O diagnostic | + +Guest readiness, applications, and SOC state are not observed. A domain handle, +domain name, declared listener, ping, or TCP connection is not accepted as proof +of any nested concern. + +## Admission and exactness + +The provisioner validates TechVault concerns before snapshot reconciliation and +before driver I/O. The native driver repeats the gate for direct callers. Values +outside the published envelope, implicit network values, silently normalized or +duplicate names, concrete images, guest placements, services, ACLs, unbound +metadata, and unsupported update/recovery shapes fail closed. + +Memory and CPU values are never clamped. Network `internal: false` remains an +explicit false value and is verified as NAT forwarding; it is not lost as an +omitted default. + +## Observation and commit + +Each admitted field has one typed `daemon-observed` readback record. Missing, +duplicate, wrong-source, type-coerced, or mismatched records fail the operation. +An exact daemon report and realization binding are required before the runtime +snapshot commits. + +Reports keep these sources separate: + +- authored scenario identity; +- planned topology and concern values; +- driver-reported operation outcomes; +- bounded daemon-observed substrate facts; +- guest-observed facts, currently `not-observed`; and +- derived evaluator analysis. + +The binding covers the published realization-envelope and configuration digests, +hashed connection/naming configuration, and hashes of the actual run-local kernel +and initramfs artifacts. Artifacts never contain raw libvirt XML, UUIDs, host +paths, connection URIs, credentials, or exception text. + +## Recovery + +Partial creation and post-create readback/binding failures trigger cleanup of only +the current operation's ownership-stamped resources. Cleanup is successful only +after native absence is verified. Lookup/listing uncertainty, ownership conflict, +or failed destroy/undefine produces a residual-state diagnostic and withholds +success. Prefix-wide cleanup is not available. + +Updates and compound delete transactions are rejected until a verified native +restore path exists. Failed provisioner operations retain the prior runtime +snapshot; successful native deletion clears the driver's prior observation report +so it cannot be reused as fresh evidence. + +## Verification focus + +Falsification coverage includes clamped resources, fabricated handles, incomplete +and duplicate observations, type coercion, inactive readback, substituted boot +artifacts, extra attachments, altered forwarding policy, foreign ownership, +partial rollback, unverifiable cleanup, stale evidence relabeling, and mismatched +realization bindings. diff --git a/docs/decisions/issue-715-asr-519-guest-observed-libvirt-probes-preflight.md b/docs/decisions/issue-715-asr-519-guest-observed-libvirt-probes-preflight.md new file mode 100644 index 000000000..587e6251d --- /dev/null +++ b/docs/decisions/issue-715-asr-519-guest-observed-libvirt-probes-preflight.md @@ -0,0 +1,326 @@ +# Issue 715 / ASR-519 Guest-Observed Libvirt Probes Preflight + +Date: 2026-07-12 + +Requirement: ASR-519. + +This note records architecture guardrails for proving selected libvirt +realization concerns from inside a booted guest. It is guidance only: it does +not add a probe, image, envelope, schema, test, command, or evidence report, and +it is not an implementation plan. + +No new ADR is required. ADR-021, ADR-066, ADR-070, and the issue #100 and #714 +preflights already own the claim, evidence-plane, realization-envelope, and +configuration-identity boundaries. This note applies them to issue #715. + +## Binding Sources + +- ADR-021 requires falsification evidence before a realization claim is + demonstrated. ADR-066 separates operational observation, captured evidence, + and derived analysis. +- ADR-070, `specs/formal/realization/envelope-semantics.md`, the published + `realization-envelope-v1` schema, and issue #100 own configuration-specific + envelope identity and the closed concern/observation-strength taxonomy. +- `specs/formal/realization/explicitness-and-realization.md` and the issue #491 + preflight own SEM-218 exactness and origin provenance. Backend origin is not + guest observation. +- The issue #603, #604, #606, #714, and #615 preflights own production apply, + ownership-safe teardown, target conformance, TechVault honesty, and evidence + boundaries. +- `ProvisioningPlan`, `Realization`, `DomainSpec`, `NetworkSpec`, + `RealizationObservation`, `DriverResult`, `Diagnostic`, `ApplyResult`, + `OperationStatus`, and `RuntimeSnapshot` are the incumbent execution and + error surfaces. +- `aces_operations.libvirt_evidence_run`, `_evidence_run_artifact`, + `_evidence_run_validation`, `_techvault_cleanup`, `run_artifacts`, and the + existing libvirt CLI commands are the incumbent proof workflow, validation, + redaction, cleanup, and persistence surfaces. + +## Architecture Decisions And Guardrails + +### Select a truthful material configuration + +The current `techvault-appliance-v1` configuration deliberately boots a +generated initramfs and rejects concrete images, cloud-init placements, +services, and ACLs. Keep that configuration and its weaker daemon-only claims +intact. It cannot become guest-certified merely by attaching a probe. + +A canonical image/appliance proof must select a separately constructible, +versioned material configuration and realization envelope. Its secret-free +configuration identity must cover at least the image/appliance content digest, +architecture, boot/firmware and seed policy, network policy, supported concern +set, guest-observation transport and probe-policy version, and any injected +environment-visible attestation mechanism. Host paths, connection handles, +credentials, and raw probe configuration are not digest input. + +Image resolution is target configuration, not new SDL syntax. An authored +source reference must resolve through one closed, configuration-selected image +policy to bytes whose digest is verified before libvirt mutation. A missing, +mutable, mismatched, or unverified image fails before define/create. Do not +overwrite the existing published envelope while retaining its id/digest, and do +not reuse the generic envelope's driver-reported cloud-init claims as +guest-observed proof. + +Manifest capability projection, selected envelope, provisioner identity, driver +mode, and evidence binding must all name the same normalized configuration. +Unknown or inconsistent target config continues to fail through +`_validate_config_keys()`, `_selected_driver_mode()`, +`_validate_manifest_mode()`, the envelope loader, and the provisioner's +identity gate. + +### Keep one concern inventory and layered observation + +Derive the field-addressed concern inventory from the existing +`ProvisioningPlan` -> `Realization` -> `DomainSpec`/`NetworkSpec` path. Probe +selection, comparison, artifact assembly, and mutation tests consume that +inventory; they must not reparse SDL, inspect YAML dictionaries, or maintain a +TechVault-only list of authored fields. + +The maximum claim is determined per field, not per domain: + +| Claim | Required observation | +| --- | --- | +| Native domain identity/state and attachment | ownership-checked libvirt daemon readback | +| Requested vCPU/memory | exact daemon definition plus bounded guest-visible CPU/memory corroboration | +| Guest network attachment/addressing | daemon attachment correlated with guest link/MAC/IP readback | +| Boot readiness | fresh guest transport response; domain `active` is insufficient | +| Initialization completion | guest-reported completion for the selected initialization mechanism, with no failure state | +| File/directory/dataset content | guest-side type, mode, bounded membership, and expected content digest/value comparison | +| Account properties | guest-side identity, groups, home, shell, disabled/credential posture without returning credential material | +| Feature/service state | concern-specific installed/configured identity and active behavior; a listener alone is insufficient | +| ACL behavior | positive and negative behavior at the declared enforcement boundary, correlated to the affected addresses | + +`RealizationObservation` remains the bounded fact carrier and must use +`ObservationStrength.GUEST_OBSERVED` only for a fact actually read at that +boundary. `Diagnostic` remains the failure carrier. Evolve the existing +`NativeLibvirtProbe` injection seam to return typed observations and diagnostics; +do not add a public probe DTO, exception hierarchy, logging-only result, or a +second concern enum. The current boolean/`detail` `ProbeResult` is not an +evidence or error envelope and raw detail must not escape the probe boundary. + +Observation proceeds through explicit stages: daemon state, guest transport, +initialization, concern probes, and cleanup. A later stage cannot repair or +upgrade a failed earlier stage. Each stage has a bounded timeout under one +overall deadline. Timeout, partial boot, unavailable transport, initialization +failure, missing/malformed/duplicate observation, unexpected type, and value +mismatch are distinct stable diagnostic codes that name the safe ACES address +and observation level. + +### Prove freshness and bind the evidence + +A fresh per-run, non-secret challenge must enter the guest through the selected +configuration's disclosed initialization/attestation mechanism and be read back +by the guest observer. The challenge is operational augmentation, not authored +scenario meaning; classify it through the existing SEM-225 augmentation carrier +as environment-visible and comparability-relevant. A prior boot, cached +cloud-init result, old driver snapshot, or response without the current +challenge cannot pass. + +Clear run-local observation state before every apply attempt and publish it only +after complete validation. Evidence assembly binds each normalized observation +to the control-plane operation id, ACES operation/address and field path, +concern, selected envelope/configuration digests, image/appliance digest, +observation source and level, UTC observation timestamp, and a `sha256:` +correlation derived from the ownership-verified native identity. Raw UUIDs, +MACs used only for correlation, instance ids, and native names are not portable +identity and do not enter the artifact. + +The operation id is joined at the operations/control-plane boundary after +`submit_provisioning()`; do not widen `LibvirtDriver.realize()` merely to pass +control-plane metadata into backend IO. The driver supplies fresh addressed +observations and bounded native correlation material, while the evidence +producer supplies run/operation identity and timestamp. Validation must reject +unjoined, stale, cross-operation, cross-envelope, or duplicate evidence. + +### Fail and clean up closed + +Production realization still enters through +`RuntimeManager.plan()` -> `RuntimeControlPlane.submit_provisioning()` -> +`LibvirtProvisioner.apply()` -> the injected native driver. A direct driver +call, hand-built `DomainSpec`, pre-existing VM, or fake connection can test a +leaf but cannot satisfy the native-proof gate. + +Guest proof is part of commit eligibility for the selected guest-certified +configuration. The provisioner cannot return success, changed addresses, a new +envelope/provenance claim, or a committed snapshot until all required daemon +and guest observations pass. Every failure preserves the baseline portable +snapshot. Unexpected backend output still passes through `_call_backend_apply()` +and the existing result/snapshot/SEM-218 gates. + +Cleanup runs in a `finally`-equivalent path after every attempt, including +planning-after-construction errors, timeout, partial boot, guest-probe failure, +artifact-validation failure, and interrupted evidence production. The current +live/evidence flows clean only after a successful captured snapshot; issue #715 +must not retain that limitation. Reuse deterministic ACES ownership stamps and +verified absence rules. Verify domains, networks, owned filters, disks/overlays, +seed media, and probe/attestation artifacts. Missing private driver inventory or +lookup uncertainty is not absence. Any residual or unverifiable category makes +the run fail and is reported only as a safe category/count plus ACES address. + +### Keep one claim-bearing evidence workflow + +Extend the existing `aces.libvirt.scenario-evidence-run/v1` local artifact and +`validate_libvirt_evidence_run_artifact()` source/redaction/binding gates; do not +create a guest-proof schema or a third report assembler. The TechVault live-gate +manifest may remain a bounded operator summary, but it must delegate to or +reference the same validated run result rather than independently decide guest +success. It may never upgrade or omit the canonical artifact's source labels, +failures, binding, or cleanup outcome. + +Continue using `run_artifact_path()`, safe run-id validation, canonical JSON, +full pre-write validation, and `atomic_write_json_artifact()`. A failed run may +emit a redacted failure artifact only when the validator admits that shape; it +cannot set `passed=true` or publish realized facts after failed cleanup. The +operator/self-hosted entry point remains under `aces libvirt`; it must retain an +explicit destructive confirmation and return non-zero on any failed stage. + +Hermetic fake-driver/probe tests validate orchestration and falsification but +cannot satisfy the native-proof gate. Committed proof must be generated by the +same operator command against a real libvirt/QEMU daemon through the production +apply path, validate before commit, identify the selected envelope/image by +digest, and contain no host-specific or secret material. + +## Required Cross-Cutting Reuse + +- **SDL and planning:** `parse_sdl_file()`, closed SDL models, + `instantiate_scenario()`, `SemanticValidator`, `RuntimeManager.plan()`, + `CompiledRealizationRequirement`, `realization_support_diagnostics()`, + `realization_disclosure()`, `ProvisioningPlan`, and processor dependency and + delete ordering. +- **Envelope and manifest:** `BackendRealizationEnvelopeModel`, + `RealizationConcern`, `ConcernDisposition`, `ObservationStrength`, canonical + digest helpers, `load_libvirt_realization_envelope()`, + `backend_manifest_payload()`, `BackendManifestV2Model`, and existing + capability/manifest consistency checks. +- **Libvirt:** `interpret_provisioning_plan()`, `Realization`, `DomainSpec`, + `NetworkSpec`, `RealizationObservation`, `DriverResult`, + `TechVaultNativeLibvirtDriver`, structured XML builders, private seed + workspace safeguards, deterministic ownership UUIDs, safe absence detection, + and current rollback/cleanup helpers. +- **Runtime, errors, and persistence:** `RuntimeControlPlane`, + `_call_backend_diagnostics()`, `_call_backend_apply()`, `Diagnostic`, + `Severity`, `OperationReceipt`, `OperationStatus`, `ApplyResult`, + `RuntimeSnapshot`, realization provenance/envelope carriers, + `ControlPlaneStore`, atomic store writes, and audit events. +- **Evidence and workflow:** `libvirt_evidence_run`, + `validate_libvirt_evidence_run_artifact()`, `redaction_violations()`, + `cleanup_native_snapshot()`, `run_artifact_path()`, + `atomic_write_json_artifact()`, the existing libvirt CLI group, and + `run_target_conformance()` for backend-neutral conformance. +- **Contract governance:** if the published envelope carrier changes, update + the hand-governed schema, model/generator parity, valid/invalid fixtures, + `contracts/schema-publication-manifest.json`, packaged corpus, and + `specs/authority/authority-boundary.yaml`. Evolve the shared contract only + when its current closed fields cannot express a portable fact. + +## Security And Whole-Path Gates + +- **Input and shape:** scenario input passes the parser, closed Pydantic shapes, + semantic validator, compiler/planner, plan model, envelope relation/identity, + provisioner capability and concern gates, then observation completeness/value + validation. Unknown input or unsupported concerns fail before IO; probes do + not parse authored SDL. +- **Target/config:** all driver mode, image policy, probe transport/policy, + timeouts, and augmentation choices pass one extended `_validate_config_keys()` + and normalized target configuration before manifest construction. Library + callers receive the same validation as Typer callers. Image bytes and probe + policy are digest-bound; paths, handles, and credentials are not serialized. +- **Authentication/authorization:** no new HTTP route is required. In-process + operator execution retains explicit confirmation. If provisioning is exposed + through HTTP, it keeps `ControlPlaneSecurityConfig.strict_defaults()`, verified + bearer/proxy identity, backend/operator roles, target scope after either auth + mechanism, request-size limits, idempotency fingerprints, and audit events. + Guest-probe capability does not grant a caller general guest command execution. +- **Secrets:** connection URIs with user information, passwords, tokens, private + keys, cloud-init bodies, account material, probe credentials, environment + dumps, and connector/transport reprs never enter CLI argv, digests, + diagnostics, audit details, snapshots, fixtures, artifacts, or command output. + Prefer a libvirt guest-agent or equivalent injected transport that needs no + guest credential. Any later SSH transport must use an injected credential + handle, strict host identity verification, and bounded allowlisted operations; + accepting a password/key CLI option is forbidden. +- **Guest/host OS exposure:** attach only the selected, disclosed guest channel; + it is not a generic remote shell surface. Probe requests are fixed, + concern-specific, read-only, size-bounded, and allowlisted. Parse bounded + structured output and discard raw stdout/stderr. If a subprocess leaf is + unavoidable, use fixed argv, no `shell=True`, bounded timeouts, controlled + cwd/environment, and no secret argv or environment entries. Keep libvirt + imports lazy and generated secret-bearing seed files under the existing + ownership/symlink/mode protections. +- **Errors/logging:** public failure data is a stable package-local diagnostic + code, safe address, observation level, and generic message. Do not propagate + `ProbeResult.detail`, `str(exc)`, raw agent replies, XML, command output, + paths, native ids, or tracebacks through `_backend_call_failed()`, CLI output, + logs, audit, or evidence. Operational logging is supplemental; it cannot be + the only failure or evidence channel. +- **Persistence/evidence:** runtime snapshots and operation records retain only + portable ACES state and typed envelope/provenance. Guest facts belong in the + validated evidence artifact, not `RuntimeSnapshot.metadata`, + `ApplyResult.details`, or a private observation database. The shared redaction + gate and artifact validator run before atomic persistence. +- **Cleanup:** the host layer verifies owned native objects and every run-local + artifact category after success and failure. It neither scans/deletes by name + prefix nor treats all lookup exceptions as absence. Residual state prevents a + passing operation/report. + +## Extensibility Seam + +The seam is the normalized material target configuration selecting a versioned +envelope plus the existing injected `NativeLibvirtProbe`/driver boundary. The +observer is parameterized by the canonical concern inventory, safe ACES +address/field path, per-stage/overall deadlines, selected probe-policy version, +and envelope identity, and returns only `RealizationObservation` and +`Diagnostic` values. + +A second image family, architecture, initialization system, libvirt guest-agent +implementation, or credential-free transport adds a configuration/envelope +variant and observer implementation. It must not require changes to SDL syntax, +the concern enum, planner actions, runtime control-plane route, error hierarchy, +store, report writer, or evidence-source taxonomy. + +## Falsification Guardrails And Gotchas + +- Mutations must cover false handles, inactive/wrong domains, CPU or memory + mismatch, wrong NIC/MAC/IP, missing current challenge, stale prior snapshot, + partial boot, guest transport timeout/unavailability, omitted or failed + initialization, missing/duplicate/type-coerced facts, wrong content digest, + wrong account properties, synthesized/wrong service identity, ACL positive or + negative mismatch, envelope/image/probe-policy mismatch, and incomplete + cleanup. +- Every failed mutation asserts a failed `OperationStatus`, empty + `changed_addresses`, unchanged in-memory and persisted baseline snapshots, no + new realization provenance/envelope claim, no passing evidence artifact, and + verified cleanup or a redacted residual-state failure. +- Do not equate active domain, ping, TCP connect, guest-agent availability, + cloud-init `done`, package presence, process presence, or a seed descriptor + with all nested concerns. Each is evidence only for its exact named fact. +- Do not compare authored/planned data with an echo written into the guest and + call that independent observation. The probe must read the realized system or + exercise behavior at the enforcement boundary; the per-run challenge proves + freshness, not concern correctness. +- Do not expose a generic command runner, accept probe commands from SDL, infer + checks from service names, duplicate the concern inventory, or make raw probe + output part of the evidence schema. +- Do not let a fake driver/probe, direct driver call, hand-built spec, + pre-existing guest, skipped cleanup, or self-skipping integration test produce + the committed native proof. +- Do not broaden the default hermetic verification graph to require libvirt, + QEMU/KVM, privileges, a host image, network access, or credentials. The + self-hosted proof is an explicit separate gate. + +## Non-Goals And Implementation Boundaries + +- No implementation of issue #715 in this preflight. +- No new SDL syntax, universal image registry, backend profile, capability + language, public probe DTO/schema, control-plane route, exception hierarchy, + persistence service, observation database, or parallel report format. +- No general-purpose guest management/remote-execution API and no participant + observation capability claim. Backend guest realization evidence remains + operational/captured evidence unless a separate governed participant boundary + projects it. +- No claim that one canonical guest proves all images, operating systems, + services, ACL mechanisms, TechVault applications, SOC detection quality, + backend equivalence, or envelope subsumption. +- No issue #716 honesty-conformance runner or issue #717 final scenario + certification. Issue #715 supplies concern-specific observations that those + downstream gates may consume. diff --git a/docs/decisions/issue-721-dsl-105-canonical-sdl-yaml-preflight.md b/docs/decisions/issue-721-dsl-105-canonical-sdl-yaml-preflight.md new file mode 100644 index 000000000..ad12ffafd --- /dev/null +++ b/docs/decisions/issue-721-dsl-105-canonical-sdl-yaml-preflight.md @@ -0,0 +1,392 @@ +# Issue 721 DSL-105 Canonical SDL YAML Preflight + +Date: 2026-07-11 + +Issue: #721. + +Requirement: DSL-105, Typed Parsing, Normalization, And Authoritative SDL +Format. + +This note records architecture preflight guardrails for defining one canonical +SDL YAML source format. It is guidance for the implementation. It does not +change parser behavior, schemas, examples, diagnostics, or digest semantics. + +## Binding Sources + +- `specs/sdl/` is the language-neutral normative SDL authority. Its + `document-model.md` and `diagnostics.md` already own encoding, mapping + identity, normalization scope, document phases, and fail-closed diagnostics. +- ADR-001 owns the SDL/parser decision. Its case-insensitive and hyphenated key + handling may remain as explicitly selected migration behavior; it must not + remain an undocumented second canonical dialect. +- ADR-009, ADR-019, and `specs/authority/authority-boundary.yaml` make + `contracts/schemas/` and `contracts/fixtures/` normative while Python is a + non-normative reference implementation. +- ADR-053 owns module parsing, trust, lock, digest, signature, and composition + behavior. Canonical source handling must enter that pipeline rather than + bypass it. +- ADR-061 and `contracts/schema-publication-manifest.json` own published-schema + evolution and change records. The current SDL schemas are `draft`; correcting + their phase description is allowed in place but still requires the existing + ledger and reference-bundle parity gates. +- ADR-075 and `specs/evolution/versioning-deprecation-and-migration.md` separate + compatibility, deprecation, and migration. A migration spelling is not a + second stable format and must never be silently reinterpreted as canonical. + +## Architecture Decisions + +### One source profile + +The authoritative source profile is `sdl-yaml/v1` with these rules: + +- Input is UTF-8, contains exactly one YAML 1.2.2 document, and uses the YAML + 1.2 Core schema for implicit scalar resolution. YAML 1.1 coercion is not part + of SDL: plain `yes`, `no`, `on`, and `off` are strings, while booleans are + `true` or `false` under the Core rules. +- The constructed value domain is I-JSON/JCS-compatible: mappings, sequences, + Unicode strings without lone surrogates, null, booleans, integers that are + in the interoperable range `[-9007199254740991, 9007199254740991]`, and finite + IEEE-754 binary64 numbers. The Core schema does not implicitly construct + timestamps; date-like plain scalars are strings. Native timestamp, binary, + set, ordered-map, pair, arbitrary-object, non-finite-number, and other + implementation-specific values are invalid. A future need for wider integers + or decimals requires a typed string representation and a new canonicalization + profile, not silent implementation-dependent rounding. +- Explicit tags and tag directives are not SDL authoring syntax. Quoting is the + portable way to force a string. A safe loader remains mandatory, but + "SafeLoader accepted it" is not a validity rule. +- Every mapping key is a string scalar. Exact duplicates and normalized + structural-key collisions are rejected on the source-marked node graph before + a last-write-wins dictionary or model can be built. +- Acyclic anchors and aliases are representation-only conveniences. They add no + identity, reference, import, or precedence semantics. Cycles and operational + alias/node/depth budget exhaustion fail cleanly without partial construction. +- `<<` merge keys are not canonical `sdl-yaml/v1`. An explicit migration read + may retain the current disjoint-union behavior, but it must diagnose the merge + as non-canonical; any source/local or source/source conflict remains fatal. +- Comments, scalar style, anchor names, line endings, and mapping order do not + change SDL meaning. Sequence order and string code points do. No Unicode + normalization is applied to authored string data or literal map keys. + +These rules belong in the normative document model and conformance fixtures, +not in PyYAML behavior notes. A conforming implementation must be able to +implement them without reading Python. + +The source profile applies to authored text before convenience transforms. +`_prepare_content()` currently dedents every input and `load_scenario()` strips +it; neither operation may precede canonical source validation because it can +change source ranges, make invalid indentation valid, or change block-scalar +content. Test callers may dedent their own literals. Likewise, no corpus, +formatter, or adapter may `safe_load()` and re-dump SDL before the source profile +sees it: that destroys duplicate keys, tags, aliases, authored spellings, scalar +tags, and source locations. + +### Canonical fields, migration spellings, and shorthands + +- The canonical spelling of a structural field is its exact published-schema + property name. Multiword SDL fields use lowercase `snake_case`. Intentional + single-word wire names such as `class`, `type`, `next`, `then`, `else`, and + `default` remain exact even when Python needs a differently named attribute. +- Existing kebab-case, mixed-case, and uppercase structural keys are migration + spellings only. Migration matching is ASCII case-folding plus `-` to `_`, and + only succeeds when the result is an actual canonical field in that structural + scope. There is no fuzzy matching or typo correction. +- Canonical validation is strict by default. Migration acceptance is explicit, + returns the same typed model, and emits a stable non-fatal diagnostic carrying + the authored spelling, canonical spelling, JSON Pointer, and source range. + Strict mode reports the same condition as a fatal source diagnostic. Neither + mode may silently overwrite a canonical field or accept incompatible + metadata/mode-based scenario dialects. +- Literal/user-defined mapping keys remain byte-for-byte authored strings. Node + names, section identifiers, `facts`, `labels`, `log_options`, native options, + and extension keys are not structural-field aliases. `Web-App` and `web_app` + may therefore remain distinct identifiers where the existing mapping-scope + catalog says the map is literal. +- The existing documented shorthands remain SDL authoring syntax, not another + dialect. They expand once in the shared parser before typed construction. + Canonical serialization is always longhand. Stale `min-score` guidance must + not reintroduce the scoring surface removed by ADR-073. +- Enum values continue to use the existing case/hyphen normalization in + `normalize_enum_value()` and `parse_enum_or_var()`; their canonical serialized + values are the declared lowercase values. Field-key normalization and enum + normalization remain separate concepts. + +Canonical scalar and collection types are the YAML 1.2 Core-resolved types that +the normalized schema declares. A whole-string `${name}` placeholder is the +documented exception for a variable-capable typed field. Generic Pydantic +coercion is not SDL syntax: quoted integers, `yes`/`no`/`on`/`off` or `0`/`1` as +boolean spellings, arbitrary strings in a placeholder branch, and scalar-to-list +coercion are canonical only if the normative source contract explicitly names +that exact shorthand. Existing convenience coercions retained for compatibility +are migration syntax and receive the same source diagnostics as field aliases. +The published normalized schema should constrain placeholder-only string +branches to the variable-token grammar instead of advertising every string as a +valid normalized value. + +### Raw syntax, normalized model, and schemas are distinct phases + +The implementation must keep these boundaries explicit: + +1. raw `sdl-yaml/v1` source representation; +2. normalized authoring object after key canonicalization, declared scalar and + collection normalization, shorthand expansion, enum normalization, and typed + structural construction; +3. expanded authoring scenario after module composition; +4. instantiated scenario after variable binding; +5. compiled/runtime artifacts. + +`sdl-authoring-input-v1.json` is the normalized authoring-object schema. Its +title and description must say that it applies after YAML source-profile checks +and normalization, but before module expansion and instantiation. It is not a +validator for YAML token spelling, duplicate keys, tags, anchors, aliases, or +migration diagnostics. `instantiated-scenario-v1.json` remains the concrete +post-instantiation schema. + +Do not publish a second full Scenario schema merely to label raw YAML. The raw +contract is the normative source-profile prose plus valid, invalid, and +migration fixtures under the existing SDL fixture family. Canonical reusable +examples use canonical keys, canonical enum spellings, and expanded longhand so +their directly decoded object validates against the artifact advertised for +authoring. "Directly decoded" means decoded once with the shared `sdl-yaml/v1` +resolver and source checks, not PyYAML defaults. A parse-then-`model_dump()` +schema test remains useful normalized-model evidence, but it must not be +presented as proof that the raw example matched the source contract. + +`schema_bundle()`, `tools/generate_contract_schemas.py`, and +`tools/check_generated_schemas.py` remain the reference-compatibility proof. +Schema changes remain hand-governed through the existing publication manifest; +there is no second SDL schema registry, source-of-truth model, or change ledger. + +### Canonical semantic serialization and digest + +Canonical identity is defined over a successfully parsed, expanded, and +semantically validated authoring scenario immediately before instantiation. It +is not defined over raw YAML bytes and is unavailable for parse-only or +semantically invalid input. + +The canonical payload is a versioned semantic projection, not a blind +`model_dump()` and not a claim that the bytes are round-trippable authoring YAML. +Its scenario member uses public JSON-mode values and canonical wire property +names in normalized longhand. It preserves authored field presence after +normalization: omitted defaults must not be materialized indiscriminately, +because `model_fields_set` feeds the normative SEM-218 explicitness classifier +and can change compiler/planner behavior. Shorthand-expanded fields count as +present in the normalized meaning. + +The projection must also include a stable representation of every otherwise- +private channel that can change instantiation, compilation, or planning, or +prove that the channel is derivable from the scenario member. Today that +includes `module_variable_specs` and `module_node_variable_refs`; excluding them +would make scenarios with different imported-module constraints share an +identity. Derived explicitness may be omitted only when preserved field presence +and values deterministically reconstruct it. Semantic advisories, source ranges, +filesystem paths, trust/cache evidence, and explanatory provenance text are +non-semantic and excluded. The profile specification must enumerate the +projection; Python private-attribute names are not the contract. + +Object members, including preserved literal keys, are serialized using the +[RFC 8785 JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html); +arrays retain their semantic order and strings are not Unicode-normalized. Input +is already constrained to JCS's I-JSON number and Unicode domain, so +canonicalization must never round or repair a value. The canonical bytes are +UTF-8. The digest is SHA-256 over those bytes and is represented with the +repository's existing lowercase `sha256:` convention. A digest record must +identify the canonicalization profile (`aces-sdl-semantic/v1`); a bare hash must +not imply an unversioned algorithm. + +This semantic digest is deliberately separate from: + +- an authored file's raw byte digest; +- OCI layer, config, and manifest digests; +- module lockfile `content_digest`, `manifest_digest`, and `export_hash`; and +- signatures over the existing module signer payload. + +Those existing values protect source/package integrity and supply-chain +identity. They must not be silently redefined to ignore comments, aliases, +spelling, or file layout. Conversely, a semantic digest is not a signature, a +trust decision, a secret-redaction mechanism, or proof that a scenario ran. + +## Required Incumbents + +- Authority and publication: `specs/sdl/document-model.md`, + `specs/sdl/diagnostics.md`, `contracts/schemas/sdl/`, + `contracts/fixtures/sdl/`, `contracts/schema-publication-manifest.json`, + `tools/check_schema_publication.py`, `tools/check_generated_schemas.py`, and + `specs/authority/authority-boundary.yaml`. +- Source loading and mapping identity: `aces_sdl._yaml_loader`, + `aces_sdl._mapping_scopes.MappingScope`, `HASHMAP_SECTIONS`, + `NESTED_HASHMAP_FIELDS`, `is_literal_map_field()`, and the existing + source-ranged mapping analyzer. +- Normalization and typing: `parser._load_normalized_data()`, + `_normalize_keys()`, `_expand_shorthands()`, `aces_sdl._base` enum/scalar + helpers (including the existing integer/float/boolean coercion audit surface), + `SDLModel(extra="forbid")`, `Scenario`, `aces_sdl.explicitness`, and + `SemanticValidator`. +- Composition and trust: `parse_sdl_file()`, `expand_sdl_modules()`, + `ImportDecl`, `ModuleDescriptor`, `resolve_import()`, `TrustPolicy`, lockfile + validation, digest/version/export checks, signature verification, path + confinement, cycle rejection, and bounded OCI extraction. +- Public ingress surfaces: `parse_sdl()`, `parse_sdl_file()`, + `load_sdl_fragment()`, `load_scenario()`, the reference processor, + language-service format/edit/diagnostic helpers, MCP authoring and operation + tools, the static MCP reference/example text, example-library checks, and SDL + CLI verify/publish commands. Imported documents and fragments must use the + same source policy as roots. +- Diagnostics and presentation: `SDLError`, `SDLParseError`, + `SDLParseDiagnostic`, `SDLValidationError`, `ScenarioValidationError` as the + existing high-level wrapper, `_language_diagnostics`, and MCP + `operation_support.stage_error()`. Add codes or advisory severity to this + envelope; do not add another exception or response hierarchy. +- Verification: `test_sdl_parser.py`, `test_yaml_mapping_keys.py`, + `test_sdl_fuzz.py`, `test_language_service.py`, `test_mcp_server.py`, + `test_example_schema_conformance.py`, `test_pipeline_determinism.py`, and the + canonical nox `contracts`, `tests`, `fuzz`, `docs`, and `verify` sessions. + Extend the existing example-corpus leg to check original source before + parse-and-dump validation; do not create a second corpus registry. + +## Cross-Cutting Security And Runtime Layers + +- **YAML safety gate:** `_SDLSafeLoader` remains the only SDL composer. Its + resolver must implement the source profile without mutating PyYAML's global + resolver tables. It rejects unsafe/non-profile tags, duplicate keys, alias + cycles, merge ambiguity, excessive source/scalar size and depth/node/alias + work, non-I-JSON numbers/strings, and other non-profile values before recursive + normalization or model construction. Count alias edges/work, not only unique + composed nodes, because normalization can otherwise amplify a small graph. +- **Shape gates:** the mapping analyzer runs before dictionary construction; + canonical scalar/collection shape is checked before Pydantic; then + `SDLModel(extra="forbid")` rejects unknown structure. Existing field validators + retain range, identifier, redaction, argv, environment-name, and domain-specific + shape checks; `SemanticValidator` retains reference and graph closure. + Canonicalization must bypass none of them, and `parse_float_or_var()` must not + admit `NaN` or infinity through a quoted string. +- **Module and filesystem gates:** local imports remain base-confined; OCI and + locked imports retain trust policy, allowed registry, version, digest, + lockfile, export-hash, signature, extraction, namespace, and cycle checks. + Canonicalization consumes the already validated/expanded model and must not + create an alternate file reader or untrusted network-fetch path. +- **Secret handling:** `enforce_observed_value_redaction()` and the existing + `redacted`/`operator_secret` omission validators still apply after + normalization. SDL may legitimately contain scenario credentials under + ADR-057, so canonical bytes and digests must not be logged, persisted, or + returned as if they were sanitized. Hashing a low-entropy secret is not + redaction. +- **MCP/input limits:** the existing 64 KiB language-service, authoring, + inspection, and operation-tool guards remain adapter limits. The shared YAML + loader also needs one cohesive, explicitly threaded operational-limits policy + for raw size, scalar size, depth, nodes, and alias work so direct library/file + callers are not an unbounded YAML-bomb path. Adapter limits may be stricter. + Limit refusal is an operational diagnostic, not a claim that another + implementation must consider the SDL semantically invalid; do not scatter new + constants or ambient environment switches through adapters. +- **Auth and transport:** the current MCP surface is the existing FastMCP + transport and introduces no new HTTP/control-plane endpoint or authorization + model. This work must not add a bypass around a host's transport auth or put + source text, credentials, or private keys into process arguments. +- **Configuration and OS exposure:** source-format/migration selection is an + explicit API/CLI input, not an ambient environment variable. CLI entrypoints + continue to accept validated file paths and private-key paths, not key values; + no shell command construction, environment dump, or new bind/listen surface is + required. +- **Error envelope:** canonical and migration diagnostics reuse + `SDLParseDiagnostic` and carry codes, stage, severity, canonical pointer, + bounded source locations, and a logical source identity when imports are + involved. Messages may quote field-key spellings but not mapping values, full + YAML, canonical payloads, parameter maps, environment values, secrets, + tracebacks, or unrestricted absolute paths. Language-service and MCP adapters + preserve the same structured record. A migrated model may be semantically + valid while its original source remains non-conforming; do not fold source + migration notices into `SemanticValidator.warnings` or report the raw document + as canonical. +- **Logging and persistence:** `aces_sdl.scenarios` remains the existing + advisory/load logging boundary and must log only bounded code/path/spelling + metadata for migration notices. No new logger, audit stream, database, cache, + lockfile field, or automatic digest persistence belongs in this change. + +## Extension Boundary + +The extension seam belongs at source ingress, not in Pydantic models, +validators, processors, or backends. A named `source_format` value pins +`sdl-yaml/v1`; an orthogonal explicit migration policy decides whether legacy +spellings/merges are rejected or canonicalized with diagnostics. Thread those +values through the existing `_load_normalized_data()` path used by strings, +files, fragments, imports, formatters, and MCP tools. Do not add scattered +`case_insensitive`, `allow_hyphens`, `allow_merge`, or `yaml_11` booleans. +Operational parser limits are a separate cohesive input to that same ingress; +they do not select language meaning and must not leak into Pydantic models. + +A future source-format revision gets a new format identifier and a deliberate +compatibility relation under ADR-075. A future canonicalization algorithm gets +a new `aces-sdl-semantic/vN` identifier. Neither variation requires a second +Scenario model, validator stack, schema registry, or runtime endpoint. + +## Gotchas And Anti-Patterns + +Avoid: + +- relying on PyYAML defaults, Pydantic coercion, or Python's `bool`-is-`int` + behavior to define portable scalar validity; +- dedenting, stripping, `safe_load()`-round-tripping, or otherwise rewriting the + text before source-profile and source-range validation; +- treating `Scenario.model_json_schema()` as a raw YAML grammar or treating a + parse-then-dump example test as raw-source schema validation; +- keeping the six current kebab-case workflow serialization aliases + (`on-success`, `on-failure`, `on-exhausted`, `max-attempts`, + `min-attempts`, `compensate-with`) while declaring snake_case canonical; +- normalizing user identifiers, native option keys, labels, facts, extension + keys, or JSON Pointer tokens as structural fields; +- accepting a canonical root while parsing imported modules or fragments with a + more permissive implicit mode; +- auto-detecting dialects from top-level keys or silently treating an unknown + field as a migration alias; +- publishing migration spellings through Pydantic `Field(alias=...)`; validation + aliases, canonical serialization names, and Python-safe attribute names are + different concerns; +- preserving YAML merge precedence, resolving cycles, or expanding aliases + without resource budgets; +- using `yaml.safe_dump()` output as cross-language identity bytes; it remains a + human formatter, not the canonical digest serialization; +- reusing phase-specific helpers merely because they say "canonical": the + compiled-runtime witness in `test_pipeline_determinism.py`, schema-publication + hashing, and run-artifact JSON formatting do not implement + `aces-sdl-semantic/v1`; +- omitting defaults or private/public fields inconsistently between repeated + canonicalization passes, materializing omitted defaults that carry authored + explicitness, dropping module side channels that affect downstream behavior, + or sorting arrays whose order is semantic; +- feeding arbitrary-precision integers, lone surrogates, `NaN`, or infinity to + JCS and relying on a library to round, replace, or reject them differently; +- producing an authoritative semantic digest from + `skip_semantic_validation=True`, a directly constructed `Scenario`, or a + partially expanded import graph; +- replacing raw module/OCI digests or signature payloads with the semantic + digest, or presenting a digest as authenticity, execution evidence, or + redaction; +- duplicating `parse_sdl()` and `parse_sdl_file()` policy branches. The file + entrypoint already can delegate to the string parser with a path; source-mode + and diagnostic logic must have one implementation; +- adding another full SDL schema, canonical field table, shorthand engine, + exception hierarchy, diagnostic DTO, size-limit constant, migration service, + or schema/change manifest; +- rewriting accepted ADR bodies. Any needed ADR-001 clarification follows the + ADR-059 amendment/pin process while current normative behavior lives in + `specs/sdl/`. + +## Non-Goals + +- Implementing the parser, schema, example, fixture, formatter, diagnostic, or + digest changes in this note. +- Adding or changing SDL domain fields, semantic reference rules, variable + semantics, module resolution, compiler/runtime contracts, or backend behavior. +- Accepting metadata/mode-based scenarios, YAML 1.1, arbitrary tags, merge + precedence, JSON5, TOML, or another scenario dialect. +- Building a general-purpose YAML validator, migration service, source registry, + persistence layer, control-plane API, auth system, network service, or audit + pipeline. +- Defining canonical identity for instantiated scenarios, compiled plans, + runtime snapshots, evidence artifacts, or OCI packages. Those phases retain + their existing authorities and require separate versioned decisions if they + later need semantic digests. +- Treating formatting as source preservation. Comments, anchors, scalar style, + and original field spelling are intentionally not recoverable from the + normalized semantic projection. diff --git a/docs/decisions/issue-722-normative-sdl-catalog-parity-preflight.md b/docs/decisions/issue-722-normative-sdl-catalog-parity-preflight.md new file mode 100644 index 000000000..23f575adf --- /dev/null +++ b/docs/decisions/issue-722-normative-sdl-catalog-parity-preflight.md @@ -0,0 +1,287 @@ +# Issue 722 Normative SDL Catalog Parity Preflight + +Date: 2026-07-11 + +Issue: #722. + +Requirement: none. The issue title, body, and acceptance criteria are the +contract. + +This note records architecture guardrails for reconciling the normative SDL +catalogs with the live authoring contract. It does not edit the catalogs, +schemas, models, validators, examples, or release workflow. No new ADR is +needed: ADR-009, ADR-019, and the issue-498 preflight already fix the authority +direction and catalog boundary; this issue adds enforcement and repairs drift +within that boundary. + +## Binding Authorities And Incumbents + +- `specs/sdl/README.md`, `document-model.md`, `sections.md`, `references.md`, + `runtime-inventory.md`, and `diagnostics.md` are the language-neutral + normative SDL prose authority. +- `contracts/schemas/sdl/sdl-authoring-input-v1.json` is the published, + hand-governed structural enumeration. `contracts/schema-publication-manifest.json`, + `tools/check_schema_publication.py`, and ADR-061 govern any change to it. +- `Scenario.model_fields` and `schema_bundle()` are reference-implementation + evidence. `tools/check_generated_schemas.py` already proves that generated + output matches the published schema without rewriting that authority. +- `_mapping_scopes.HASHMAP_SECTIONS` owns parser treatment of authored map keys; + `_module_symbols.HASHMAP_SECTIONS` owns the narrower module-composable symbol + set. They have different jobs and must not be merged merely to make their + counts equal. +- `SemanticValidator._named_ref_index()`, its targetable filtering, + `aces_sdl.semantics.participant_behavior`, and + `_language_metadata.REFERENCE_COMPLETION_TARGETS` are implementation evidence + for reference validation and authoring-tool navigation. Completion metadata + is not a semantic validator and is not a complete reference registry. +- `_runtime_service_families.RUNTIME_SERVICE_FAMILIES` is the existing + node-runtime family registry. Its key, collection, primary id, and child-ref + tree must be compared with `specs/sdl/runtime-inventory.md`; no second runtime + family registry is warranted. +- Behavior-specific governed domains already exist in + `controlled-vocabularies-v1`, the controlled-vocabulary validation helpers, + and `aces_contracts.manifest_authority`. Do not replace those domains with a + generic SDL-symbol lookup. + +## Authority Direction And Parity Boundary + +The release check must be a read-only, three-way compatibility proof: + +1. parse top-level field names, requiredness, and shapes from the published + authoring schema; +2. parse the normative rows from `specs/sdl/sections.md` and + `specs/sdl/references.md`; and +3. inspect the reference implementation's model and existing registries as + realization evidence. + +All comparisons are bidirectional set comparisons. A schema/model field absent +from the catalog and a catalog field absent from the schema/model are both +failures. The check must never generate or overwrite normative prose or a +published schema, and implementation metadata must never be used to render the +normative tables. That preserves independent implementation authority while +making drift visible. + +The dated preflight inspection for this issue yields 28 top-level fields: five +metadata or composition fields and 23 authoring sections, of which 22 are +map-keyed and one (`forwarding_agents`) is list-valued; it also finds 17 +node-scoped runtime families. Those are issue evidence, not a new maintained +count source. Current-language values may appear durably only in a +checker-validated catalog summary or derived report. Explanatory prose, model +docstrings, schema descriptions, MCP text, and examples should otherwise use +count-free wording. The OCR stress fixture's "14 sections" may remain only +where it is explicitly a fixture-scope claim, not a claim about the live +language. + +## Catalog Row Contract + +Every top-level row must carry enough information to support an independent +implementation without opening Python: + +- kind: metadata/composition or authoring section; +- document phase, using the vocabulary in `document-model.md` section 7 rather + than processor/runtime phase names; +- value shape: scalar, fixed mapping, map keyed by authored identity, or list; +- requiredness and default/omission behavior; +- identity source: scenario name, mapping key, list element id, or none; +- outbound reference edges or an explicit `none`; and +- the normative semantic owner (SDL file, formal spec, and/or owning ADR). + +The phase column must distinguish fields consumed by composition or +instantiation from fields that survive those phases. It must not call compiled +runtime output an SDL document phase. The identity column must distinguish map +keys from in-element ids, especially scenario-level `forwarding_agents`. + +The checker should parse stable table headings and exact columns, following the +bounded Markdown-table pattern in `tools/check_semantic_coverage.py`. Missing +headings, malformed rows, duplicate fields, unknown shape/phase tokens, and a +blank coverage cell are policy failures, not parser tracebacks. Do not add a +parallel YAML catalog or a second full SDL schema to make Markdown easier to +check. + +## Reference-Edge Semantics + +Reference rows need exact canonical source paths, not broad prose such as +"authority refs." Each row must name its candidate domain, resolution phase, +failure behavior, and semantic owner. The catalog must distinguish at least: + +- SDL symbol sets and the generic targetable set; +- aggregate-local symbols such as workflow steps and observation information; +- derived domains such as participant roles; +- governed controlled-vocabulary scopes; +- published contract-id registries; +- qualified runtime-family/child paths; and +- opaque external/profile refs that currently receive only shape validation. + +An `_ref` suffix does not make those domains interchangeable. The catalog is a +coverage and meaning contract; it must not be used to generate semantic +validation or a new exception path. + +For `behavior_specifications`, the live candidate and validation semantics to +record are: + +| Field | Candidate domain and current validation | +|-------|-----------------------------------------| +| `participant_refs` | Keys in `agents`; unresolved variables defer to instantiation; dangling refs are fatal. | +| `participant_role_refs` | Role values of flattened entities actually bound by declared agents, not arbitrary agent keys or every enum value; dangling refs are fatal. | +| `action_contract_refs` | Keys in `action_contracts`; dangling refs are fatal. | +| `observation_boundary_refs` | Keys in `observation_boundaries`; dangling refs are fatal. | +| `outcome_interpretation_rule_refs` | Keys in `outcome_interpretation_rules`; dangling refs are fatal. | +| `authority_scope_refs` | The generic targetable alias index, with exact-one resolution and fatal dangling/ambiguity; it is not Agent `operating_scope` and not unrestricted `any`. | +| `behavior_mode` | Controlled-vocabulary scope `behavior_specifications.behavior_mode`; this is a governed term, not an SDL symbol ref. | +| `ai_offensive_behavior_refs` | Controlled-vocabulary scope `behavior_specifications.ai_offensive_behavior_refs`. | +| `offensive_behavior_refs` | Controlled-vocabulary scope `behavior_specifications.offensive_behavior_refs`. | +| `backend_feature_support_refs` | Union of `participant-runtime-behavior-features` and `participant-runtime-interaction-features`. | +| `evidence_contract_refs` | The published processor/backend/participant contract-id sets from `aces_contracts.manifest_authority`, not every schema-bundle id. | +| `realization_profile_ref` | Optional non-empty opaque profile reference today; the catalog must not claim local resolution that the implementation does not perform. | + +Reference lists also retain the existing model-level non-empty and per-field +uniqueness checks. Every list-valued behavior reference above skips a whole +unresolved `${name}` value during authoring semantic validation and is checked +again after instantiation; scalar `behavior_mode` does not use that path, while +`realization_profile_ref` currently has no semantic resolver. Deferral must be +stated per field according to the actual validator, not asserted generically +for every governed value. + +`REFERENCE_COMPLETION_TARGETS` remains completion/navigation metadata. Every +entry it exposes must have a normative reference row, but the reverse need not +hold because not every governed or opaque domain has editor completions. Its +current `("behavior_specifications", "authority_scope_refs"): "any"` entry is +broader than semantic validation and must be reconciled with the shared +targetable policy rather than documented as true. Do not copy the validator's +targetable exclusions into another unowned list. + +Runtime-family parity is structural rather than numeric: compare every family +key, collection, primary id, and recursively addressable child collection +between `RUNTIME_SERVICE_FAMILIES` and the runtime-family index. A matching +count with a renamed or missing row is still a failure. Scenario-level +`forwarding_agents` remains a top-level list section; node-level +`nodes..runtime.forwarding_agents` remains a runtime family. + +The extensibility seam is the checked row shape, parameterized by contract +target (published schema path, model class, and catalog table heading) and by +reference-domain token. Adding the next section, edge, or runtime family should +add authority/model/registry rows without adding a hard-coded count or a +field-specific branch to the checker. A future authoring-contract version can +reuse the same extractors with a second contract target; it must not silently +mix v1 and v2 rows. + +## Release Gate And Failure Surface + +Use one dedicated catalog-parity checker and wire it once into the canonical +nox verification graph. The natural home is the `contracts` leg beside schema +publication and generated-schema drift because the check compares published +contract structure with normative prose and its implementation. `verify`, the +pre-push hook, and CI then inherit it; do not duplicate the command in GitHub +Actions or release-please workflows. Relevant `specs/sdl/`, SDL model/registry, +schema, checker, and checker-test paths should trigger the contracts leg in the +optimized pre-commit graph. + +The gate must scan the complete live catalogs on every invocation, not only +changed rows. It should reuse `tools.policy.common.PolicyFailure`, deterministic +sorting, JSON rendering, and the existing exception mechanism. Store source +line numbers while parsing so failures and the eventual #541 closure evidence +identify exact catalog rows. Catalog drift is a repository-policy failure; it +must not become `SDLParseError`, `SDLValidationError`, a runtime diagnostic, or +a new SDL exception hierarchy. + +Focused checker tests should cover malformed and duplicate rows, missing and +extra top-level fields, wrong requiredness/shape/identity, map/list confusion, +reference rows with wrong candidate domains, missing runtime families or child +refs, checked-summary drift, and a passing live-repository integration case. +Existing semantic tests remain the evidence for actual reference behavior; the +policy test does not replace them. + +## Cross-Cutting Security And Operational Layers + +- **Authentication/control plane:** none is in scope. The checker performs no + network access, API calls, authorization decisions, or control-plane + mutation. `ControlPlaneSecurityConfig` and HTTP handlers must not be pulled + into this repository-policy concern. +- **File and config shape:** read only fixed repo-relative catalog/schema paths + and canonical package registries. Parse JSON and Markdown as inert data. If a + future CLI makes paths configurable, pass them through + `tools.policy.common.safe_repo_path`; never follow catalog links or imports. +- **Schema/model validation:** retain `SDLModel(extra="forbid")`, source-profile + checks, `SemanticValidator`, controlled-vocabulary validation, and + `tools/check_generated_schemas.py`. The parity gate observes these surfaces; + it does not bypass or duplicate them. +- **Secret handling:** do not inspect scenario values, environment dumps, + credentials, or arbitrary files. Failure messages may name repo paths, + catalog line numbers, field paths, and expected/actual identifiers, but must + not print file bodies, schema payloads, environment values, tracebacks, or + secrets. Existing hygiene, private-key, and gitleaks stages remain the + repository secret gates. +- **Environment and OS exposure:** add no environment binding and no external + command that carries data in process argv. Nox should invoke the checker with + a static script path through the frozen project environment. The checker + writes no files and needs no temporary or persistent state. +- **Errors and observability:** malformed inputs become bounded + `PolicyFailure` records and a non-zero exit. Nox `SessionReporter` is the + canonical stage log; no new logger, telemetry sink, HTTP error envelope, or + raw exception traceback is needed. +- **Persistence:** none. Do not rewrite catalogs, generated schemas, the schema + manifest, caches, or reports as a side effect of checking parity. + +## Stale-Surface Audit Guardrails + +The reconciliation must cover all reader-facing completeness claims, not only +the two normative tables. Current audit targets include: + +- `specs/sdl/README.md` acceptance questions and the stale reconciliation note + in `document-model.md`; +- the current/future contradiction in `observability-and-evidence.md` and the + evidence-syntax limitation in `docs/explain/sdl/limitations.md`; +- the incomplete overview and non-canonical `behavior-specifications` spelling + in `docs/explain/sdl/sections.md`; +- numeric completeness claims in `docs/explain/sdl/index.md`, + `docs/explain/sdl/complex-scenarios.md`, and + `docs/explain/reference/glossary.md`; +- static counts, accepted section names, and example claims in + `aces_mcp.tools.reference`; and +- examples/tests that claim complete-language coverage while exercising only a + historical subset. + +Model and published-schema descriptions are count-free in the current tree; +preserve that property. A documentation count and a validation-pass count are +different concepts and must not be made equal. Likewise, an example can claim +coverage of named constructs it actually asserts, but not "all sections" +without a checked comparison to the catalog. + +## Gotchas And Anti-Patterns + +Avoid: + +- generating normative prose from `Scenario`, or generating implementation + registries from normative Markdown; +- adding a YAML/JSON meta-schema that duplicates the existing catalog and + published SDL schema; +- treating `REFERENCE_COMPLETION_TARGETS` as validation authority or using + `"any"` where the validator uses targetable, role-derived, or governed sets; +- inferring semantics solely from `_ref`/`_refs` naming; +- collapsing controlled-vocabulary refs, contract ids, evidence refs, profile + refs, SDL symbols, and runtime-family refs into one resolver; +- broadening or narrowing live validation merely to make prose match; any true + semantic change needs its own authority and tests; +- equating map-keyed authoring sections with module-exportable symbols; +- counting `imports` as the one list-valued authoring section, or counting the + two forwarding-agent placements as one surface; +- describing `evidence_requirements` as future syntax or as captured evidence; +- adding a second policy command directly to CI/release workflows; or +- hand-editing schema descriptions without the publication-manifest ledger and + generated-bundle parity required by ADR-061 and repo policy. + +## Non-Goals And Implementation Boundary + +- No SDL syntax, model field, schema structure, reference candidate set, + validation severity, compiler output, or runtime behavior changes are + authorized by this issue. +- No validator consolidation, general SDL metamodel, documentation generator, + new runtime-family registry, exception hierarchy, API, database, or + persistence layer is in scope. +- Do not rewrite accepted historical ADR bodies; current catalogs and + explanatory docs carry the reconciliation. +- Do not close or comment on #541 from this work. The implementation should + produce line-addressable repository evidence that a separately authorized + workflow can cite. +- Do not add changelog fragments or edit package versions; release-please owns + those surfaces. diff --git a/docs/decisions/issue-724-instantiated-phase-contract-preflight.md b/docs/decisions/issue-724-instantiated-phase-contract-preflight.md new file mode 100644 index 000000000..be95c7158 --- /dev/null +++ b/docs/decisions/issue-724-instantiated-phase-contract-preflight.md @@ -0,0 +1,175 @@ +# Issue #724 — Instantiated Phase Contract Preflight + +Date: 2026-07-12 + +Issue: #724. + +Requirement: none. The issue title, body, and acceptance criteria are the +contract. + +This is implementation guidance only. It does not change normative prose, +published schemas, or reference behavior. + +## Binding Authority And Incumbents + +- `specs/sdl/document-model.md`, `variables-and-instantiation.md`, + `sections.md`, `references.md`, and `diagnostics.md` own SDL phase semantics. + In particular, the existing four-step binding contract remains: choose, + type-check, constraint-check, substitute and revalidate. +- `contracts/schemas/sdl/` is the normative machine-readable contract under + ADR-009 and ADR-061. `schema_bundle()` is a compatibility proof, not schema + authority; `contracts/schema-publication-manifest.json`, + `tools/check_schema_publication.py`, and + `tools/check_generated_schemas.py` remain mandatory for a schema change. +- `aces_sdl` owns parsing, composition, instantiation, and semantic validation + (ADR-036). Reuse `Scenario`, `ExpandedScenario`, `instantiate_scenario`, + `SemanticValidator`, the token helpers in `aces_sdl._base`, and the existing + SDL exception/diagnostic boundary. Do not move SDL meaning into processor, + runtime, CLI, or MCP code. +- Module trust and resolution remain exclusively in `aces_sdl.module_registry`: + `ResolvedModule`, lock records, path confinement, digest checks, export-hash + checks, signature verification, and bounded OCI extraction. Provenance is a + record of an already verified resolution; it must never reimplement or + weaken that gate. +- Preserve the current narrow planner need represented by + `module_variable_specs`, `module_node_variable_refs`, and + `node_variable_refs`. Those private side channels must become explicit, + portable phase provenance, not a new general backend template system. + +## Phase Contract + +The implementation must make these shapes distinct. A field not listed for a +phase is forbidden, not represented as an empty or null compatibility shell. + +| Form | Public shape and purpose | Allowed authoring machinery | +| --- | --- | --- | +| Normalized authoring | `Scenario`, after `sdl-yaml/v1` decoding, key normalization, shorthand expansion, and structural closure. Local declaration ids only. | `variables`, `module`, and declared `imports` are allowed. | +| Expanded authoring | Internal `ExpandedScenario`, after trusted module resolution and namespace rewrite, before final binding. Generated qualified top-level ids are allowed. | Root variables may remain for final binding; `module` and `imports` have been consumed. Resolution facts travel in typed expansion context, not in rewritten authoring fields. | +| Instantiated | The portable `instantiated-scenario-v1` payload and its concrete model. It contains executable scenario content plus required `instantiation_provenance`. | No `variables`, `imports`, or `module` member at all; no `${name}` token in any string value. | +| Canonical snapshot | A JCS/RFC-8785 digestable envelope for an instantiated artifact: profile id, concrete instantiated scenario, and exactly its instantiation provenance. | None. It is a sealed artifact, not input to parsing, composition, or substitution. | + +`aces-sdl-semantic/v1` remains the canonical identity of the validated, +expanded **authoring** scenario and must continue to reject an instantiated +model. Do not silently redefine it. The instantiated snapshot needs its own +profile identifier and digest function, with a back-reference to that authored +digest. A future envelope change mints a new snapshot profile; it does not +change the meaning of the existing profile. + +## Decision Guardrails + +1. Replace inheritance-as-phase-proof. `InstantiatedScenario` must not inherit + the authoring fields and then try to police them after construction. Factor + shared concrete section fields only as an implementation detail if needed; + keep the two closed public payload shapes separate. The instantiated schema + and model must reject even `variables: {}`, `imports: []`, or `module: null` + through ordinary closed-world structural validation. +2. Require a dedicated, immutable-on-public-API + `instantiation_provenance` object on every instantiated payload, including a + no-variable/no-import scenario. It is the portable replacement for + `_instantiation_parameters`, `_instantiation_profile`, + `_module_variable_specs`, `_module_node_variable_refs`, and + `_node_variable_refs`; no consumer may depend on Python private state. +3. The provenance object must carry only replay-relevant facts: the canonical + authored digest/profile; the selected profile; resolved root bindings with + source (`provided` or `default`); resolved import records in declared order; + and the existing narrow capability-constraint facts needed for + `nodes.os`/`infrastructure.count`. A resolved import record needs its + namespace path, requested source/version/digest, module id/version, resolved + source, manifest/content digest, export hash, signer identity when present, + and its resolved bindings. Use namespace segments, not a dotted string that + consumers must parse. +4. Capability provenance is not a live `Variable` declaration. Model it as + typed constraint facts addressed to the concrete field and parameter identity + (including only the constraint data the planner needs, such as + `allowed_values`). This is the extension seam for another explicitly + justified pre-instantiation capability check. Do not retain a second + `variables` map, reconstruct general template evaluation in the planner, or + expose private composition naming as author input. +5. `module` is packaging metadata and `imports` are authoring instructions; + neither survives in executable scenario content. Their resolved evidence is + retained only under provenance. Absolute `root_file` paths, OCI cache paths, + trust-policy contents, credentials, registry headers, and raw signatures do + not belong in the portable artifact. +6. The public conversion path is `parse_sdl`/`parse_sdl_file` (authoring), then + `instantiate_scenario` (concrete artifact), followed by compiler admission. + The public instantiator must always apply the normal input and post-binding + semantic gates. Any pre-merge module-only helper is private and must never + mint or publish an `InstantiatedScenario`. A direct model payload is only + external artifact admission: it must receive the same structural, + provenance-consistency, token, and semantic validation before compilation. + `model_construct` is an explicitly unsafe Pydantic escape hatch and not a + supported ingestion API. + +## Security And Cross-Cutting Gates + +- **Source and configuration ingress:** retain `load_sdl_yaml`, source limits, + mapping-key checks, `SDLModel(extra="forbid")`, and parser normalization. Do + not make the authoring parser accept generated qualified names or instantiated + payloads. +- **Module supply chain:** build records only from `resolve_import()` results, + after local-path confinement, lock/digest/export-hash/signature checks, and + OCI resource bounds. A provenance record is evidence, not authorization to + bypass the registry or trust policy. +- **Model/schema/semantic admission:** the closed instantiated model, the + published JSON Schema, `SemanticValidator`, and the post-substitution + structural revalidation all remain necessary and non-duplicative: schema + makes the interoperable shape rejectable, model validation enforces + cross-field provenance rules, and semantic validation resolves concrete SDL + references and control flow. +- **Secrets and error envelopes:** parameter values can reach + `SDLInstantiationError` and MCP responses today. Do not render raw values, + Pydantic `str(ValidationError)`, whole parameter mappings, or full scenario + payloads on a binding failure. Reuse the bounded, input-free diagnostic style + already used by parser diagnostics and the existing + `SDLParseError`/`SDLValidationError`/`SDLInstantiationError` hierarchy. The + MCP `sdl_instantiate` and operation pipeline must stop echoing resolved + parameter values. Existing `redacted`/`operator_secret` validators still + prohibit a raw substituted secret from appearing in a portable scenario or + provenance payload. +- **OS, logging, and persistence:** instantiation/provenance capture must add no + subprocess invocation, argv/env binding, log/audit store, or control-plane + persistence. The only permitted remote access remains the existing + `module_registry` OCI resolution path. In particular, do not add a CLI flag or + log line that puts a parameter value in process argv or telemetry. Reuse the + module registry's checkout-independent local `resolved_source` convention; + never persist host paths. +- **Runtime boundary:** compiler and reference-processor entry points may + consume an admitted instantiated artifact, but runtime/backends must receive + only their existing compiled model/plan DTOs. Do not add a backend-facing SDL + variable/provenance channel. + +## Contract, Test, And Workflow Guardrails + +- Keep prose, `InstantiatedScenario`, `schema_bundle()`, the hand-governed + `instantiated-scenario-v1.json`, parser/instantiator APIs, and fixtures in + exact agreement. Update the publication manifest hash and `last_change` when + the schema changes; the current contract is draft, so the required tightening + may remain in the `v1` lineage under ADR-061. +- Add conversion fixtures for flat and composed input, and negative conformance + fixtures for each forbidden authoring field, unresolved token, missing or + inconsistent provenance, and attempted direct-model/compiler admission with + a non-concrete shape. Exercise JSON Schema independently of Pydantic, plus + parser/instantiator/processor conversion and canonical-snapshot determinism. +- Preserve existing fixture discovery and JSON checks under + `contracts/fixtures/sdl/`, `test_instantiated_scenario_schema.py`, + `test_sdl_parser.py`, `test_sdl_validator.py`, + `test_sdl_canonicalization.py`, `test_sdl_module_registry.py`, and processor + planner tests. Run the repository policy, requirement-governance, + schema-publication, generated-schema, and full `verify` graph configured in + `.ground-control.yaml` / `.gc/plan-rules.md`. + +## Non-Goals And Anti-Patterns + +This change does not create an environment-variable binding language, a second +template syntax, a new schema registry, a new exception hierarchy, a generic +provenance database, an import resolver outside `module_registry`, a runtime +API, a backend protocol field, or an audit/logging subsystem. It does not make +an artifact's claimed provenance cryptographic proof that the local process +performed expansion; verification remains at the existing import/trust gate. + +Avoid allowing empty authoring machinery as a compatibility crutch; relying on +private attributes or `isinstance` alone as an admission proof; reusing the +authoring schema for instantiated output; treating an instantiated snapshot as +an authoring canonical digest; copying raw parameter values into diagnostics, +MCP output, logs, or paths; and adding per-field planner special cases instead +of one typed provenance seam. diff --git a/docs/decisions/issue-725-objective-truth-semantics-preflight.md b/docs/decisions/issue-725-objective-truth-semantics-preflight.md new file mode 100644 index 000000000..1105ee9cd --- /dev/null +++ b/docs/decisions/issue-725-objective-truth-semantics-preflight.md @@ -0,0 +1,337 @@ +# Issue #725 — Objective Truth Semantics Preflight + +Date: 2026-07-12 + +Issue: #725. + +Requirement: none. The issue title, body, and acceptance criteria are the +contract. + +This is implementation guidance only. It does not define new SDL syntax, +publish a schema, change runtime behavior, or provide an implementation plan. + +## Problem Boundary + +The current `Condition` has four meanings that must be separated: + +1. `aces_sdl.conditions.Condition` is an executable command-plus-schedule or a + package `Source`. +2. `nodes.*.conditions` turns that template into a node-bound evaluation + resource (`ConditionBinding`). +3. events, agent starting state, workflow predicates, and objective success + treat the same name as an observable state claim. +4. `EvaluationResultContract.passed` reports a backend-produced Boolean without + preserving the proposition, evidence adequacy, or inability to decide it. + +An executable check is not a proposition. Exit status, package identity, +polling cadence, retries, and environment are realization details; none states +what must be true. The implementation must establish backend-neutral truth +before selecting or executing a probe. + +## Semantic Model And Boundaries + +Use one canonical proposition model. Do not create separate condition, +workflow-predicate, objective-predicate, and backend-predicate languages. + +- A **proposition** is a named, declarative, side-effect-free expression over a + governed stable SDL address/property or an evidence-bearing observation. Its + inspectable meaning includes the typed subject/property, predicate family and + operator, expected value or bound, units where applicable, quantification, + and evidence admissibility. It contains no command, package, callback, + credential, query language, polling policy, or backend id. +- An **assertion** is a typed use of a proposition. It adds semantic role + (`precondition`, `invariant`, or `postcondition`), expected polarity, scope, + and governed temporal/evidence context. Keep this a use-site contract over a + proposition, not a second expression language. A negative assertion expects + the proposition to be false; it is not a special probe kind and must not turn + unknown into true. +- A **probe binding** is a capability-checked realization artifact that binds a + proposition to an executable observation mechanism. It carries backend, + implementation/artifact identity, version or digest, bound target, supported + predicate/observation/time capabilities, and realization/evidence + provenance. It does not change proposition meaning. +- A **truth result** is a portable claim about one proposition evaluation. It + carries truth value, proposition and assertion identities, observation and + clock context, binding provenance, evidence refs, and loss/redaction or + limitation disclosure. It is not a score, reward, operation status, or + evaluator lifecycle status. + +The proposition expression must be a closed, discriminated typed union, not a +universal `operator + Any` tuple. Predicate families should bind to governed +property types and add only their meaningful operators. Subjects resolve via +the existing canonical SDL address/reference machinery. Do not admit JSONPath, +JMESPath, arbitrary Python, shell, regex-as-policy, or backend-native field +paths as portable semantics. Quantifiers operate over an explicitly resolved, +finite subject set; thresholds use the owning property's type and unit rather +than untyped strings. + +Declared-state propositions and observed-state propositions are distinct. +Static SDL facts may be decided from an admitted instantiated scenario. +Runtime facts require an observation contract and evidence basis. An authored +`evidence_requirement` remains capture intent, not proof; a truth result must +cite the captured evidence or observation record that actually supports it. + +## Assertion And Composition Semantics + +- Preconditions determine whether the scoped operation/objective is applicable + at its governed start boundary. A false, unknown, or unsupported precondition + is not successful execution and must fail closed; it must not be rewritten as + objective failure without preserving the applicability result. +- Invariants are evaluated over a governed window. Sampling a point does not + prove an interval invariant unless the proposition's observation contract + declares the sampling/coverage rule and the evidence satisfies it. +- Postconditions are evaluated at the governed completion boundary. Command + completion or operation success is not proof of a postcondition. +- Objective success composes assertion results, not probe exit codes. Its + expression must reference declared invariant/postcondition assertions and use + one shared composition implementation also consumed by workflows where the + same truth algebra applies. + +The portable truth domain is exactly `true`, `false`, `unknown`, and +`unsupported` for this issue: + +- `unknown`: the proposition is supported in principle, but available evidence + cannot decide it (missing, stale, partial, conflicting, redacted, lossy beyond + the declared bound, or probe failure). +- `unsupported`: the selected backend/binding cannot realize the required + predicate, observation strength, evidence basis, or governed temporal + guarantee. + +Negation swaps only `true` and `false`; it preserves `unknown` and +`unsupported`. For `all_of`, any `false` is decisive and all `true` yields +`true`; otherwise `unsupported` dominates `unknown`. For `any_of`, any `true` +is decisive and all `false` yields `false`; otherwise `unsupported` dominates +`unknown`. Empty composition is invalid. Unknown or unsupported never counts +as objective success. This is truth composition, not arithmetic scoring. + +Lossy or partial evidence may produce `true`/`false` only when the proposition +declares an admissible loss/uncertainty bound and the evidence proves it stayed +within that bound. Otherwise it produces `unknown`. Capability absence produces +`unsupported`; ordinary absence of an observation from a capable mechanism is +`unknown`, not `false`. Contradictory observations remain `unknown` unless a +governed conflict/aggregation rule decides them. + +## Time And Evidence Guardrails + +Objective `window` references already have one name-level authority: +`aces_sdl.semantics.objectives.analyze_objective_window`. Reuse it for +story/script/event/workflow scope and dependency refresh; do not duplicate its +resolution rules in proposition evaluation. + +Those references do not by themselves define clock time. Any cadence, +duration, freshness, deadline, dwell, or interval claim must use the governed +time concepts established by ADR-022 and the existing typed time carriers: +time domain, clock authority, event/boundary identity, and any disclosed weaker +or unsupported guarantee. Reuse `ParticipantTimeDomain`, participant temporal +contracts/runtime contexts, and `ExperimentClockContextModel` only within +their owning boundaries; do not pretend either is a general SDL clock. If no +governed carrier exists for a desired proposition window, leave that form +unsupported until the time model adds one. `Condition.interval`, backend wall +clock, host monotonic time, evaluator timestamps, and polling retries are never +portable temporal semantics. + +Evidence ownership remains governed by ADR-064/066 and +`specs/sdl/observability-and-evidence.md`: SDL evidence requirements express +capture intent, experiment evidence records carry captured evidence, and +derived measures/evaluator outputs remain separate. Truth-result provenance +may reference these carriers but must not copy raw evidence into diagnostics, +snapshots, audit details, or free-form result `details`. + +## Migration Of Current Conditions + +Migration must be explicit and loss-aware: + +- `Condition.command`, `interval`, `timeout`, `retries`, `start_period`, and + `environment` map only to a legacy command probe binding and its execution + policy. They do not define a proposition. +- `Condition.source` maps only to a package/artifact-backed probe binding. The + existing `Source.name/version` is identity, not integrity or semantic proof; + executable probe artifacts must also pass the applicable ADR-071 reusable + asset trust/integrity policy and record verified provenance. +- A migrator cannot infer expected truth, target property, polarity, threshold, + quantifier, temporal semantics, or evidence adequacy from arbitrary command + text or a package name. It must require an explicit proposition/assertion + mapping or emit a bounded migration error. A compatibility adapter may map + zero/nonzero exit status as a disclosed legacy observation, but may not claim + backend-neutral semantic equivalence. +- Objective `success.conditions`, workflow predicate conditions, event + conditions, and agent `starting_conditions` migrate to assertion/proposition + references according to their owning role. `nodes.*.conditions` migrates to + probe placement/binding. These consumers must not continue sharing one + ambiguous namespace silently. +- Preserve existing identifiers through an explicit migration ledger where the + mapping is one-to-one. Ambiguous, lossy, or unmapped entries fail closed; do + not first-match or silently drop them. Apply the versioning/deprecation rules + in ADR-075 and `specs/evolution/versioning-deprecation-and-migration.md`. + +## Canonical Incumbents To Reuse + +- **Authority and publication:** ADR-009/019/061, `specs/authority/authority-boundary.yaml`, + `contracts/schemas/`, `contracts/schema-publication-manifest.json`, + `schema_bundle()`, `tools/check_schema_publication.py`, and + `tools/check_generated_schemas.py`. Published schemas are the normative + machine-readable authority; generator parity is a compatibility proof. +- **Concept authority:** ADR-012/062, the `observables` concept family, + `scenario-condition` reference model, controlled-vocabulary validation, + semantic profiles, and manifest `concept_bindings`. Evolve the existing + observable reference model; do not publish a competing concept catalog. +- **SDL ingress and phases:** `load_sdl_yaml`, `sdl-yaml/v1` limits and mapping + checks, `SDLModel(extra="forbid")`, canonical identifiers/addresses, + variables, composition, `instantiate_scenario`, instantiated-artifact + admission, and post-instantiation semantic revalidation. +- **Shared semantics:** `SemanticValidator`, the named-reference index, + `analyze_objective_semantics`, `partition_objective_dependencies`, + `analyze_objective_window`, and the existing validator/compiler agreement + pattern. Extend this pure semantic seam; do not add validator-only or + backend-only truth rules. +- **Processor/runtime contracts:** `compile_runtime_model`, `ObjectiveRuntime`, + `ConditionBinding` migration paths, canonical evaluation addresses, + `EvaluationExecutionContract`, evaluation history/state validation, + `Diagnostic`, `OperationStatus`, `OperationReceipt`, and `RuntimeSnapshot`. + Truth results need a typed carrier; they must not be hidden in `spec`, + `metadata`, `details`, tags, or log text. Lifecycle `ready/failed` remains + orthogonal to truth `true/false/unknown/unsupported`. +- **Backend capability and realization:** `EvaluatorCapabilities`, + `BackendManifestV2Model`, `backend_manifest_payload()`, governed capability + scopes, realization-support diagnostics/disclosure, and realization + provenance. Extend the evaluator capability surface with governed predicate, + observation-strength, evidence, and time dimensions rather than adding a + proposition-specific manifest or backend callback registry. +- **Evidence and trust:** `EvidenceRequirement`, experiment capture/evidence + contracts, evidence refs and checksums, redaction/loss disclosure, ADR-071's + reusable-asset trust policy, and existing module/artifact digest/signature + mechanisms where the asset family applies. +- **Errors and observability:** `SDLParseError`, `SDLValidationError`, + `SDLInstantiationError`, collect-all semantic diagnostics, `Diagnostic`, + control-plane audit summaries, and the redacted API exception handler. Do not + introduce a proposition exception hierarchy or logging pipeline. +- **Tests and conformance:** the published valid/invalid fixture corpus, + schema-independent fixture validation, semantic agreement tests, + backend-manifest/conformance helpers, and runtime result-contract tests. + +## Security And Cross-Cutting Layers + +The intended design passes these layers and must satisfy each one: + +1. **Source/parser gate:** propositions and assertions are inert structured + data decoded through `load_sdl_yaml`; parsing, validation, language service, + MCP, canonicalization, and compilation never execute a probe. Source limits, + duplicate-key checks, canonical fields, and safe YAML construction remain in + force. +2. **Closed shape and reference gate:** `SDLModel(extra="forbid")`, published + schemas, portable identifiers, typed addresses, and `SemanticValidator` + reject unknown fields, untyped operands, dangling/ambiguous refs, invalid + operators for a property type, empty compositions, and assertion cycles. +3. **Instantiation/config gate:** existing variable typing, allowed-value + constraints, substitution, unresolved-token rejection, and semantic + revalidation apply. Do not add environment-variable or CLI binding for + expected values, thresholds, commands, or credentials. +4. **Capability/admission gate:** backend manifests must declare support for the + required predicate family, observation/evidence strength, and temporal + guarantee. Planner/runtime admission returns existing structured diagnostics + for unsupported realization; it never approximates silently. +5. **Executable artifact gate:** probe packages/images/scripts are untrusted + realization inputs. Apply existing allowlist, digest, signature, source-size, + and trust-policy mechanisms as applicable. Never treat `Source.name` or a + successful import as proof of safe execution. +6. **OS/process gate:** no secret, bearer token, credential, private key, raw + environment value, or hidden truth may enter command text, process argv, + filenames, diagnostics, or logs. New adapters use fixed argv and no shell + where possible, bounded time/output/resource limits, a controlled working + directory/environment, least privilege, and explicit output redaction. The + legacy command compatibility path must be policy-gated and must not become + the portable probe contract. +7. **Runtime/API gate:** any live truth/probe operation reuses + `ControlPlaneSecurityConfig.strict_defaults()`, verified bearer/proxy + identity, target-bound role authorization, request-size limits, idempotency + fingerprints, audit records, and the existing redacted internal-error + response. A backend-native exception or probe stdout/stderr is never the + public error envelope. +8. **Persistence/evidence gate:** persist typed truth, binding provenance, + digests, clock context, evidence refs, and bounded loss summaries through + versioned contracts. Do not persist raw command output, credentials, host + paths, backend-native objects, complete evidence payloads, or full + tracebacks in snapshots, result `details`, audit records, or fixtures. + +## Extensibility Seam + +The required seam is a pure proposition analysis/evaluation contract +parameterized by: + +- governed predicate family and property type; +- canonical subject resolver; +- quantifier/threshold semantics; +- assertion role and polarity; +- truth algebra/composition operator; +- governed temporal-context resolver; and +- evidence-admissibility and observation-strength policy. + +The compiler lowers the same analyzed proposition into a capability request; +backend adapters supply versioned probe bindings and observations against that +request. Adding one reasonable future predicate family, observation carrier, +or time-domain carrier should add a governed union member, capability term, +binding implementation, fixtures, and tests. It must not require edits to every +objective/workflow consumer or a new per-backend truth implementation. + +## Required Fixture Semantics + +The contract corpus must prove, independently of one backend implementation: + +- positive and negative assertions over the same typed proposition; +- ambiguous/insufficient evidence yielding `unknown` rather than false; +- absent backend capability yielding `unsupported` rather than unknown; +- lossy/redacted evidence staying unknown unless an admitted bound decides it; +- composition and negation over all four truth values; +- two materially different probe bindings producing equivalent truth results + for the same proposition, with distinct binding provenance; and +- a backend that reports `true`/`false` without the required evidence, + capability, clock context, or binding provenance being rejected. + +## Gotchas And Anti-Patterns + +Avoid: + +- renaming `Condition` to `Predicate` while leaving command/source fields in it; +- treating command exit zero, package success, HTTP 2xx, or probe completion as + portable truth without an explicit typed mapping; +- treating missing evidence, timeout, redaction, unsupported capability, and + false as the same value; +- Boolean negation that turns unknown/unsupported into success; +- reusing participant action preconditions as objective assertions: they own + action applicability and support/evidence refs, not general state logic; +- reusing evaluator `passed`, score, reward, derived measure, operation + success, or workflow step outcome as the proposition truth model; +- using objective-window membership, polling interval, timestamps, or host + clocks as an implicit temporal model; +- universal string operators, free-form property paths, opaque constraint + bags, backend query languages, or arbitrary callbacks in SDL; +- letting a backend author or rewrite proposition meaning while binding it; +- duplicating schemas, reference resolvers, truth tables, capability catalogs, + validators, exceptions, diagnostics, audit logs, stores, or conformance + runners; and +- leaking commands, environment values, evidence content, hidden truth, + backend-native ids, host paths, stdout/stderr, or tracebacks through portable + artifacts or errors. + +## Non-Goals And Implementation Boundaries + +- This preflight does not implement #725, choose final serialized field names, + publish a schema/ADR/formal spec, migrate examples, or change runtime output. +- It does not add a general rules engine, policy engine, CEP/stream processor, + temporal logic language, solver, query language, remote probe service, or + plugin system. +- It does not define statistical grading, confidence intervals, reward, + leaderboard scores, derived measures, or experiment analysis. Those remain + in the experiment/evaluator plane. +- It does not collapse world state, participant-visible observation, authored + capture intent, captured evidence, or derived analysis into one truth store. +- It does not make participant action preconditions or workflow step outcomes + aliases for objective assertions. +- It does not authorize arbitrary command execution or make legacy conditions + semantically portable. Legacy compatibility is a disclosed migration aid, + not the target architecture. +- SDL semantics stay in `aces_sdl`, neutral portable DTOs in `aces_contracts`, + compilation/planning in `aces_processor`, live admission/state in + `aces_runtime`, backend declarations in `aces_backend_protocols`, and concrete + probe execution in backend adapters. No implementation logic belongs under + compatibility-only `implementations/python/src/aces/`. diff --git a/docs/decisions/issue-726-sdl-lineage-provenance-ledger-preflight.md b/docs/decisions/issue-726-sdl-lineage-provenance-ledger-preflight.md new file mode 100644 index 000000000..957c3b47a --- /dev/null +++ b/docs/decisions/issue-726-sdl-lineage-provenance-ledger-preflight.md @@ -0,0 +1,276 @@ +# Issue 726 SDL Lineage And Provenance Ledger Preflight + +Date: 2026-07-12 + +Issue: #726. + +Requirement: none. The issue title, body, and acceptance criteria are the +contract. + +This note fixes architecture guardrails for the lineage, derivation, and +third-party provenance remediation. It does not create the ledger, audit or +change a third-party notice, correct citations, add a checker, or change SDL +semantics. + +## Boundary And Authority + +The implementation must keep three records distinct: + +1. **Intellectual lineage** records that a source influenced ACES syntax or + meaning. It is not evidence that source code was copied. +2. **Artifact/code derivation** records copied, translated, or structurally + adapted source artifacts at exact paths and revisions. It is the input to + the copyright/license disposition. +3. **Implementation examples** demonstrate behavior. Similarity in a test, + fixture, or example is not language authority or code-derivation evidence. + +`docs/explain/sdl/lineage.md`, `precedents.md`, and +`related-work-comparison.md` remain explanatory views. The auditable ledger +must be a normative, versioned contract artifact; explanatory tables consume +it and must not carry a parallel classification or source registry. + +The ledger does not belong in `contracts/concept-authority/`. +`concept-families-v1.json` answers what shared concepts mean; it does not answer +whether ACES code was copied, whether syntax is compatible, or whether a notice +is owed. Establish a dedicated `contracts/provenance/` authority root for a +versioned SDL provenance ledger and govern its published schema under +`contracts/schemas/provenance/`. Adding that normative artifact family requires +the ADR-009/ADR-019 authority-boundary process and the corresponding +`specs/authority/authority-boundary.yaml` classification. Do not place the +authoritative ledger under non-normative `docs/`, `research/`, `examples/`, or +`implementations/`. + +## Ledger Contract + +Use one closed, versioned ledger with stable, namespaced subject identities. +The contract must express these concepts without relying on prose conventions: + +- **Source records:** source id and kind; project/standard/publication identity; + exact version, tag, commit, edition, or DOI as appropriate; immutable primary + citation; pinned artifact URL; and reviewed license source. A Git source such + as OCR must record the full commit, not only `v0.21.2`. +- **Subject records:** subject kind plus canonical ACES coordinate. Use existing + catalog ids and published-schema coordinates (`contract_id` plus JSON + Pointer), not Python module/class names or headings in explanatory docs. +- **Claims:** one subject may have multiple non-overlapping claims because its + syntax and semantics may have different sources. Each claim states the + lineage plane (`syntax`, `semantics`, `artifact/code`, or `example`), the + issue-required classification, source refs, exact ACES and source artifact + boundaries, divergence, primary citation, and compatibility status. +- **Third-party disposition:** per source and derived-artifact boundary, record + the reviewed license/copyright material, derivation extent, whether a notice + is required, the resulting repository artifact or a rationale for no notice, + review date, and evidence refs. `unknown` is valid during the audit but must + fail release acceptance for any source with an artifact/code claim. + +The required classification labels mix two dimensions. Preserve the labels but +make the dimension explicit: `adopted syntax`, `adopted semantics`, `adapted`, +and `ACES-native` are lineage claims; `removed` and `planned` are disposition +claims. The schema/checker must reject `planned` or `removed` as the only claim +for a current catalog subject and must reject a current/executed compatibility +claim for a planned subject. Do not copy the concept-authority provenance enum +(`adopted`, `adapted`, `native`) or semantic-coverage statuses +(`active`, `partial`, `planned`) into this contract; those vocabularies answer +different questions. + +Compatibility is also independent of provenance. Record it explicitly and +directionally (for example, ACES acceptance or behavior relative to a named +source revision). `adopted syntax` does not imply parser compatibility, +`adopted semantics` does not imply runtime compatibility, and `adapted` does +not imply incompatibility. + +The phrase **direct port** is prohibited in current reader-facing prose unless +the same statement explicitly names the plane, exact source revision and +artifact boundary, and compatibility status. Prefer the ledger's precise +classification. Code comments such as the current `aces_sdl.nodes` module +header must be reconciled with the artifact/code audit rather than treated as +proof of copying. + +## Coverage And Canonical Incumbents + +Coverage must be derived from existing authorities, never from the current +`precedents.md` rows or from a new hand-maintained family list: + +- top-level fields and authoring sections: + `specs/sdl/sections.md`, checked against + `contracts/schemas/sdl/sdl-authoring-input-v1.json` and `Scenario.model_fields` + by `tools/check_sdl_catalog_parity.py`; +- node runtime families: `specs/sdl/runtime-inventory.md`, checked against + `_runtime_service_families.RUNTIME_SERVICE_FAMILIES` by the same parity gate; +- shared concept families and recurrent normative structures: + `contracts/concept-authority/concept-families-v1.json` and + `reference-models-v1.json`, validated by their existing closed-world contract + models and concept-authority governance gate; and +- published contract identity and schema coordinates: + `contracts/schema-publication-manifest.json` and the published schemas. + +The ledger checker must compare namespaced sets bidirectionally: every current +canonical subject has a ledger record, every current ledger subject resolves to +one canonical subject, and tombstoned/planned records are explicitly outside +the current set. A matching count is not coverage. The subject namespace must +distinguish a top-level section, a runtime family, a concept family, and a +reference model even when they share a word such as `relationships`. + +Field-level adoption or porting claims must use exact schema pointers and exact +source paths/symbols. A family-level claim may describe a bounded family, but it +must not be presented as proof that every child field was adopted. Do not infer +field origin from the containing Python file, class inheritance, a shared field +name, or repetitive prose. The ledger shape must allow another field pointer to +be added without a schema change. + +Reuse these repository mechanisms: + +- authority: ADR-009, ADR-019, + `specs/authority/authority-boundary.yaml`, and + `tools/check_authority_boundary.py`; +- contract shape: `ContractModel(extra="forbid")`, existing constrained string + aliases and model validators, `aces_contracts.versions`, `schema_bundle()`, + and the valid/invalid fixture convention; +- schema publication: ADR-061, + `contracts/schema-publication-manifest.json`, + `tools/check_schema_publication.py`, and + `tools/check_generated_schemas.py` (published schema is authority; generated + output is compatibility evidence); +- artifact validation: `tools/check_json_artifacts.py`, `check-jsonschema`, and + the existing catalog-derived validation pattern used by UCO alignment; +- policy failures and paths: `tools.policy.common.PolicyFailure`, + `safe_repo_path`, deterministic sorting/JSON rendering, and the shared + exception mechanism; +- workflow: the single `noxfile.py` policy/contracts graph and + `SessionReporter`; `verify`, hooks, and CI inherit that wiring; and +- documentation/citation stance: + `docs/explain/reference/documentation-style-guide.md` and the source-log + discipline under `docs/research/related-work-comparison/`. + +The lineage checker is a repository contract/policy checker. Its failures must +not become `SDLParseError`, `SDLValidationError`, runtime `Diagnostic`, a +control-plane DTO, or a new exception hierarchy. It observes the SDL contract; +it does not validate authored scenario instances. + +## Citation And License Audit Guardrails + +External identity must be verified against primary sources during the audit. +Record title, authors/maintaining body, year/edition, DOI or canonical document +identifier, and immutable URL as separate fields so a correct DOI cannot be +paired with the wrong title/year. The known CRACK row that labels the 2020 +*Computers & Security* DOI as the 2018 IEEE NCA paper must be corrected as two +distinct works. A DOI resolver result is verification evidence, not a source of +ACES semantics. + +CI must remain offline and deterministic. It can validate identifier syntax, +duplicate/conflicting bibliographic identities, internal references, pins, and +recorded verification metadata; it must not fetch GitHub, DOI resolvers, or +documentation sites on every run. Dead-link and citation review is a bounded +implementation-time audit whose evidence is committed in the ledger/source +notes. + +License disposition must inspect the license and copyright state at each exact +source revision and the actual ACES artifact/code boundary. Repository license +detection, a project homepage badge, or a current default-branch license is not +sufficient evidence for an older commit. Do not automatically conclude that a +notice is unnecessary because the implementation is a rewrite, uses different +names, or is small. Conversely, intellectual influence alone is not copied +code. Where the audit cannot reach a supportable disposition, record the +blocker and obtain qualified legal review; the checker must never manufacture a +legal conclusion. + +If a notice is required, keep the verbatim notice in the repository's +distribution-visible notice surface and point to it from the ledger. Do not +duplicate notice text in every lineage row. The wheel/sdist packages the entire +`contracts/` corpus through `implementations/python/hatch_build.py`, so the +implementation must explicitly decide whether the legal notice itself also +needs inclusion outside the corpus in built distributions; do not assume that +shipping the audit record satisfies a license's notice condition. + +## Security And Operational Layers + +The design passes these cross-cutting layers: + +1. **Authentication/control plane:** none. No HTTP endpoint, bearer identity, + role policy, backend, or Ground Control data path is part of the ledger. +2. **File/config shape:** parse only fixed repo-relative JSON/Markdown/schema + paths as inert data. Closed models and published JSON Schema reject unknown + fields and invalid unions. Any ledger-provided path passes `safe_repo_path`; + no symlink escape, absolute path, `..`, dynamic import, or evaluated content + is permitted. +3. **Source and semantic validators:** the checker consumes the existing SDL + catalog parity, concept-family, reference-model, runtime-family, and schema + publication authorities. It must not weaken or duplicate `SDLModel`, + `SemanticValidator`, controlled-vocabulary validation, or generated-schema + parity. +4. **Secret handling:** only public citations, repository coordinates, and + public source metadata belong in the ledger. Do not inspect environment + dumps, credentials, private source URLs, local Git credential helpers, or + `/home/atomik/.secrets`; do not place tokens or private URLs in source + records, fixtures, failures, or docs. +5. **Environment/OS exposure:** add no environment binding, daemon, network + requirement, cache, database, or mutable state. Checked-in validation uses + fixed argv with no shell and carries no source metadata or credentials in + process argv. One-time authenticated research, if unavoidable, stays outside + committed command logs and CI. +6. **Error envelope and observability:** malformed ledgers produce bounded + `PolicyFailure` records with rule id, repo path, subject id, and line/pointer + where available. Do not print file bodies, notice bodies, environment + values, raw external responses, tracebacks, or credentials. Nox + `SessionReporter` is the only new stage-level observability needed. +7. **Persistence/distribution:** validation is read-only. It writes no repaired + ledger, fetched source, report, or cache. The checked-in contract is the + durable record; package inclusion continues through the canonical corpus + build seam. + +## Extensibility Seam + +The seam is a namespaced subject resolver plus a source-kind-discriminated pin, +not a hard-coded OCR table. Parameterize the checker by ledger path/schema, +current authoring contract id, and catalog extractors. A later SDL contract +version, runtime family, reference model, external standard edition, or +additional source revision should add a subject/source/claim record without +changing the ledger schema or adding a field-specific checker branch. + +Source pins must be discriminated: Git sources require repository + full commit ++ path/symbol; standards require maintaining body + edition/version + canonical +document id; publications require bibliographic identity + DOI/immutable URL; +ACES-native claims require internal authority refs and no invented external +source. Do not force all sources into a nullable “version or commit” bag. + +## Gotchas And Anti-Patterns + +Avoid: + +- making `precedents.md`, a spreadsheet, a README table, Python comments, or a + research source log the authoritative ledger; +- placing derivation/license facts in the concept-family catalog or treating + UCO alignment as SDL code provenance; +- one classification per family when syntax and semantics have different + sources; +- using `planned` as evidence of current implementation, or hiding removed + items by deleting their tombstones; +- claiming field-level coverage from a family label or matching counts; +- keying subjects by Python classes, filenames, prose headings, or display + titles instead of stable catalog ids/schema coordinates; +- unpinned default-branch links, short commits, bare version strings, or DOI-only + citations with no verified identity; +- treating semantic influence, translation, code copying, fixture similarity, + and wire compatibility as synonyms; +- live network validation in policy/CI, vendoring whole upstream repositories, + or committing long third-party excerpts as audit evidence; +- adding a second schema registry, catalog parser, corpus loader, policy result + type, exception hierarchy, logger, workflow command, or CI-only check; and +- rewriting accepted ADR bodies outside ADR-059's amendment process. + +## Non-Goals And Implementation Boundary + +- No SDL syntax, semantic model, parser, validator, compiler, runtime, API, + persistence, logging, schema compatibility, or package behavior changes are + authorized by this issue. +- The ledger documents provenance; it does not promise source compatibility, + conformance, clean-room status, or legal clearance beyond the recorded + evidence and disposition. +- Dependency SBOM/license scanning is not a substitute for the source-derivation + audit and is not added by this issue. +- Existing concept-family provenance, UCO alignment, semantic coverage, + experiment provenance, and phase-derivation contracts remain distinct and + are not migrated into the SDL provenance ledger. +- Historical accepted ADR prose is not normalized for terminology. Correct + current explanatory docs, comments, citations, and machine-readable records; + amend an accepted ADR only when its current normative decision is affected. diff --git a/docs/decisions/issue-727-scientific-scenario-completeness-profile-preflight.md b/docs/decisions/issue-727-scientific-scenario-completeness-profile-preflight.md new file mode 100644 index 000000000..7e7233188 --- /dev/null +++ b/docs/decisions/issue-727-scientific-scenario-completeness-profile-preflight.md @@ -0,0 +1,277 @@ +# Issue 727 Scientific-Scenario Completeness Profile Preflight + +Date: 2026-07-12 + +Issue: #727. + +Requirement: GOV-942. The requirement plus the issue title, body, and +acceptance criteria are the contract. + +This note fixes the architecture guardrails for a versioned REV1 scenario +completeness matrix. It does not publish the matrix, add a profile schema or +loader, change SDL validity, declare any profile complete, or add examples. +No new ADR is needed: ADR-009, ADR-016, ADR-019, ADR-055, ADR-061, ADR-066, +ADR-068, ADR-070, ADR-072, ADR-073, ADR-074, and ADR-079 already fix the +authority and concept boundaries this issue must compose. + +## Decision Boundary + +REV1 answers one question: **which semantic surfaces must be present, may stay +underspecified, or are excluded for a stated scenario-use claim, and what is +the repository's current delivery status for each required surface?** It is a +scope and claim contract. It is not another SDL grammar, validator-strength +record, backend feature manifest, experiment artifact, or runtime admission +decision. + +The five profiles form an intended-use progression, not five subclasses of +`Scenario` and not an automatic proof ordering: + +1. valid SDL fragment; +2. deployable scenario intent; +3. participant-evaluation scenario; +4. controlled experiment scenario; and +5. reproducible benchmark/study input. + +Structural SDL validity remains permissive. `name` is the only required +top-level authoring field; a fragment that passes source, closed-model, and +semantic checks does not thereby satisfy any stronger completeness profile. +Conversely, stronger profiles compose existing SDL and experiment contracts; +they must not make experiment tasks, runs, studies, evidence, or apparatus +context new SDL top-level sections. + +## Canonical Matrix Model + +Use one canonical, machine-readable, revision-labelled matrix with stable +profile ids and stable, atomic concern ids. Reader-facing prose or tables must +be generated from, or mechanically checked against, that authority. A concern +row must not combine independently deliverable facts merely because the issue +lists them together. For example, parameter typing, factor declaration, +allocation/randomization, and seed preservation need separate rows when their +statuses differ. + +Keep these two dimensions separate: + +- **profile disposition**: `required`, `allowed-underspecified`, or + `excluded`; and +- **delivery status**: exactly `implemented`, `partial`, `external-contract`, + `deliberately-excluded`, or `missing`. + +Each concern row must also name its semantic owner, evidence paths, limitation, +and issue/requirement reference where applicable. `external-contract` is valid +only when a named, versioned non-SDL contract supplies the concern and the +profile states the binding obligation. A link to an open issue, proposed ADR, +formal design, or prose aspiration is not implementation evidence. + +A profile may be labelled complete only when every `required` row is either +`implemented` or a satisfiable `external-contract` with a named contract id +and version, binding location, executable validation/evidence path, and no +unresolved integration gap. `partial` and `missing` always block completeness. +`deliberately-excluded` can satisfy only an `excluded` row, never a required +one. Omission is not a status. Profile completeness must be computed from rows, +not hand-authored as an independent boolean that can drift. + +REV1 is a revision of the completeness taxonomy, not the SDL source version, +semantic canonicalization profile, software release, or current delivery +snapshot. Preserve a separate, explicit delivery-assessment revision (and +assessed repository release/commit or date) so an implementation status change +does not silently mutate the meaning of REV1. Completeness is always computed +for `(profile revision, delivery assessment revision)`. The extensibility seam +is `(profile_family, profile_revision, profile_id, concern_id)` joined to a +versioned delivery assessment by `concern_id`, with dispositions parameterized +by profile id. A later concern should add an atomic row; a later profile should +add a disposition record; a delivery change should publish a new assessment; +and a semantic change to existing profile or concern meanings should publish a +new profile revision rather than silently edit REV1. Do not bake five profiles +into five validators or five model classes, and do not repeat one concern's +delivery status in every profile cell. + +Normative prose belongs under `specs/`. Publish the canonical closed matrix +under the existing `contracts/profiles/` root and validate it through the +existing contract, fixture, schema-publication, and corpus-packaging machinery. +That root is currently described in parts of the authority stack as +"capability profile declarations" even though it also contains semantic +profiles. The implementation must update the canonical authority description +and its drift fixtures once to cover governed versioned profile declarations, +while keeping backend, semantic, validation, and scientific-completeness +profile families explicitly distinct by subdirectory, schema, ids, and prose. +Do not smuggle this matrix in as a backend capability profile and do not create +a second profile root. Do not introduce a machine-readable duplicate merely to +render a Markdown table; derive or check reader-facing views against the one +authority. Examples remain non-normative under `examples/`, while valid/invalid +contract fixtures belong under `contracts/fixtures/`. + +## Concern Ownership And Reuse + +The matrix must disposition every issue concern against these incumbents. A +row may cite several incumbents, but it must not copy their schemas or redefine +their terms. + +| Concern | Canonical incumbent and boundary | +| --- | --- | +| Structural fragment validity | `specs/sdl/document-model.md`, `sections.md`, `diagnostics.md`, `sdl-yaml/v1`, `contracts/schemas/sdl/sdl-authoring-input-v1.json`, `load_sdl_yaml`/`parse_sdl`, `SDLModel(extra="forbid")`, and `SemanticValidator`. This is the floor, not completeness. | +| Authored versus observed state | ADR-033 and ADR-066; `specs/sdl/observability-and-evidence.md`; authored SDL, runtime snapshots/results/history, evidence records, and derived measures remain distinct planes. | +| Scoped specificity and open-world intent | ADR-070; `specs/formal/realization/explicitness-and-realization.md`; `aces_sdl.explicitness`, `CompiledRealizationRequirement`, realization envelopes, provenance, and disclosures. Exact/constrained/open is not inferred from field omission ad hoc. | +| Parameters, factors, controlled variation, randomization, and seeds | SDL variables/`instantiate_scenario()` own scenario substitution. ADR-055/068/074 and the existing `experiment-authoring-input-v1`, task, run, study, apparatus, parameter, stochastic-control, factor, allocation, replication, stopping-rule, and cross-artifact validators own experimental design and archival realization. Do not duplicate these in `Scenario`. | +| Propositions and assertions | ADR-079; `specs/formal/objectives/proposition-and-assertion-semantics.md`; the existing proposition/assertion/condition models, reference catalog, shared semantic analyzers, truth domain, evidence basis, and evaluator-capability seams. Probe success is not proposition truth. | +| Cleanup and rollback | Workflow compensation semantics under ADR-006 and the formal workflow state machine own authored compensation. Backend teardown/reconciliation owns resource cleanup. Manual/advanced rollback remains an explicitly documented gap in `docs/explain/sdl/limitations.md`; do not merge these meanings into one boolean. | +| Time domains, clocks, ordering, deadlines, and pacing | ADR-022 participant time semantics, formal participant concurrency/time contracts, objective-window analysis, experiment clock context, and run timestamps are bounded incumbents. `docs/explain/sdl/limitations.md` and the related-work comparison state that the full SDL time/clock authoring model is incomplete. A timeout or timestamp alone must not satisfy this row family. | +| Episodes, reset, budgets, trajectories, hidden assets, verifiers, and rewards | ADR-013/022/054/067/069/073, participant episode/runtime contracts, participant histories, observation boundaries, outcome interpretation, experiment evidence/results, and CAGE-2 replication artifacts own the implemented portions. Graded reward/scoring is deliberately outside authored SDL; hidden truth and benchmark assets require view-boundary and experiment provenance, not free-form scenario metadata. Split these into atomic rows because delivery status differs. | +| Credential-bearing setup/provisioning | ADR-056/057 and the existing account/runtime-value classification distinguish intentional scenario credentials from operator secrets. Provisioning plans, apparatus context, participant/backend manifests, and realization provenance own setup. Raw operator credentials, tokens, keys, and environment dumps are never completeness evidence or portable profile content. | +| Host architecture and substrate constraints | ADR-055/063/070, experiment apparatus constraints/context, backend manifests and capability profiles, realization envelopes, compiled planning requirements, and realized-form disclosure. Do not encode Docker/libvirt/host-native choices as universal SDL semantics. | +| Vulnerability and weakness semantics | The existing `vulnerabilities` SDL section, concept-authority stack, controlled vocabularies/reference models, typed relationships, propositions/assertions, and evidence semantics. A vulnerability inventory entry is not proof of exploitability, reachability, or observed weakness. | +| Flexible step tooling | Existing scripts/workflows, action contracts, behavior specifications, participant implementation manifests, reusable artifact trust/integrity policy, and backend realization profiles. Tool names or ATT&CK labels are not portable behavior; arbitrary shell/argv must not become profile meaning. | + +Status evidence must be taken from the current normative artifact plus an +executable surface where the claim requires behavior. Accepted ADRs and formal +specs establish meaning, not delivery by themselves. Appropriate evidence is +the published schema/fixture corpus, shared semantic validator/compiler/planner +agreement, contract validators, conformance results, runtime/evidence tests, or +an explicit external-contract binding. Research notes and roadmap issue state +may explain a gap but cannot upgrade its status. + +## Existing Cross-Cutting Contracts + +The implementation must build on these repository-wide incumbents: + +- **Authority and publication:** ADR-009/019/061, + `specs/authority/authority-boundary.yaml`, `contracts/schemas/`, + `contracts/schema-publication-manifest.json`, `schema_bundle()`, + `tools/check_schema_publication.py`, `tools/check_generated_schemas.py`, + `tools/check_json_artifacts.py`, `tools/check_authority_boundary.py`, and the + existing `aces_contracts.corpus` packaged/source-checkout resolution seam. +- **SDL source and semantics:** `sdl-yaml/v1`, safe YAML ingress, source limits, + duplicate-key and mapping-shape checks, `SDLModel(extra="forbid")`, + `SemanticValidator`, `instantiate_scenario()`, canonical identifiers, + `specs/sdl/sections.md`, and `specs/sdl/references.md`. +- **Experiment and evidence:** ADR-055/064/065/066/068/074; the existing task, + authoring-input, run, study, apparatus, capture, evidence, derived-measure, + traceability, realized-form, and augmentation contracts and their + cross-artifact validators. +- **Capabilities and realization:** processor/backend/participant manifests, + `manifest_authority`, backend profiles, controlled vocabularies, semantic + profiles, realization envelopes/disclosures, and conformance runners. +- **Diagnostics and errors:** `SDLParseError`, `SDLValidationError`, + `SDLInstantiationError`, collect-all semantic diagnostics, `Diagnostic`, + `Severity`, operation envelopes, and `tools.policy.common.PolicyFailure` for + repository checks. No completeness-specific exception hierarchy is needed. +- **Persistence and observation:** `RuntimeSnapshot` and `ControlPlaneStore` + own live state; versioned experiment contracts own archival records; evidence + refs/digests and bounded disclosures replace raw payload copies. The matrix + itself is checked-in static authority, not a database or runtime cache. +- **Workflow and verification:** `.ground-control.yaml`, `.gc/plan-rules.md`, + ADR-014, `noxfile.py`, `tools/verify_all.py`, repo-policy and requirement- + governance checks, JSON/schema checks, Sphinx docs, and `SessionReporter`. + The matrix is drift-prone by definition, so its semantic checker should emit + `PolicyFailure` and be wired once into the canonical nox graph rather than + duplicating CI logic. Reuse `check_json_artifacts.py` for schema validation; + the focused checker owns only cross-row completeness and evidence/path + invariants that JSON Schema cannot express. + +Minimal examples must be validated by the same production parser/contracts and +must declare the exact completeness profile id and revision outside scenario +meaning unless a governed carrier already owns that field. Reuse existing +examples where they genuinely meet every required row, but do not relabel a +large showcase as minimal or complete. A stronger example should compose the +same scenario/snapshot with task, experiment-authoring input, apparatus, run, +study, and evidence artifacts as required; it must not inline those contracts +into SDL. Incomplete profiles may have gap illustrations, but those do not +satisfy the acceptance criterion for an implemented profile. + +## Security And Operational Layers + +The intended issue is a static normative profile and examples, so several +runtime layers are deliberately not traversed. The boundary must remain +explicit: + +1. **Repository file/config gate:** read only fixed repo-relative normative, + contract, and example paths as inert data. Resolve packaged/source corpus + defaults through `aces_contracts.corpus`; validate any caller-controlled + profile id before path construction using the closed id grammar established + by `backend_profiles`, and confine checker-provided artifact/evidence paths + with `tools.policy.common.safe_repo_path`. No absolute paths, `..`, symlink + escape, dynamic import, evaluated YAML, or network fetch. +2. **SDL source/shape gate:** example SDL passes `sdl-yaml/v1` source limits, + safe loading, duplicate-key checks, `SDLModel(extra="forbid")`, published + schema parity, semantic validation, and post-instantiation revalidation. + Completeness checking observes these results and does not replace them. +3. **Contract/config-shape gate:** any machine-readable profile or composed + experiment example uses closed `ContractModel` shapes, published schemas, + `x-aces-invariants`, fixture validation, cross-artifact validators, and the + schema-publication manifest. No untyped metadata or parallel DTO/model is + acceptable. +4. **Authentication/authorization gate:** no auth or control-plane endpoint is + in scope. If a future service exposes completeness evaluation, it must reuse + `ControlPlaneSecurityConfig.strict_defaults()`, verified identity, + target-bound role authorization, request-size limits, idempotency, + fingerprints, and audit events; this issue must not add a weaker endpoint. +5. **Secret-handling gate:** profiles, matrix rows, examples, fixtures, + diagnostics, logs, and docs may contain only safe identifiers, refs, + classifications, digests, and bounded summaries. They must not contain + operator credentials, bearer tokens, private keys, hidden answer material, + raw prompts, environment dumps, raw backend objects, or raw evidence. + Intentional scenario credentials remain governed by ADR-056/057 and must not + be copied into the matrix or public evidence. +6. **Environment and OS/process gate:** add no environment binding, daemon, + database, mutable cache, shell evaluation, or external command carrying + profile payloads/secrets in argv. Repository checks use fixed argv in the + existing nox environment and write no generated authority. +7. **Error-envelope and observability gate:** repository drift produces bounded + `PolicyFailure` records with profile/concern ids and repo locations. SDL and + contract example failures retain their existing error/diagnostic surfaces. + Do not print rejected payload bodies, raw values, tracebacks, or backend + exceptions. `SessionReporter` is sufficient; no new logger or telemetry + stack is warranted. +8. **Persistence/distribution gate:** the checked-in normative artifact is the + durable matrix. Examples/fixtures are packaged only through the canonical + corpus/build paths. Do not persist profile claims in runtime snapshot + metadata, audit blobs, tags, logs, or a new repository/service. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating `name`-only structural validity as deployability or scientific + adequacy; +- treating a formal design, accepted ADR, open issue, schema property, or + example mention as implemented behavior; +- one status cell that mixes profile applicability with delivery status; +- broad rows whose implemented sub-concern hides a missing sub-concern; +- calling a profile complete while any required row is partial or missing, or + while an external contract has no named binding; +- conflating completeness with ASR-511/515 validation strength, GOV-920 + semantic profiles, backend profiles, instantiation profiles, realization + support, participant feature support, or action admission; +- duplicating SDL fields inside task/run/study contracts or duplicating + experiment fields inside `Scenario`; +- a second section/reference registry, schema registry, profile loader, + validator stack, exception hierarchy, persistence store, logger, audit path, + or CI workflow; +- deriving completeness from backend support, successful deployment, a + private `_semantic_validated` flag, fixture size, tags, or free-form prose; +- treating authored evidence requirements as captured evidence, runtime state + as authored intent, task as scenario, run as episode/workflow operation, or + reward/verifier output as an SDL objective; +- treating timestamps as clocks, timeout as pacing, retries as controlled + randomization, repeated operations as replications, or a seed field as proof + of reproducibility; and +- putting secrets, hidden truth, raw argv, backend-native paths, or evidence + bodies into examples or claim disclosures. + +## Non-Goals And Implementation Boundaries + +- Do not make stronger completeness profiles mandatory for ordinary SDL parse + or semantic validation. +- Do not change the SDL grammar, source profile, schemas, models, validators, + compiler, planner, runtime, backend behavior, or experiment contracts merely + to publish REV1. +- Do not implement missing roadmap concerns, replay execution, range + provisioning, reset, scoring/reward engines, verifier execution, or a full + time/clock model in this issue. +- Do not certify a backend, apparatus, experiment result, academic claim, or + benchmark quality. REV1 states required surfaces and gaps; validation-basis + disclosures and evidence support concrete claims. +- Do not create a profile-evaluation API, database, UI, registry service, or + automatic migration system. +- Do not promise that an artifact is deployable or reproducible on every + backend. Host/substrate requirements and unsupported guarantees remain + explicit apparatus/capability/realization disclosures. diff --git a/docs/decisions/issue-73-dsl-115-specificity-preflight.md b/docs/decisions/issue-73-dsl-115-specificity-preflight.md new file mode 100644 index 000000000..15376c730 --- /dev/null +++ b/docs/decisions/issue-73-dsl-115-specificity-preflight.md @@ -0,0 +1,153 @@ +# Issue 73 / DSL-115 Specificity Preflight + +Date: 2026-07-08 + +Requirement DSL-115 asks for author-selectable specificity across scenario, +participant, evaluation, and experiment concerns. This note is architecture +preflight only. It does not define new SDL syntax, schemas, validators, +runtime behavior, conformance behavior, or an implementation plan. + +## Architecture Decisions + +- Treat DSL-115 as a specificity contract over existing concern-owning + surfaces, not as a new global abstraction level. +- Reuse the existing exact/constrained/open vocabulary from SEM-218 + (`aces_sdl.explicitness`) for authored intent. If richer machine-checkable + domains are needed, align with ADR-070 and the realization envelope domain + model instead of creating a second constraint language. +- Open or underspecified forms are allowed only where the owning SDL, + contract, or semantic rule explicitly admits them. Silence remains + fail-closed. +- Scenario specificity belongs in the SDL scenario model, variables, + references, instantiation, explicitness metadata, and realization-envelope + semantics. Do not move scenario meaning into experiment-core records. +- Participant specificity belongs in the participant surfaces that already own + it: `Agent`, participant action contracts, observation boundaries, behavior + specifications, and participant runtime contracts. +- Evaluation specificity must respect ADR-073: SDL objectives express + observable success through `conditions`; graded scoring, reward, derived + measures, and evaluator outputs belong in the experiment/evaluator plane. +- Experiment specificity belongs in experiment-core task, apparatus context, + run, study, capture, evidence, and derived-measure contracts. Do not add + tasks, runs, studies, or evaluator scoring records to SDL. +- Constraints that affect semantics must be machine-checkable: typed + variables, `allowed_values`, governed references, domain descriptors, and + semantic invariants. Prose-only constraints are explanatory, not normative. +- Preserve authored specificity provenance through instantiation, compilation, + planning, runtime disclosure, and persisted snapshots. Substituting a + variable must not falsely promote a constrained authored value to exact. +- Published schema changes must follow the contract publication path: + `contracts/schemas/**`, `contracts/schema-publication-manifest.json`, + generated-schema parity, and `schema_bundle()` compatibility. + +## Canonical Incumbents + +- Authority chain: ADR-009, ADR-061, `contracts/README.md`, + `.gc/plan-rules.md`, `tools/check_repo_policy.py`, + `tools/check_schema_publication.py`, `tools/check_generated_schemas.py`, and + `tools/verify_all.py`. +- SDL parsing and model shape: `aces_sdl.parser.parse_sdl`, + `aces_sdl.parser.parse_sdl_file`, `yaml.safe_load`, hashmap key + preservation, variable-key rejection, and `SDLModel(extra="forbid")`. +- SDL semantic validation: `SemanticValidator`, `SDLParseError`, + `SDLValidationError`, `SDLInstantiationError`, SDL diagnostics, references, + variables, and instantiation specs. +- Specificity and realization: `specs/formal/realization/explicitness-and-realization.md`, + ADR-070, `specs/formal/realization/envelope-semantics.md`, + `aces_sdl.explicitness`, `RealizationEnvelopeModel`, + `aces_sdl.realization_envelope`, `CompiledRealizationRequirement`, + `realization_support_diagnostics`, `realization_disclosure`, and + `RuntimeSnapshot.realization_provenance`. +- Participant semantics: `Agent`, `ParticipantActionContract`, + `ParticipantObservationBoundary`, `ParticipantBehaviorSpecification`, + participant behavior validators, and participant runtime contracts. +- Evaluation and experiment contracts: ADR-055, ADR-064, ADR-068, ADR-073, + experiment task, apparatus context, run, study, capture spec, evidence + record, and derived-measure contracts. +- Runtime and API cross-cutting behavior: `Diagnostic`, `OperationStatus`, + `OperationReceipt`, `ControlPlaneSecurityConfig`, role authorization, + request-size guards, idempotency keys, request fingerprints, audit records, + and the redacted control-plane error handler. +- Conformance: `run_target_conformance(reference_scenario=...)` as the + current #663 bridge, backend manifest validation, profile loading, and + `schema_bundle()`. + +## Cross-Cutting Layers + +- Parse/model gate: continue through `yaml.safe_load`, normalized SDL models, + closed Pydantic models, hashmap key preservation, variable-key rejection, and + removed scoring-section rejection. +- Reference/semantic gate: preserve fail-closed reference resolution, collect + all semantic diagnostics, and report paths and concern kinds rather than raw + payload values. +- Instantiation/config gate: use the existing variable declaration, type, + default, `allowed_values`, substitution, unresolved-token, and revalidation + pipeline. Do not introduce a new env-binding path for specificity. +- Contract/schema gate: use `ContractModel`, published schemas, schema + manifests, generated bundles, and `x-aces-invariants` for semantic rules + that schemas cannot express directly. +- Manifest/planner gate: use backend manifest realization declarations, + `resolve_realization_concern()`, support diagnostics, and runtime + disclosure. Unsupported exact or constrained requirements are diagnostics, + not silent approximation. +- Runtime/control-plane gate: keep strict default auth, bearer/proxy identity + validation, role checks, request-size limits, idempotency, audit records, and + redacted FastAPI exception envelopes. +- OS and secret-exposure gate: do not place credentials, tokens, private keys, + environment dumps, process argv, backend-private state, hidden truth, host + paths, full tracebacks, or raw logs in SDL artifacts, contracts, fixtures, + diagnostics, audit records, persisted snapshots, or examples. +- Error-envelope gate: use SDL exceptions, `Diagnostic`, `OperationStatus`, + and redacted API errors. Diagnostics should identify address, field path, + domain, scope, or kind, not sensitive authored or realized values. +- Persistence/evidence gate: store provenance, digests, references, and + explicit evidence records through the established contracts. Do not hide new + specificity state in metadata blobs, tags, or untyped log payloads. + +## Extensibility Boundary + +The required seam is per-concern specificity metadata on the owning surface, +parameterized by concern kind, scope, domain, closure, carriage, and +provenance. Existing anchors are `ExplicitnessRecord`, ADR-070 realization +domains, and the planner concern-kind mapping in +`resolve_realization_concern()`. + +Future variations should add governed concern kinds, domain descriptors, +semantic invariants, or schema fields at the owning boundary. They should not +require re-editing every backend, duplicating schemas, or introducing a +top-level `specificity` bag. + +## Gotchas And Anti-Patterns + +- Do not add a universal `specificity:` root section or a generic + `open|constrained|exact` field disconnected from the owning concern. +- Do not treat missing data as open unless the owning rule explicitly says so. +- Do not conflate authored explicitness with backend + `RealizationSupportMode`, participant feature support levels, semantic + profiles, validation strength, backend profiles, or experiment study + membership. +- Do not reintroduce SDL scoring or reward language for evaluation + specificity. +- Do not encode normative domains in free-form `constraints`, notes, comments, + or ungoverned extension maps. +- Do not promote variable-substituted values to exact when the authored form was + constrained. +- Do not resolve ambiguous references by first match or by parser order. +- Do not persist new runtime state in `RuntimeSnapshot.metadata`, tags, audit + detail blobs, or raw backend logs. +- Do not add implementation logic under `implementations/python/src/aces/**`; + that tree is a compatibility surface. +- Do not create duplicate parsers, validators, exception hierarchies, schema + bundles, workflow scripts, logging paths, or security gates. + +## Non-Goals + +- No implementation of DSL-115 in this preflight. +- No new SDL syntax, schema publication, manifest version, compiler behavior, + planner behavior, runtime API behavior, or conformance behavior in this note. +- No change to requirement status, coverage, or traceability records. +- No relocation of experiment tasks, runs, studies, rewards, scoring, or + evaluator results into SDL. +- No solver, backend callback language, external query language, or hidden + policy engine for constraints. +- No implementation plan. diff --git a/docs/decisions/issue-738-associated-artifact-manifests-preflight.md b/docs/decisions/issue-738-associated-artifact-manifests-preflight.md new file mode 100644 index 000000000..b0f1009ae --- /dev/null +++ b/docs/decisions/issue-738-associated-artifact-manifests-preflight.md @@ -0,0 +1,442 @@ +# Issue 738 Associated Artifact Manifests Preflight + +Date: 2026-07-12 + +Issue: #738. + +Requirement: none. The issue title, body, and acceptance criteria are the +authoritative contract. + +This note records architecture guardrails for scenario- and +experiment-associated non-semantic artifact manifests. It is implementation +guidance only. It does not add normative specification text, schemas, contract +models, validators, fixtures, trust-policy entries, packaging behavior, APIs, +storage, or runtime behavior. + +## Binding Sources + +- ADR-009, ADR-019, `specs/authority/authority-boundary.yaml`, and + `contracts/README.md` keep normative prose under `specs/`, normative schemas + and fixtures under `contracts/`, and Python under the non-normative reference + implementation boundary. +- ADR-055 and `specs/formal/experiment-core/README.md` own experiment tasks, + authoring inputs, apparatus context, runs, studies, typed references, + `ExperimentArtifactRefModel`, `ExperimentChecksumModel`, and cross-artifact + validators. Identifier-bearing collections use keyed maps when uniqueness is + part of the portable contract. +- ADR-064 through ADR-066 own captured evidence, derived analysis, provenance, + sensitivity/redaction, and the rule that archival evidence is not scenario + meaning or live runtime state. +- ADR-071 and `specs/supply-chain/reusable-asset-trust-integrity.md` keep + identity, integrity, and authenticity distinct; treat reusable asset as a + family-specific role; and forbid a universal `TrustedAsset` payload. +- ADR-053 owns module registry, lockfile, OCI, signature, bounded-read, and + archive-extraction behavior. A scenario companion manifest must not become a + second module package, registry, lockfile, or archive format. +- The canonical SDL profile in `aces_sdl.canonical` identifies validated, + expanded SDL meaning only. Its `aces-sdl-semantic/v1` bytes and digest must + not absorb documentation, starter material, evaluator assets, reports, + profiles, operator material, or other companion bytes. +- ADR-059 governs changes to accepted ADRs. Any implementation amendment to + ADR-055 or ADR-071 needs an amendment row and pin update, or a superseding + ADR; accepted text must not be edited silently. +- ADR-061, ADR-075, `contracts/schema-publication-manifest.json`, and + `specs/evolution/versioning-deprecation-and-migration.md` govern contract + lineage, compatibility, publication records, and migration. + +## Architecture Decisions + +### One standalone attachment contract, not a package model + +Publish one closed, versioned associated-artifact-manifest contract. It is a +portable statement that one exact artifact-reference set is attached to one +explicit parent. It is not an SDL section, experiment record subtype, archive +index, live-directory inventory, trust decision, acquisition request, or +universal reusable-asset payload. + +The contract has a closed scope discriminator and a constrained parent +reference: + +- scenario scope permits `scenario` and `scenario-snapshot` parents; +- experiment scope permits the existing experiment artifact families named by + the issue: task, experiment authoring input, apparatus context, run, and + study; and +- the scope and parent kind must agree. Catch-all `other` parents are not + conformant attachment points. + +Reuse `ExperimentReferenceModel` and its constrained reference patterns rather +than creating a second generic reference vocabulary. Add the missing explicit +authoring-input reference kind at that shared seam if needed; do not disguise +it as `protocol`, `manifest`, or `other`. Parent matching remains +family-specific because task, authoring-input, apparatus, run, study, scenario, +and scenario-snapshot payloads have different identity fields. + +A generic `scenario` parent is an association to a conceptual scenario id only. +It cannot make a snapshot-integrity claim and must retain the existing id-only +restriction. A `scenario-snapshot` parent may carry the incumbent version and +digest binding, which the parent matcher checks by applying +`canonical_sdl_digest()` to a validated, expanded scenario. Experiment parent +refs bind the incumbent id/version fields. They must not gain a `ref_digest` +until that parent family has a normative canonical payload profile and a +validator for it; a generic experiment reference field is not permission to +self-certify a parent digest. The manifest never changes the scenario's +semantic digest. + +### Reuse the incumbent artifact descriptor + +The artifact entry must deliberately reuse or factor the incumbent +`ExperimentArtifactRefModel` / `ExperimentChecksumModel` shape: stable local +`artifact_id`, role, media type, URI, checksum, byte size, source, creation +time, sensitivity, optional description, and applicable provenance/evidence +links. Do not publish a scenario-only copy of those fields. + +Scenario companion roles may extend the shared role vocabulary where the +existing roles are insufficient, but role remains a closed contract field. +Do not use media type, filename suffix, directory name, URI path, or free-form +description as a hidden role discriminator. + +Within one manifest: + +- `artifact_id` is an opaque, case-sensitive, stable local identity. It is not + a path and is not content identity. +- `(checksum algorithm, checksum value, size)` identifies payload bytes. The + URI is a non-authoritative locator; checksum validation establishes + immutability even if a locator is mutable. +- In the portable manifest, the URI is an absolute, non-secret URI (including a + content-addressed URN where appropriate), not a pack-relative filesystem + path. URI scheme support and acquisition remain consumer policy. Userinfo, + bearer material, or other embedded credentials are forbidden. +- Artifact entries use a keyed object map, with each key equal to the embedded + `artifact_id`. This follows ADR-055 and makes duplicate local ids impossible + in the constructed portable object. Duplicate JSON member names must still + fail at the JSON ingress boundary rather than being accepted with + last-write-wins behavior. +- Reusing an artifact id in a different manifest is allowed because ids are + manifest-local. Repeating the same checksum under distinct ids is allowed + when the producer intentionally gives the same bytes distinct roles or + locators; both entries remain in the canonical set and both must validate. +- One URI must not make conflicting checksum, size, or media-type claims in the + same manifest. Exact duplicate descriptors under different keys are invalid + aliasing, not two artifacts. + +`source` and provenance fields are assertions, not authenticity evidence. +`sensitivity` governs handling and disclosure but does not grant entitlement. +No sensitivity value may weaken byte binding. Restricted or redacted metadata +must still retain the non-secret checksum and size needed for verification. + +### Separate logical manifest identity from set identity + +The manifest needs a stable logical id/version and a separately computed +associated-artifact-set digest. The set digest is derived; a caller cannot +establish it by supplying a string. + +Define a versioned canonicalization profile for the abstract contract. Its +canonical projection contains the profile id, attachment scope, exact parent +reference, and exact keyed artifact-reference set. It excludes the set-digest +field itself and excludes filesystem traversal order, archive metadata, +materialization paths, manifest filenames, export tiers, and packaging-layout +metadata. Digest spellings must be normalized by the profile so equivalent hex +case does not create different set identities; URI strings, opaque ids, and +descriptive values must not receive filesystem- or platform-dependent +normalization. + +Use the repository's incumbent RFC 8785/JCS and lowercase prefixed-digest +convention unless the normative specification records a compelling +incompatibility. Keep the associated-artifact canonicalization profile +distinct from `aces-sdl-semantic/v1`: equal SDL meaning does not imply equal +attachment sets, and equal attachment sets do not imply equal SDL meaning. + +The set digest changes when the parent reference or any canonical artifact +entry changes. It does not change merely because a downstream archive orders +files differently or chooses another safe layout. + +### Byte binding is a mandatory cross-artifact gate + +Closed schema/model validation proves shape only. Full conformance and every +integrity/trust claim require a named cross-artifact validator that receives: + +- the validated manifest; +- the concrete parent artifact when parent-payload identity must be checked; + and +- one concrete, explicitly supplied byte stream for every artifact id. + +The validator computes each declared checksum and byte size from the supplied +bytes, checks that every manifest entry has exactly one byte binding, checks +the parent reference against the supplied parent, and recomputes the set digest +from canonical contract data. A mapping that supplies only a caller-asserted +digest, size, path, URI, or prior validation boolean is not a byte binding. A +missing byte stream is a conformance failure, not an optional or offline mode. + +The validator must not fetch URIs, walk directories, unpack archives, resolve +registry credentials, or infer a payload from a filename. Acquisition and +materialization are caller responsibilities. The validator consumes already +staged bytes through a narrow reader/resolver parameter so filesystem, object +store, OCI, and in-memory consumers can share the same checksum logic without +putting those transports into the ACES contract. + +Reads must be streaming and bounded. Reject declared artifact counts, +per-artifact sizes, or total sizes that exceed caller-supplied policy limits +before reading; stop at the declared size plus one byte when detecting a size +mismatch; and never buffer an unbounded artifact set. Follow the bounded-read +pattern in `aces_sdl.module_registry`, but do not import its OCI-private +helpers or its archive semantics into the portable validator. + +### Attachment never implies inheritance + +The only attachment established by a manifest is its exact `parent_ref`. +Scenario attachments are not automatically task attachments; task attachments +are not automatically authoring-input, run, apparatus, or study attachments; +run attachments are not automatically study attachments; and study membership +does not copy member attachments into the study. + +Attaching the same bytes at another scope requires another conforming manifest +whose own parent reference names that scope. Because the parent participates in +set identity, the two manifests have different set digests even when their +artifact maps are identical. Lineage between those manifests may use existing +typed references, but lineage does not create attachment or inheritance. + +### Trust mapping is family-specific + +Keep four claims distinct: + +1. the semantic SDL digest identifies validated expanded SDL meaning; +2. the associated-artifact-set digest identifies one parent plus one exact + artifact-reference set; +3. each `artifact_checksum` binds one entry to concrete payload bytes; and +4. authenticity is an independently verified signature/trust-policy decision. + +The parent asset retains its incumbent integrity mechanism. Associated bytes do +not contribute to the scenario semantic digest, scenario-snapshot digest, task +identity, run identity, apparatus identity, authoring-input identity, or study +identity. + +Map the manifest/set as a distinct `associated_artifact_set` reusable-asset +family under ADR-071. Its set digest is `integrity_digest`; its referenced +payloads require `artifact_checksum`. This requires the reusable-asset trust +policy contract and reference fixture to gain the new family while preserving +the existing `reusable_scenario` and experiment-family mappings. A signature +over the set digest may satisfy `authenticity_signature`; the manifest digest +or checksum alone never does. + +This is not a scenario-distribution/bundle asset family. A distribution may use +the manifest, but archive or filesystem layout remains downstream. A generic +scenario attachment cannot satisfy reusable-scenario snapshot integrity. A +snapshot-scoped manifest can be independently trusted as an associated set, +but it still does not alter the parent snapshot digest. + +## Required Incumbents + +- Authority and publication: `specs/`, `contracts/schemas/`, + `contracts/fixtures/`, `contracts/schema-publication-manifest.json`, + `specs/authority/authority-boundary.yaml`, `ContractModel`, `schema_bundle()`, + `tools/generate_contract_schemas.py`, `tools/check_generated_schemas.py`, + `tools/check_schema_publication.py`, and `tools/check_json_artifacts.py`. +- Experiment identity and artifacts: `ExperimentReferenceModel`, + `ExperimentScenarioReferenceModel`, + `ExperimentScenarioSnapshotReferenceModel`, `ExperimentTaskReferenceModel`, + `ExperimentArtifactRefModel`, `ExperimentChecksumModel`, + `ExperimentTaskModel`, `ExperimentSpecModel`, + `ExperimentApparatusContextModel`, `ExperimentRunModel`, + `ExperimentStudyModel`, `_canonical_digest()`, + `_experiment_reference_key()`, `_validate_unique_experiment_references()`, + and the task/run/study cross-artifact validators. +- Portable semantic invariants: `_add_aces_invariant()`, + `AcesSemanticInvariantEntryModel`, the `x-aces-invariants` profile, and + `validate_aces_semantic_invariant_annotations()`. Payload-byte binding must + be published as a named required semantic invariant; generic JSON Schema + consumers must not claim full conformance after shape validation alone. The + current invariant `inputs` shape names contract instances only, so it must + name the manifest and applicable parent contracts while the validator's + public callable contract explicitly requires external byte readers. Do not + invent a synthetic JSON "bytes contract" or falsely describe a digest map as + the payload input; extend the semantic-invariant profile deliberately only if + portable discovery of non-contract inputs becomes a broader requirement. +- Canonical identity: `aces_sdl.canonical` and its RFC 8785 implementation as + a serialization precedent only. Associated-artifact canonicalization belongs + in the contract-owned package, not in SDL semantic identity code. +- Trust policy: `ReusableAssetTrustPolicyModel`, + `ReusableAssetFamilyTrustPolicyModel`, `REUSABLE_ASSET_FAMILIES`, the + normative trust specification, the published trust-policy schema, and its + valid/invalid fixture family. +- Diagnostics and conformance: `aces_contracts.diagnostics.Diagnostic`, + `Severity`, `aces_conformance.conformance`, `_MODEL_VALIDATORS`, + `_fixture_case_diagnostics()`, and the existing structured CLI report. Reuse + this envelope instead of adding an artifact-manifest exception hierarchy. +- Corpus and installed-distribution access: `aces_contracts.corpus`, the + existing `schemas` and `fixtures` families, Python package `force-include` + rules, and installed-corpus tests. Do not add a second manifest/schema loader. +- Manifest capability authority: `aces_contracts.manifest_authority` allowlists + describe processor/backend/participant runtime support, not every published + contract. Do not add the standalone manifest contract to those allowlists or + backend profiles unless that surface actually consumes it. +- Persistence precedents, if a later producer writes a manifest: + `aces_operations.run_artifacts.atomic_write_json_artifact()` and + `LocalControlPlaneStore._atomic_write()` demonstrate same-directory temporary + files plus `os.replace`. They are precedents for atomic writes, not authority + to store attachment manifests in control-plane state. +- Workflow gates: `.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_adr_immutability.py`, and + `tools/verify_all.py`. + +## Cross-Cutting Layers + +The intended design must pass each layer it touches: + +- **JSON/config ingress:** parse local JSON as data, reject duplicate member + names before object construction, then apply the published Draft 2020-12 + schema and closed `ContractModel(extra="forbid")` model. Do not evaluate + content, resolve remote schema references, or accept unknown fields. +- **Shape validation:** enforce scope/parent discrimination, parent-kind + constraints, keyed artifact-id equality, checksum length/algorithm, byte + sizes, RFC 3339 creation times, closed role/sensitivity values, and locator + shape through shared contract validators. Reuse archival datetime validators + rather than creating another date parser. +- **Cross-artifact semantic validation:** run parent matching, collision rules, + set-digest recomputation, and one-to-one payload-byte binding after shape + validation. Publish these requirements through `x-aces-invariants` and make + conformance invoke the same validator rather than reimplementing its logic. +- **Trust-policy gate:** evaluate parent integrity, set integrity, payload + checksums, and authenticity as separate evidence classes. Trust status never + grants authorization or bypasses sensitivity handling. +- **Secret-handling surface:** manifests carry public verification metadata, + not bearer tokens, registry credentials, signed-URL secrets, private keys, + raw credentials, hidden prompts, answer keys, environment dumps, or raw + payloads. URI userinfo and credential-bearing query strings are nonconformant + producer behavior and must never be echoed in diagnostics. +- **Resource and acquisition surface:** the core validator performs no network, + archive, registry, or directory acquisition. Downstream acquisition applies + its own allowlists, timeouts, root confinement, archive safety, and immutable + staging before passing bounded readers to the validator. +- **OS/process exposure:** no subprocess, shell, environment-variable policy + channel, token-bearing argv, ambient current-directory walk, symlink-following + traversal, or host-absolute path belongs in contract validation. The byte + resolver is an in-process parameter, not a CLI secret channel. +- **Persistence:** no ACES control-plane or runtime persistence changes are in + scope. Do not place manifests or validation status in `RuntimeSnapshot`, + `RuntimeSnapshot.metadata`, `ControlPlaneStore`, operation records, or audit + details. Downstream consumers own immutable staging, atomic promotion, + retention, and use-time revalidation. +- **Logging/observability:** conformance emits stable `Diagnostic` code, domain, + address, severity, and bounded message. It may name the contract field or + artifact id, but must not include payload bytes, full rejected objects, + credential-bearing URIs, environment data, or tracebacks. There is no new + runtime metric or log stream in this issue. +- **HTTP/auth surface, if later exposed:** use + `ControlPlaneSecurityConfig.strict_defaults()`, `_MutatingIdentity` versus + `_ReadIdentity`, `request_size_guard_response()`, request fingerprints and + idempotency, `record_audit()`, published request/response models, and the + redacted 500 handler. Validation success is not authentication, + authorization, or entitlement. URI fetching must not occur inside a request + handler merely because a manifest was accepted. +- **Error envelopes:** Pydantic/JSON Schema shape failures stay contract + failures; semantic and byte-binding failures use `Diagnostic`; a thin + convenience validator may raise existing `ValueError` after diagnostics are + available. SDL parsing errors remain SDL errors, and future HTTP adapters use + bounded `HTTPException` details plus the existing redacted internal-error + envelope. Do not add a parallel exception tree. + +## Conformance Diagnostics And Negative Cases + +Use one diagnostic producer as the source of truth, with stable codes for at +least: + +- missing concrete payload binding or a digest-only/caller-asserted binding; +- payload checksum mismatch and payload size mismatch; +- recomputed set-digest mismatch; +- parent reference or parent payload mismatch; +- scope/parent-kind confusion; +- duplicate artifact id, keyed-id mismatch, exact descriptor aliasing, and + conflicting claims for one locator; and +- changed artifact set under a previously asserted set digest. + +Diagnostics must distinguish structural invalidity from unverified integrity. +An otherwise well-shaped manifest with no supplied bytes is structurally valid +but not fully conformant and cannot satisfy trust policy. + +Valid fixtures must cover generic scenario, sealed scenario-snapshot, and every +supported experiment parent family without duplicating contract shapes. +Focused invalid fixtures must cover both attachment scopes and every negative +case above. Cross-artifact tests must mutate the parent, artifact set, bytes, +checksum, and size independently so no test passes because two claims drifted +together. Schema-only tests and model/semantic tests remain distinct. + +## Downstream Packaging And Consumer Boundary + +Scenario-pack tooling owns filesystem layout, manifest filename, archive or OCI +layout, traversal rules, release tiers, catalog metadata, and materialization. +It may produce the ACES portable manifest only after selecting a stable byte +set; a walk over a live mutable directory is not an atomic snapshot and is not +the ACES asset model. + +Consumers own acquisition, immutable staging, storage, entitlement, and +use-time verification. They must stage the parent and every referenced payload, +run ACES shape and byte-binding validation, derive rather than trust the set +digest, atomically promote the verified set, retain the manifest with the +verified bytes, and verify again before use when storage guarantees do not make +that redundant. A caller-supplied package digest may be retained as untrusted +metadata, but cannot be persisted as ACES conformance or trust evidence unless +it equals the validator-derived set digest. + +## Extensibility Seam + +The seam is a closed attachment scope plus constrained parent-reference kind, +the versioned canonicalization profile, and an injected bounded byte resolver. +Adding a future parent family should require one constrained reference/matcher, +one schema enum/union extension, and focused fixtures; it must not require +changing SDL semantics, every experiment root, storage, archive layout, +conformance diagnostics, and trust code independently. + +Adding future artifact roles extends the shared artifact-role vocabulary. +Adding a canonicalization revision publishes a new profile and compatibility +evidence; it does not silently reinterpret existing set digests. Adding a new +transport implements the downstream resolver/acquisition boundary and leaves +the manifest and byte validator unchanged. + +## Gotchas And Anti-Patterns + +Avoid: + +- changing `canonical_sdl_digest()` or treating companions as SDL meaning; +- creating `TrustedAsset`, `ScenarioBundle`, `ScenarioPack`, or a second + experiment artifact descriptor with duplicated fields; +- using a live directory walk, archive order, inode metadata, permissions, + symlink targets, manifest filename, or export tier in portable set identity; +- accepting a caller-supplied package/set digest without recomputing it and + binding every referenced checksum to concrete bytes; +- treating `artifact_id`, URI, filename, parent id, schema validity, or a prior + validation boolean as integrity or authenticity; +- allowing generic scenario refs to carry version/digest/path qualifiers or + using experiment `other` refs to avoid a missing typed parent kind; +- inferring attachment or trust across scenario, task, authoring-input, + apparatus, run, and study scopes; +- duplicating checksum models, reference vocabularies, canonical JSON helpers, + schema registries, corpus loaders, trust-policy tables, semantic validators, + diagnostics, exception hierarchies, persistence stores, audit logs, or nox + workflows; +- fetching remote URIs, walking paths, extracting archives, or following + symlinks in the contract model or core byte validator; +- storing verification state in live runtime snapshots or reconstructing an + archival manifest from mutable control-plane state; +- logging raw bytes, full validation inputs, credential-bearing locators, + secrets, or absolute host paths; and +- hand-editing generated/reference schema output, silently editing accepted + ADRs, or treating implementation models as normative authority. + +## Non-Goals And Implementation Boundaries + +- No scenario-pack directory layout, manifest filename, archive/OCI layout, + filesystem traversal algorithm, release tier, or catalog metadata. +- No SDL syntax or semantic identity change. +- No Shifter ingestion, storage, entitlement, launchability, or API policy. +- No acquisition workflow, registry, object store, persistence service, + retention job, background verifier, or use-time launch integration. +- No new cryptography, signer discovery, key distribution, certificate + authority, transparency log, or secret-bearing payload. +- No automatic inheritance between attachment scopes and no mutation of parent + asset identity when attachments change. +- No reuse of module archive layout, lockfiles, or registry signatures as the + associated-artifact manifest contract. +- No runtime/control-plane endpoint, config/environment setting, subprocess, + database table, or new logging stream. +- No implementation work in this preflight note. diff --git a/docs/decisions/issue-90-gov-901-903-versioning-governance-preflight.md b/docs/decisions/issue-90-gov-901-903-versioning-governance-preflight.md new file mode 100644 index 000000000..6334ab227 --- /dev/null +++ b/docs/decisions/issue-90-gov-901-903-versioning-governance-preflight.md @@ -0,0 +1,384 @@ +# Issue 90 GOV-901/902/903 Versioning Governance Preflight + +Date: 2026-07-11 + +Issue: #90. Spawned implementation issues: #240 (GOV-901), #241 (GOV-902), +and #242 (GOV-903). Branch anchor: GOV-901. + +Requirement: none. The joint design issue is the authoritative contract. + +This note records architecture guardrails for the joint versioning, +deprecation, and migration policy. It is implementation guidance only: it does +not create the policy ADR, normative governance specification, lifecycle +records, migrations, compatibility adapters, validators, release changes, or +published contract changes. + +## Binding Sources + +- ADR-009, ADR-019, and `specs/authority/authority-boundary.yaml` make prose + under `specs/` normative and keep implementation code non-normative. The + governance rules belong in a normative spec; an ADR should record the + decision and rationale without duplicating the full rule set. +- ADR-061 and `contracts/schema-publication-manifest.json` already own + published JSON Schema lineage, `draft`/`stable` classification, canonical + hashes, change descriptions, removal tombstones, and the conservative + stable-schema compatibility gate. The joint policy must compose with this + authority rather than create a second schema registry or classifier. +- ADR-010 and `tools/policy/adr_policy.yaml` own the one-way `aces.*` + compatibility layer and package ownership boundaries. Import compatibility + is one governed surface, not the definition of ecosystem compatibility. +- ADR-053 and `aces_sdl.module_registry` own scenario/module version matching, + lock identities, digest and signature checks, and import trust. Module + versions are not package versions or contract lineage identifiers. +- ADR-014, `.ground-control.yaml`, `.gc/plan-rules.md`, and `noxfile.py` make + `nox -s verify` the canonical verification graph. +- ADR-059, `docs/decisions/adrs/adr-index.yaml`, and + `tools/check_adr_immutability.py` govern accepted ADR evolution. ADR-061 must + not be silently edited to absorb the new policy. +- `docs/explain/releasing.md`, `.github/workflows/release-please.yml`, + `release-please-config.json`, `.release-please-manifest.json`, and + `implementations/python/pyproject.toml` are the current package-release + workflow. Release-please, Conventional Commit PR titles, and the static + project version are the canonical incumbents. +- `specs/sdl/diagnostics.md` defines the SDL error/advisory boundary. + `SDLParseError`, `SDLValidationError`, `SDLInstantiationError`, shared + `Diagnostic`, Typer errors, and bounded control-plane `HTTPException` + responses are existing delivery envelopes; lifecycle policy must not create + a universal exception hierarchy. + +## Version Domains Must Stay Distinct + +The joint policy must classify the existing version domains before it defines +compatibility. A common spelling does not give two fields common semantics. + +| Surface | Canonical incumbent | Meaning and guardrail | +|---|---|---| +| Python distribution and Git tag | `pyproject.toml`, release-please manifest/config/workflow | One `X.Y.Z` package release and matching `vX.Y.Z` tag. Release-please owns bumps and `CHANGELOG.md`; feature work must not hand-edit either. | +| Published contract lineage | `contracts/schema-publication-manifest.json`, ADR-061, `contracts/schemas/` | Exact contract ids such as `backend-manifest-v2` and payload discriminators such as `backend-manifest/v2`. The major suffix identifies a lineage; it is not a package version or a stability promise. | +| State/envelope schema identity | `aces_contracts.versions` and closed contract models | Exact wire discriminator such as `workflow-step-state/v1`. It selects payload shape; it is not an apparatus or package release. | +| Processor/backend support declaration | `aces_contracts.manifest_authority` and manifest models | Despite the legacy field name `supported_contract_versions`, values are exact governed contract ids, not SemVer ranges. Do not reinterpret the field in place. | +| Apparatus identity | `ApparatusIdentity`, processor/backend/participant manifests | Product/component identity version. Current contracts require only a non-empty value and compatibility blocks name counterpart implementations; they do not establish version-range negotiation. | +| SDL scenario and module | `Scenario.version`, `ModuleDescriptor.version`, `ImportDecl.version`, module registry | Author/module release identity. Import matching uses `packaging` specifiers for parseable versions and exact-string fallback otherwise. `*` means unpinned. It does not select an SDL schema version. | +| Domain artifact versions | experiment task/run/study fields, behavior `semantic_version`, external vocabulary source versions | Version the owning domain assigns to the artifact or source. It remains governed by that domain and must not inherit package or schema bump rules accidentally. | +| Decision and requirement status | ADR status/pin gate and Ground Control requirement status | Governance workflow state, not consumer-facing artifact lifecycle. `deprecated` ADR status must not be reused as a contract deprecation record. | + +The normative governance spec should carry one surface-class matrix that names +the identifier syntax, authority, compatibility relation, lifecycle evidence, +and migration evidence for each class. This is a documentation seam, not a new +runtime registry or universal `Version` model. + +## GOV-901 Requirement-Surface Map + +The six surface names in GOV-901 are requirement taxonomy, not six new runtime +types. Their repository authorities and compatibility evidence are: + +| GOV-901 surface | Existing authority and validation boundary | Compatibility guardrail | +|---|---|---| +| Language | `specs/sdl/`, `contracts/schemas/sdl/`, the SDL catalog parity gate, `SDLModel`, parser, instantiator, and `SemanticValidator` | Distinguish raw `sdl-yaml/v1`, normalized authoring contracts, derived phase contracts, and author-assigned `Scenario.version`. Schema acceptance alone does not establish semantic preservation across phases. | +| Semantic | ADR-016, `docs/explain/reference/shared-semantic-integrity.md`, `specs/concept-authority/semantic-profiles.md`, semantic profiles, concept catalogs, semantic helpers, and cross-stage agreement tests | Compatibility is phase- and construct-family-specific. `profile_id`, `concept_catalog_version`, behavior-assumption ids, and domain `semantic_version` values are separate identifiers; none is a package or SDL schema version. | +| Processing | ADR-008, ADR-036, processor manifests, `aces_contracts.manifest_authority`, compiler/planner contracts, conformance profiles, and owning `aces_processor` tests | `supported_sdl_versions` and `supported_contract_versions` are exact governed contract ids. Processing compatibility requires declared capabilities plus validation, compilation/planning, and conformance evidence; apparatus identity versions do not create range negotiation. | +| Contract | ADR-061, `contracts/schema-publication-manifest.json`, published schemas, closed `ContractModel` DTOs, exact wire discriminators, fixtures, and schema/conformance gates | Keep lineage identity, schema stability, structural compatibility, reader compatibility, and lifecycle state distinct. The manifest checker is a conservative structural floor, not proof of semantic or operational compatibility. | +| Module | ADR-053, `Scenario.module`, `ImportDecl`, `ModuleDescriptor`, `aces_sdl.module_registry`, lockfiles, and trust policy | A constraint match is only version selection. Registry allowlisting, archive safety, signature, digest, export-hash, namespace rewriting, and whole-scenario semantic validation remain independent mandatory gates. | +| Experiment | ADR-055, ADR-064, ADR-065, ADR-068, ADR-074, experiment-core schemas/models, semantic invariants, fixtures, replay/evidence checks, and conformance evidence | Contract `schema_version` and task/run/study/capture/measure domain versions have different roles. Do not infer scientific replay, evidence, or apparatus compatibility from common `v1` lineage suffixes or the package release. | + +The normative surface-class matrix may keep rows at the smallest authority that +has one identifier and comparison rule; it does not need one row merely because +the requirement uses one noun. It must, however, make every row above +traceable without inference. If a future change splits a surface into an +independently released family, that family gets its own matrix row and extends +its owning checker or conformance evidence rather than creating a parallel +ecosystem registry. + +## Architecture Decisions And Guardrails + +- Use one umbrella ADR for the cross-surface decisions and one normative + governance specification under `specs/`. Keep rationale and rejected + alternatives in the ADR; keep operational rules, terms, and the surface + matrix in the spec. Explanatory release or migration docs may point to the + spec but must not restate a competing policy. +- Treat versioning, deprecation, and migration as related but separate + concepts. A version identifies an artifact or lineage; compatibility is a + directional relation between a producer and consumer; deprecation is a + lifecycle notice while a surface still works; migration is the documented or + executable transition. None is a synonym for another. +- Define compatibility direction explicitly: backward (new reader accepts old + producer output), forward (old reader accepts new producer output), and full + compatibility where both hold. Name the producer, consumer, and compared + surface. A bare claim that a change is "compatible" is insufficient. +- Separate structural acceptance, semantic equivalence, behavioral + compatibility, and operational interoperability. Passing JSON Schema does + not prove that meaning or runtime behavior is preserved; sharing a package + version does not prove that two apparatus manifests interoperate. +- Keep stability and lifecycle orthogonal. ADR-061 `draft`/`stable` says how a + schema may evolve; active/deprecated/removed says whether consumers should + adopt it. Do not overload `stability`, JSON Schema's `deprecated` annotation, + ADR status, or requirement status to carry all lifecycle meanings. +- Resolve ADR-061's additive-change assumption explicitly. The reference + `ContractModel` and `SDLModel` are closed (`extra="forbid"`), and many enum + fields are literals. An optional property or enum addition that is additive + to a schema can still be rejected by an older installed reader. The joint + policy must either state the compatible-reader assumption and its evidence or + supersede/narrow the earlier claim; it must not silently call structural + schema growth end-to-end backward compatible. +- Define pre-1.0 package behavior deliberately. Release-please demotes a major + breaking bump to a minor release before 1.0. The policy must say what + compatibility a `0.y.z` consumer may rely on and must not imply ordinary + post-1.0 SemVer guarantees while automation follows a different rubric. +- Preserve the bundled-release fact: the Python code and contract corpus ship + in one `aces-sdl` artifact. This gives a coordinated release unit but does not + erase independent contract ids or make an older external processor/backend + compatible with a newer corpus automatically. +- A deprecation record must identify the exact surface, first release or + contract lineage carrying the notice, replacement, migration reference, + removal eligibility rule, and any security exception. Notice without a + supported replacement and migration path is not a complete deprecation. +- Removal must be explicit and evidence-backed. For published schemas, retain + ADR-061 version-bump rules and manifest tombstones. For Python/API/CLI/SDL + surfaces, use their existing release notes, tests, diagnostics, and + compatibility seams. Do not encode every removal in the schema manifest. +- Security removals need a named exception path that can shorten ordinary + notice while recording impact, affected versions, mitigation, and migration. + "Security" must not become an unreviewed bypass for arbitrary breaking + changes. +- Deprecation notices are non-fatal while the old surface remains supported. + Actual removal or an unsupported version fails through the owning surface's + existing error envelope. Do not turn a lifecycle notice into an SDL semantic + error, or hide an invalid removed construct as a non-fatal advisory. +- Migration guidance must be version-pair and surface specific. Human-readable + notes belong under the existing `docs/migration/` boundary; parser/CLI + messages may link or point to them. An automated migrator is justified only + for deterministic transformations and must be idempotent, preserve source + data, report lossy/ambiguous cases, and fail closed rather than silently drop + unknown fields. +- Compatibility adapters remain at the owning boundary: `aces.*` re-exports in + the compatibility tree, SDL normalization/composition in `aces_sdl`, contract + readers in `aces_contracts`, CLI presentation in `aces_cli`. Do not create a + cross-package migration service or make runtime/backend layers reinterpret + authored source. + +## Required Incumbents + +- Release and package identity: + `.github/workflows/release-please.yml`, `release-please-config.json`, + `.release-please-manifest.json`, `implementations/python/pyproject.toml`, + installed-distribution metadata, `aces_cli.main --version`, and + `docs/explain/releasing.md`. +- Release classification and audit trail: `tools/check_pr_title.py`, + `.github/workflows/pr-title-lint.yml`, Conventional Commit squash titles, + release-please's release PR, Git tag, GitHub Release, PyPI artifact, and + generated `CHANGELOG.md`. +- Published contracts: ADR-009, ADR-061, `contracts/README.md`, + `contracts/schema-publication-manifest.json`, + `tools/check_schema_publication.py`, `tools/check_generated_schemas.py`, + `tools/check_json_artifacts.py`, `schema_bundle()`, and the existing valid and + invalid fixture suites. +- Contract version vocabulary: `aces_contracts.versions`, + `aces_contracts.manifest_authority`, closed `ContractModel` DTOs, processor, + backend, participant implementation, control-plane, profile, and conformance + models that consume those exact ids. +- SDL/module compatibility: `Scenario`, `ModuleDescriptor`, `ImportDecl`, + `Lockfile`, `TrustPolicy`, `_satisfies_version()`, digest/signature/version + gates, whole-scenario semantic validation, and module registry tests. +- Compatibility direction and ownership: ADR-010, + `tools/policy/adr_policy.yaml`, `tools/policy/repo_policy.py`, owning packages + under `implementations/python/packages/`, and wrapper-only + `implementations/python/src/aces/`. +- Diagnostics and errors: `specs/sdl/diagnostics.md`, SDL error classes, + `aces_contracts.diagnostics`, Typer's current user-facing errors, + `ControlPlaneSecurityConfig.strict_defaults()`, bounded `HTTPException` + details, audit records, and the redacted internal-error handler. +- Repository workflow: `.ground-control.yaml`, `.gc/plan-rules.md`, + `noxfile.py`, `tools/check_repo_policy.py`, + `tools/check_requirement_governance.py`, `tools/verify_all.py`, ADR index and + immutability checks, docs build, and existing release/schema/manifest tests. + +## Whole-Repo View + +The intended design is repository governance, not a local schema edit. Its +scope includes: + +- normative policy prose under `specs/` and the authority manifest that makes + that prose binding; +- the policy ADR, ADR index, acceptance pin, and ADR-061 relationship; +- package version metadata, release-please configuration, PR title policy, + release workflow permissions, Git tags, PyPI publication, and changelog; +- published schemas, fixtures, profiles, concept catalogs, schema publication + manifest, contract corpus packaging, generated-schema parity, and contract + allowlists; +- SDL scenario/module versions, module import ranges, lockfiles, trust policy, + parser normalization, deprecation messages, and semantic diagnostics; +- processor/backend/participant manifest identity, supported contract ids, + compatibility declarations, conformance profiles, and runtime admission; +- explanatory release, migration, SDL, API, and contributor documentation; +- owning packages and compatibility-only wrappers; and +- policy, contract, unit, conformance, docs, and release-workflow tests in the + canonical nox graph. + +No database, service repository, runtime state store, or new controller is +needed to state this policy. Git-tracked specs, manifests, release metadata, +and tests are the persistence and audit surfaces. + +## Cross-Cutting Layers + +The intended design must pass every layer it touches: + +- Normative authority gate: binding rules live under `specs/`; the ADR records + why. Reference Python models, examples, docs, and generated schemas remain + consumers/evidence. A new top-level authority root or duplicate governance + schema is not required. +- Release-input validation: PR titles are untrusted GitHub event data and must + continue through `tools/check_pr_title.py`, which parses event JSON without + shell interpolation. Breaking markers and release types must match + release-please configuration and the normative policy. +- Release security: retain pinned GitHub Actions, least-privilege job + permissions, PyPI OIDC trusted publishing, protected environment use, and no + stored PyPI token. A future release-policy check must not require secrets or + widen `contents`, `pull-requests`, or `id-token` permissions. +- Package/config shapes: keep release-please's JSON config and manifest, + `pyproject.toml` project version, and installed distribution metadata as the + package seams. Do not add environment-variable version overrides or a second + version file. +- Schema publication gate: schema paths remain normalized, repo-relative, and + contained under `contracts/schemas/`; ids match filenames; hashes and change + descriptions match content; stable incompatible edits require a new lineage; + removals carry tombstones. Generated-schema parity remains evidence, not + authority. +- Contract and SDL validation: exact schema discriminators and supported + contract ids continue through closed Pydantic models, semantic validators, + JSON Schema validation, profile/conformance checks, and manifest authority + allowlists. Compatibility policy must not bypass these gates with coercion or + "best effort" fallback. +- Module trust and version gate: module constraints continue through + `packaging` specifier parsing or exact-string fallback, then lockfile, + registry allowlist, signature, digest, export-hash, archive-safety, and + whole-scenario validation. A version match is not a trust decision. +- Auth and control-plane gate: the design adds no endpoint. If a future API + exposes version/support information, it must reuse strict default auth, + read-role dependencies, request-size guards, published DTOs, audit events, + and redacted errors. Compatibility status must never grant authorization. +- Secret-handling gate: versions, lifecycle records, compatibility reports, + migrations, release notes, and diagnostics must not carry tokens, private + keys, registry credentials, environment dumps, private configuration, or raw + payloads. No secret file or new secret is needed for governance validation. +- OS/process exposure gate: keep version and base-revision values as ordinary + validated data. Do not put credentials in process argv, shell-interpolate PR + titles or migration input, execute user-provided version strings, or add + subprocess-based version discovery when distribution metadata and Git + artifacts already exist. +- Error-envelope gate: accepted-but-deprecated use gets a bounded notice in the + owning channel; unsupported or removed input gets the existing SDL, + contract, CLI, conformance, or HTTP error. Messages may name surface ids and + versions but must not dump full artifacts, environment state, tracebacks, or + secrets. +- Observability and audit: package evolution is visible through the release PR, + tag, GitHub Release, PyPI artifact, and `CHANGELOG.md`; schema evolution + through hashes, `last_change`, tombstones, and CI; policy verification through + nox `START`/`PASS`/`FAIL` events. Do not add runtime logs, telemetry, or a + database audit table for repository governance. + +## Extensibility Seam + +The extension seam is the normative surface-class matrix. Each row owns: + +- identifier and comparison syntax; +- source of truth; +- producer and consumer roles; +- structural, semantic, behavioral, and operational compatibility claims; +- stability and lifecycle rules; +- deprecation notice and removal eligibility parameters; and +- required migration evidence. + +The obvious future variation is a new independently released artifact family +or a different notice window for an existing class. It should add or amend one +matrix row and extend the owning manifest/checker, not require edits to every +parser, controller, DTO, service, repository, exception, and workflow. +Lifecycle thresholds belong as named policy parameters in the normative spec; +surface-specific machine enforcement stays with the existing schema manifest, +release workflow, module registry, or compatibility wrapper gate. + +## Existing Drift To Resolve Deliberately + +- `.gc/plan-rules.md`, `docs/explain/releasing.md`, the release workflow, and + `CHANGELOG.md` say release-please owns releases and that feature work does not + add fragments. `README.md`, `CONTRIBUTING.md`, the pull-request template, and + `specs/authority/authority-boundary.yaml` still describe towncrier and + `changelog.d/`; `changelog.d/` currently exists. The policy must name one + workflow (release-please is the operating incumbent) and the repository must + not retain contradictory contributor instructions. +- `docs/conf.py` reports `0.1.0` while the project and release manifest report + `0.19.1`. CLI and compatibility fallbacks also use `0.1.0`, while processor + and backend fallbacks use `0.0.0+unknown`; the FastAPI OpenAPI document + hard-codes `0.1.0`. The design must classify package version, API description + version, and "not installed" fallback instead of synchronizing unrelated + literals blindly. +- No checked-in schema is currently `stable`, so ADR-061's breaking-change + path is exercised mainly by synthetic policy tests. Promotion criteria and + regression evidence must be explicit before the first stable promotion. +- `ImportDecl.path` is described as deprecated but accepted without an emitted + lifecycle notice. Removed scoring fields have a useful parser migration + pointer, while `docs/migration/README.md` only records historical repository + moves. These are evidence that deprecation notices and migration guidance are + not yet a coherent cross-surface lifecycle. +- `supported_contract_versions` names exact ids, and apparatus compatibility + names implementations without version constraints. Renaming or expanding + those published shapes is itself a compatibility change; governance must + document present meaning before proposing a new negotiation surface. +- ADR-061's compatibility classifier is deliberately conservative and + incomplete. Its result must be described as the enforced structural floor, + not proof of semantic or implementation-reader compatibility. + +## Gotchas And Anti-Patterns + +Avoid: + +- one universal `Version`, `Compatibility`, `Deprecation`, or `Migration` + runtime model spanning package, schema, module, apparatus, and domain + artifacts; +- treating SemVer, PEP 440, contract `vN` suffixes, state schema discriminators, + source release ids, and ADR status as interchangeable; +- calling a change compatible without naming direction, producer, consumer, + and structural/semantic/behavioral/operational dimension; +- equating JSON Schema acceptance with compatibility for older closed-world + Pydantic readers; +- using package release bumps instead of independent contract ids, or using a + new contract id for every package patch; +- reinterpreting `supported_contract_versions` as ranges, or + `compatibility.processors/backends` as version negotiation, without a new + governed contract change; +- duplicating the schema manifest, contract allowlists, module version matcher, + release version file, changelog generator, validation helpers, diagnostics, + exception hierarchy, or compatibility workflow; +- editing accepted ADR-061 without supersession or a recorded ADR-059 + amendment; +- silently accepting removed syntax, dropping unknown migration data, applying + lossy auto-fixes, or allowing adapters to become permanent undocumented + semantics; +- emitting deprecation warnings from multiple layers for one use, or turning + notices into fatal semantic errors before removal; +- hand-editing package versions or `CHANGELOG.md`, reviving a parallel + towncrier workflow, or deriving release policy from stale docs; +- adding an unauthenticated version endpoint, environment-driven compatibility + override, token-bearing argv, raw-payload error, or runtime persistence layer + for repository policy; and +- claiming a support window, migration capability, stable contract, or + compatibility guarantee that no test or release artifact demonstrates. + +## Non-Goals And Implementation Boundaries + +- Implementing GOV-901, GOV-902, or GOV-903 in this preflight note. +- Writing the issue implementation plan or deciding the spawned issues' work + sequencing. +- Creating the umbrella ADR, normative governance spec, lifecycle registry, + schema, fixture, validator, migration guide, migrator, warning, adapter, or + release gate. +- Changing package versions, tags, release-please behavior, changelog history, + branch protection, PyPI publication, or support commitments. +- Promoting any schema to `stable`, changing a contract id, adding version + negotiation, or redefining existing manifest fields. +- Removing compatibility wrappers, deprecated SDL syntax, legacy documents, + or stale version literals during architecture preflight. +- Redesigning parser normalization, semantic validation, module trust, + apparatus admission, control-plane auth, persistence, logging, or error + handling. +- Guaranteeing compatibility with arbitrary pre-policy releases or external + implementations for which the repository has no conformance evidence. diff --git a/docs/explain/getting-started.md b/docs/explain/getting-started.md index 84c48b706..179a6212f 100644 --- a/docs/explain/getting-started.md +++ b/docs/explain/getting-started.md @@ -38,7 +38,7 @@ fixtures or schema authority. | Read a complete scenario | `examples/README.md`, `examples/scenarios/*.sdl.yaml` | `pytest tests/test_scenarios.py` | The checked examples load through the current parser boundary. | | Start from a reusable template or pattern | `examples/library/catalog.yaml`, `examples/library/templates/`, `examples/library/patterns/` | `python tools/check_example_library.py` | The catalog covers scenario, workflow, participant behavior, task, run, and study surfaces with parser-validated template bodies. | | Author a small SDL file | [`docs/explain/sdl/index.md`](sdl/index.md), [`docs/explain/sdl/sections.md`](sdl/sections.md), [`docs/explain/sdl/validation.md`](sdl/validation.md) | `parse_sdl_file()` or `load_scenario()` | The file is accepted by the current SDL model and semantic validator. | -| Use an agent-facing authoring surface | `aces-mcp`, then `aces_tool_surface`, `aces_agent_guidance`, and [`docs/explain/sdl/language-service.md`](sdl/language-service.md) | `aces_agent_guidance`, `sdl_completions`, `sdl_apply_edit`, `sdl_diagnostics`, `sdl_format`, `sdl_references`, `sdl_validate`, `sdl_design_assessment`, `sdl_plan`, `sdl_claims_assessment` | The agent can help author, inspect, edit, dry-run, and qualify claims without repository-local code access. | +| Use an agent-facing authoring surface | `aces-mcp`, then `aces_tool_surface`, `aces_agent_guidance`, `aces_intended_use_profiles`, and [`docs/explain/sdl/language-service.md`](sdl/language-service.md) | `aces_agent_guidance`, `aces_intended_use_profiles`, `sdl_completions`, `sdl_apply_edit`, `sdl_diagnostics`, `sdl_format`, `sdl_references`, `sdl_validate`, `sdl_design_assessment`, `sdl_plan`, `sdl_claims_assessment` | The agent can choose an intended-use scope, inspect current ACES blockers, and help author, edit, dry-run, and qualify claims without repository-local code access. | | Use variables or imports | [`docs/explain/sdl/parser.md`](sdl/parser.md), [`docs/explain/sdl/sections.md`](sdl/sections.md) | `aces sdl resolve`, `aces sdl verify-imports` | Imports and variable placeholders follow the current parser rules. | | Inspect current limits | [`docs/explain/sdl/limitations.md`](sdl/limitations.md), [`docs/explain/sdl/testing.md`](sdl/testing.md) | Compare the use case to the listed materialized surfaces | Unsupported or partial surfaces are identified before authoring. | | Work on backend conformance | [`docs/explain/sdl/runtime-architecture.md`](sdl/runtime-architecture.md), `contracts/README.md`, [`docs/explain/reference/backend-conformance.md`](reference/backend-conformance.md) | `aces conformance --help` and the conformance tests | The backend work is aligned with published contracts and fixtures. | @@ -106,7 +106,9 @@ uv run aces-mcp Start with `aces_tool_surface`, then call `aces_agent_guidance` for machine-readable scope boundaries, invariants, review priorities, and -safe-operating expectations. Use `sdl_claims_assessment` before making research +safe-operating expectations. Call `aces_intended_use_profiles` to select an +intended-use scope and inspect current ACES delivery blockers. Use +`sdl_claims_assessment` before making research or range-readiness claims. These tools do not execute participant actions or start a live range. diff --git a/docs/explain/reference/backend-conformance.md b/docs/explain/reference/backend-conformance.md index 8bb1e29c7..38a2d66b7 100644 --- a/docs/explain/reference/backend-conformance.md +++ b/docs/explain/reference/backend-conformance.md @@ -95,7 +95,7 @@ The conformance path touches these gates: reports `conformance.unsupported-capability-claim` when a manifest claims a role or feature without the runtime contract evidence surface needed to check it. -- Control-plane probe validation: live probes must use `RuntimeControlPlane` +- Control-plane probe validation: target probes must use `RuntimeControlPlane` and existing operation receipt/status/snapshot envelopes rather than backend native objects. - Error-envelope leakage: report failures as `Diagnostic` values with stable @@ -186,29 +186,30 @@ delegate that forwards to the same Typer command. ## Target Conformance Reference Scenario -Target conformance drives a live provisioning/snapshot probe (issue #606) that -proves *real* realization — a succeeded provisioning operation, non-empty -changed addresses, and a mutated snapshot — not merely a valid manifest. The -probe needs a scenario to realize. By default it uses a generic linux-vm -scenario (`_DEFAULT_CONFORMANCE_SCENARIO`). +Target conformance drives a provisioning/snapshot probe (issue #606) that proves +the target adapter accepts a plan, reports changed addresses, and mutates a +snapshot rather than passing on manifest shape alone. With a recording driver, +this is hermetic adapter evidence, not native libvirt or guest realization. +Native and guest certification require the stronger observation gates tracked +by issues #715-#717. The probe needs a scenario to exercise and defaults to a +generic linux-vm scenario (`_DEFAULT_CONFORMANCE_SCENARIO`). A single hard-coded scenario wrongly assumes *every* backend can realize it. Fixed-topology emulation backends (which map ACES nodes onto a pre-built environment) and bounded simulation backends legitimately cannot realize an arbitrary scenario, yet still honor the provisioning contract. `run_target_conformance` therefore accepts an optional `reference_scenario` (issue #663): a backend or -caller supplies a scenario it declares it can realize, and the probe holds it to -**full realization of that scenario** — the #606 mutation guard is unchanged, so -this negotiates *which* scenario is realized without weakening the requirement -that one is. - -This runner parameter is a **temporary bridge**. The durable answer is a portable -*realizability envelope* — one parameterized/typed SDL semantics with open/closed -posture that both authored scenarios and backend manifests reference, plus a -scenario/envelope subsumption relation the probe checks (and from which it derives -an in-envelope witness). That design is tracked in #667, with the subsumption -relation in #668; contract conformance and scenario realizability are distinct -dimensions and must stay separately reportable. +caller supplies a scenario inside its declared envelope. The probe verifies +adapter-level accounting for that scenario; it does not upgrade driver-reported +facts to daemon- or guest-observed evidence. + +Issue #100 publishes configuration-bound realization envelopes using the shared +parameterized SDL semantics from #667 and the membership/subsumption relation +from #668. Manifests, provisioning plans, and snapshots carry one immutable +envelope/configuration identity. Replacing the temporary +`reference_scenario` bridge with generated positive and negative probes remains +tracked by #716; contract, adapter, native-daemon, and guest conformance remain +distinct reportable dimensions. ## Non-Goals diff --git a/docs/explain/reference/coding-standards.md b/docs/explain/reference/coding-standards.md index 6645e189e..5c50719a6 100644 --- a/docs/explain/reference/coding-standards.md +++ b/docs/explain/reference/coding-standards.md @@ -80,8 +80,9 @@ Required artifacts: Do not add TLA+ or Alloy for FM0 work. -**Repo example:** parser key normalization in `aces.core.sdl.parser`, where -`start-time` becomes `start_time` but user-defined names remain concrete. +**Repo example:** explicit SDL source migration, where a formatter diagnoses and +rewrites legacy `start-time` as canonical `start_time` while user-defined names +remain concrete. Strict `sdl-yaml/v1` parsing accepts only the canonical field. ### FM1 Static Semantic diff --git a/docs/explain/reference/explicitness-realization-semantics.md b/docs/explain/reference/explicitness-realization-semantics.md index 7c087036d..4ba4c1183 100644 --- a/docs/explain/reference/explicitness-realization-semantics.md +++ b/docs/explain/reference/explicitness-realization-semantics.md @@ -123,6 +123,20 @@ additional exact requirement kinds for other artifact families; that should require adding governed terms and shared semantic checks, not rewriting planner or conformance call sites. +## Authoring Specificity + +`classify_authoring_specificity()` is the DSL-115 helper for reviewing +specificity across existing owned surfaces. It reuses the SEM-218 classifier +for exact and constrained authored values, and it records open or +underspecified concerns only when the owning caller supplies the explicit path +in `admitted_open_paths`. + +Do not treat a missing field as open by default. The helper's admitted-open +paths are authoring metadata for surfaces whose SDL, contract, or semantic rule +already allows an underspecified form; they are not backend-realization +permission and do not replace manifest `realization_support`, realization +envelopes, or experiment-core contracts. + ## Part 2 Implementation Boundary The typed compiler emission and planner gate must preserve the classifier output diff --git a/docs/explain/reference/fm-classification-ledger.yaml b/docs/explain/reference/fm-classification-ledger.yaml index 7a9758e4b..3dfb94952 100644 --- a/docs/explain/reference/fm-classification-ledger.yaml +++ b/docs/explain/reference/fm-classification-ledger.yaml @@ -434,3 +434,38 @@ entries: waived_artifacts: - kind: property_based_or_differential_tests rationale: Historical backfill records current evidence; no dedicated property-based or differential artifact was recorded for this accepted ADR. + - adr: ADR-076 + surface: Portable SDL identifiers and canonical cross-stage addresses + fm_level: FM2 + delivered_artifacts: + - kind: invariant_list + path: docs/decisions/adrs/adr-076-portable-sdl-identifiers-and-canonical-addresses.md + - kind: unit_tests + path: implementations/python/tests/test_sdl_identifiers.py + - kind: typed_ir_or_contract_coverage + path: implementations/python/packages/aces_sdl/_declarations.py + - kind: property_based_or_differential_tests + path: implementations/python/tests/test_sdl_identifiers.py + waived_artifacts: [] + - adr: ADR-078 + surface: Closed SDL phase contracts and portable derivation evidence + fm_level: FM2 + delivered_artifacts: + - kind: invariant_list + path: specs/formal/sdl-phases/README.md + - kind: unit_tests + path: implementations/python/tests/test_sdl_phase_contracts.py + - kind: typed_ir_or_contract_coverage + path: implementations/python/packages/aces_sdl/phase_contracts.py + - kind: property_based_or_differential_tests + path: implementations/python/tests/test_sdl_phase_contracts.py + waived_artifacts: [] + - adr: ADR-080 + surface: Revision-pinned SDL lineage and third-party provenance ledger + fm_level: FM1 + delivered_artifacts: + - kind: invariant_list + path: docs/decisions/adrs/adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md + - kind: unit_tests + path: implementations/python/tests/test_sdl_lineage.py + waived_artifacts: [] diff --git a/docs/explain/reference/normative-artifact-authority.md b/docs/explain/reference/normative-artifact-authority.md index b9ef602bd..1fb82938f 100644 --- a/docs/explain/reference/normative-artifact-authority.md +++ b/docs/explain/reference/normative-artifact-authority.md @@ -21,7 +21,7 @@ contributor-facing reading material. `docs/`, and examples remain non-normative worked examples. - `contracts/` is the home for normative machine-readable artifacts: published schemas, fixture corpora, capability profiles, semantic profiles, - and concept-authority catalogs. + realization envelopes, and concept-authority catalogs. - `implementations/` contains reference code only. Python models, CLI output, generated bindings, and conformance runners consume published authority; they do not define ecosystem meaning. @@ -40,7 +40,8 @@ Reuse these existing surfaces before adding anything new: - normative prose surfaces: `specs/`, especially `specs/concept-authority/` - machine-readable authority: `contracts/README.md`, `contracts/schemas/README.md`, `contracts/schema-publication-manifest.json`, - `contracts/fixtures/`, and `contracts/profiles/` + `contracts/fixtures/`, `contracts/profiles/`, and + `contracts/realization-envelopes/` - contract model and schema bundle helpers: `aces_contracts.contracts.ContractModel`, `schema_bundle()`, and the published `*Model` validators @@ -125,7 +126,7 @@ Avoid: - adding new authority-bearing artifacts anywhere other than `specs/` (for normative prose) or `contracts/` (for normative machine-readable artifacts). `docs/`, `implementations/`, `examples/`, `research/`, - `notes/`, `tools/`, and `changelog.d/` are non-normative roots per the + `notes/`, and `tools/` are non-normative roots per the authority manifest and may not host authority artifacts - adding implementation logic under the compatibility-only `implementations/python/src/aces/` tree diff --git a/docs/explain/reference/objective-semantics.md b/docs/explain/reference/objective-semantics.md index 5a67e7ebf..dd8879d99 100644 --- a/docs/explain/reference/objective-semantics.md +++ b/docs/explain/reference/objective-semantics.md @@ -15,10 +15,12 @@ A declarative *objective* binds, in one place: - zero or more **targets**, resolved through the targetable named-reference index (bare or section-qualified; objectives, workflows, and variables are not targetable); -- a **success** interpretation — `mode` (`all_of` / `any_of`) over referenced - observable `conditions` only (the OCR scoring surfaces `metrics`, - `evaluations`, `tlos`, and `goals` were removed by - [ADR-073](../../decisions/adrs/adr-073-scoring-reward-language-scope.md)); +- a **success** interpretation — `mode` (`all_of` / `any_of` / `at_least`) over + referenced invariant or postcondition assertions. Assertions use typed, + backend-neutral propositions; executable `conditions` only bind probes. The + OCR scoring surfaces `metrics`, `evaluations`, `tlos`, and `goals` were + removed by + [ADR-073](../../decisions/adrs/adr-073-scoring-reward-language-scope.md); - an optional **window** that constrains when the objective matters (the story/script/event/workflow/workflow-step reachability and consistency rules live with the window helper — see @@ -51,9 +53,10 @@ returns an `ObjectiveSemanticAnalysis`: - `window_analyses` — the per-objective `ObjectiveWindowAnalysis`, so callers reuse the window resolution rather than re-running it. -Whether a declared success `condition` is actually *bound* to a node is a -compilation-phase concern (the compiler emits `evaluation.condition-ref` -diagnostics against the resolved addresses); this analyzer deals only with the +Whether a proposition has a realizable probe binding is a compilation and +backend-admission concern. The compiler emits distinct +`evaluation.proposition.*`, `evaluation.assertion.*`, and bound-condition +diagnostics against the resolved addresses; this analyzer deals only with the name-level reference graph that is meaningful before binding resolution. ## Dependency roles @@ -77,7 +80,7 @@ That single fact lives in `partition_objective_dependencies` plus the own constant so a role change to one category lands in exactly one place. The validator, the compiler's `ObjectiveRuntime` ordering/refresh tuples, and the planner's generic reconciliation all derive their behavior from -those constants, so a change to a success condition (or to a depended-on +those constants, so a change to a success assertion (or to a depended-on objective) propagates as a refresh through `objective -> depends_on -> objective`. In the analyzer's name-level IR the derived `ordering_names` / `refresh_names` diff --git a/docs/explain/reference/realization-envelopes.md b/docs/explain/reference/realization-envelopes.md new file mode 100644 index 000000000..9d2db6f76 --- /dev/null +++ b/docs/explain/reference/realization-envelopes.md @@ -0,0 +1,175 @@ +# Realization Envelopes and Observation Strength + +This page explains how ACES states, per backend configuration, **which scenario +concerns it can realize and how strongly it can prove each one**. It is +non-normative explanation; the governing decision is +[ADR-070](../../decisions/adrs/adr-070-realization-envelope-semantics.md) and the +formal semantics live in +{download}`specs/formal/realization/envelope-semantics.md <../../../specs/formal/realization/envelope-semantics.md>`. +It is the backend-facing companion to +[Explicitness and Realization Semantics](explicitness-realization-semantics.md) +(which covers author-declaration exactness, SEM-218). + +## The problem + +"The backend realized the scenario" is not a single claim. A backend can create +a named object at the hypervisor and report success while the guest never +booted, got the wrong address, or is missing the file the scenario asked for. +Honest portability needs each backend to disclose, per concern, *what* it +realizes and *how independently that realization is observed* — and to be unable +to claim more than it can show. + +## Observation strength + +Every governed concern carries an **observation strength** — the strongest +evidence the selected configuration produces for it. The ladder is closed +(`aces_contracts.realization_envelope_carrier.ObservationStrength`): + +| Strength | Meaning | +| --- | --- | +| `none` | Not observed (the concern is `unsupported` for this configuration). | +| `driver-reported` | The driver asserts it; no independent readback. | +| `daemon-observed` | Read back from the hypervisor/daemon (e.g. libvirt domain/network XML), ownership-checked. Proves the object exists and is configured at the daemon boundary. | +| `guest-observed` | Read from **inside the realized guest** (its own `/proc`, `/sys`, `/etc`, link/file/account/service state). Proves the running system *is* what was requested, not just that an object exists. | + +A concern's **disposition** (`realized`, `transformed`, `descriptor-only`, +`unsupported`) says whether and how it is realized; the strength says how it is +proven. An `unsupported` concern must claim `none` — a configuration cannot +disclose observation for something it does not realize. + +## Concerns + +The concern taxonomy is closed (`RealizationConcern`): `topology`, +`architecture`, `image`, `resource-allocation`, `network`, `content-placement`, +`account-placement`, `feature-binding`, `service`, `acl`. A realization envelope +discloses a strength and disposition for **every** concern, so gaps are explicit +rather than implied. + +## Configuration-bound identity + +A realization envelope is bound to one **material configuration**, not to a +backend in the abstract. Its secret-free configuration identity (architecture, +image/appliance policy, network policy, supported concern set, guest-observation +transport and probe-policy version, augmentation mechanism) is hashed into a +`configuration_digest`, and the whole envelope into an `envelope_digest`. Raising +a concern's strength requires a *new* configuration and envelope — you cannot +relabel a weaker configuration as stronger. Published envelopes live under +`contracts/realization-envelopes/` and are validated on load. + +## The libvirt backend's configurations + +The libvirt backend (`aces_backend_libvirt.envelopes.LibvirtDriverMode`) ships +three material configurations: + +- **`generic`** — qcow2/cloud-init driver; concerns are `driver-reported`. +- **`techvault-appliance`** — boots a generated BusyBox initramfs appliance and + reads topology/architecture/image/resource/network back at + `daemon-observed` strength; guest concerns are `unsupported`. +- **`guest-certified-appliance`** — boots a guest-observing appliance through the + production apply path and certifies concerns from **inside** the guest. + +The guest-certified envelope discloses (verified against +`contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json`): + +| Concern | Disposition | Strength | +| --- | --- | --- | +| topology | realized | daemon-observed | +| architecture | realized | guest-observed | +| image | realized | daemon-observed | +| resource-allocation | realized | guest-observed | +| network | realized | guest-observed | +| content-placement | realized | guest-observed | +| account-placement | realized | guest-observed | +| feature-binding | unsupported | none | +| service | realized | guest-observed | +| acl | unsupported | none | + +The honesty is two-directional: content, accounts, resources, network, and a +service are certified from inside the guest, while `feature-binding` and `acl` +are disclosed `unsupported` rather than faked. One canonical guest is not claimed +to prove every image, OS, or ACL mechanism. + +## How guest-certified realization works + +For the guest-certified configuration, realization enters through the same +production path a real deployment uses +(`RuntimeManager.plan` → `RuntimeControlPlane.submit_provisioning` → +`LibvirtProvisioner.apply` → the native driver). A direct driver call, hand-built +spec, or fake connection can exercise a leaf but cannot satisfy the native-proof +gate. Then: + +1. **Boot.** A guest-observing BusyBox appliance + (`aces_backend_libvirt.guest_appliance`) is booted on real QEMU. It realizes + the bounded seeded content, account, and service placements from the plan. +2. **Read back from inside.** The appliance reads its *own* realized state — + `nproc`/`/proc/meminfo`, `ip addr`/`/sys/class/net`, in-guest file + `sha256`/mode, `/etc/passwd` posture (no credential material), and service + process + bound port — and reports bounded, line-oriented facts over a + credential-free file-backed serial channel + (`aces_backend_libvirt.guest_transport`). No SSH, no password, no general + command runner. +3. **Freshness.** A fresh per-run challenge is injected via the kernel command + line and must be echoed back; a cached or prior-boot report cannot pass. +4. **Stage and compare.** The observer + (`aces_backend_libvirt.guest_observation`) runs ordered stages (daemon → + transport → initialization → concern probes → cleanup); a later stage never + repairs an earlier one. Each concern becomes a `RealizationObservation` at + `guest-observed` strength, compared to the requested realization. Failures are + distinct, stable, redacted `Diagnostic` codes naming the safe ACES address and + observation level — never raw XML, UUIDs, host paths, URIs, or credentials. +5. **Commit eligibility.** The provisioner cannot return success, changed + addresses, or a committed snapshot until all required daemon **and** guest + observations pass. Every failure preserves the baseline snapshot. +6. **Teardown.** Cleanup runs after every attempt (including failures) and + verifies domains, networks, filters, disks, seed media, and probe artifacts + are gone; residual state fails the run. + +## Keeping claims honest + +The envelope, the observer, the falsification tests, and the evidence artifact +move together, so an envelope cannot drift into overclaiming: + +- The evidence producer (`aces_operations.libvirt_evidence_run`) binds each guest + observation to the control-plane operation, the fresh challenge, the selected + envelope/configuration and appliance digests, and a `sha256:` correlation from + the ownership-verified native identity, then runs the shared redaction and + binding validators (`aces_operations._evidence_run_validation`) *before* the + artifact is written. +- **Native-proof boundary.** A guest-certified artifact is `certifying` only when + the production driver/transport produced it. An injected fake driver may + exercise orchestration but is marked non-certifying, so a simulation can never + be published as a real proof. +- **Falsification-first** ([ADR-021](../../decisions/adrs/adr-021-falsification-first-claim-evidence-gate.md)): + the hermetic test suite feeds wrong addresses, tampered digests, dead services, + clamped memory, stale/duplicate facts, and incomplete cleanup, and asserts each + is detected and fails the run. +- Guest facts are captured evidence, kept in the validated artifact rather than + the runtime snapshot, per the observability/evidence-plane separation + ([ADR-066](../../decisions/adrs/adr-066-observability-evidence-plane-separation.md)). + +## Reproducing the proof + +The hermetic tests run in the default `nox verify` graph and never require +libvirt, KVM, privileges, or network. The native-proof gate is a separate, +opt-in run: + +```sh +# Operator/self-hosted command: boots the guest-observing appliance against a +# real libvirt/QEMU daemon and emits a validated evidence artifact. +aces libvirt techvault guest-certify \ + --scenario examples/scenarios/techvault-guest-certified.sdl.yaml \ + --project-dir . --run-id guest-proof-1 --yes +``` + +The equivalent opt-in pytest is gated on `ACES_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/`. + +## Limits + +One canonical guest-certified appliance proves a bounded concern set on a single +appliance. It does not prove all images, operating systems, service kinds, or +ACL mechanisms; those are disclosed `unsupported` rather than approximated. +Broadening coverage is downstream work (issues #716 conformance and #717 final +scenario certification), which consume these guest observations. diff --git a/docs/explain/reference/shared-semantic-integrity.md b/docs/explain/reference/shared-semantic-integrity.md index 1d5dbce17..34159e32c 100644 --- a/docs/explain/reference/shared-semantic-integrity.md +++ b/docs/explain/reference/shared-semantic-integrity.md @@ -33,8 +33,9 @@ The current canonical lifecycle is: 2. `SemanticValidator` enforces static SDL semantics and collects all authoring errors 3. `instantiate_scenario()` applies parameters/defaults, rejects unresolved - placeholders, rebuilds a concrete `InstantiatedScenario`, and reruns - semantic validation + placeholders, rebuilds the closed `InstantiatedScenario` with portable + derivation evidence, and reruns semantic validation; direct/deserialized + artifacts pass `admit_instantiated_scenario()` before compilation 4. `compile_runtime_model()` emits canonical runtime addresses, typed runtime resources, and compiled result/execution contracts 5. `plan()` validates backend capability semantics and derives typed @@ -52,10 +53,12 @@ construct by local convention. Use these existing surfaces before adding anything new: -- SDL shape and local parsing: `aces_sdl.SDLModel`, parser key normalization, - variable-key rejection, and `SDLParseError` +- SDL shape and local parsing: `aces_sdl.SDLModel`, `sdl-yaml/v1` source-profile + enforcement, explicit migration, variable-key rejection, and `SDLParseError` - static SDL validation: `SemanticValidator` and `SDLValidationError` -- instantiation: `instantiate_scenario()` and `SDLInstantiationError` +- instantiation/admission: `instantiate_scenario()`, + `admit_instantiated_scenario()`, `SDLInstantiationError`, and the published + instantiated artifact/snapshot schemas - shared SDL semantics: `aces_sdl.semantics.objectives` and `aces_sdl.semantics.workflow` - runtime graph semantics: `aces_processor.semantics.planner` @@ -87,7 +90,8 @@ A SEM-200 implementation must pass every gate it touches: out-of-scope references; collect errors through the existing validation exception instead of adding a new hierarchy. - instantiation gate: concrete scenarios must rerun semantic validation after - parameter/default substitution. + parameter/default substitution; direct artifacts must have the closed phase + shape, portable provenance, and the same semantic admission. - contract gate: external payloads use `ContractModel`, published schema generation, fixtures, and `tools/check_json_artifacts.py`; do not edit `contracts/schemas/` directly. @@ -225,14 +229,14 @@ so they are tracked by their own requirements, not here. | Construct family | Owning requirement(s) | Phases covered | Realizing artifacts | Status | | --- | --- | --- | --- | --- | | Fail-closed semantic validation (cross-cutting gate) | SEM-201 | validation, instantiation | `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_sdl/instantiate.py`, `implementations/python/tests/test_sdl_validator.py` | active | -| Stable identifiers, parameterized values, and qualified references | DSL-101, DSL-102, SEM-205 | authoring, validation, instantiation, compilation, planning, observation | `implementations/python/packages/aces_sdl/parser.py`, `implementations/python/packages/aces_sdl/variables.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/tests/test_sdl_parser.py`, `implementations/python/tests/test_sdl_validator.py` | active | +| Stable identifiers, parameterized values, and qualified references | DSL-101, DSL-102, SEM-205 | authoring, validation, instantiation, compilation, planning, execution, observation | `docs/decisions/adrs/adr-076-portable-sdl-identifiers-and-canonical-addresses.md`, `specs/sdl/document-model.md`, `specs/sdl/references.md`, `implementations/python/packages/aces_sdl/_identifiers.py`, `implementations/python/packages/aces_sdl/_declarations.py`, `implementations/python/packages/aces_sdl/parser.py`, `implementations/python/packages/aces_sdl/composition.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_runtime/backend_calls.py`, `implementations/python/packages/aces_runtime/control_plane.py`, `implementations/python/tests/test_sdl_identifiers.py` | active | | Deterministic module composition and canonical-identity stability across expansion | DSL-103, SEM-205 | authoring, validation, compilation | `implementations/python/packages/aces_sdl/composition.py`, `implementations/python/packages/aces_sdl/module_registry.py`, `specs/formal/composition-readiness.md`, `implementations/python/tests/test_sdl_module_registry.py` | active | -| Instantiation and revalidation of concrete scenarios | RUN-301 | instantiation, validation | `implementations/python/packages/aces_sdl/instantiate.py`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_run_300_lifecycle.py` | active | +| Instantiation, closed portable phase contracts, and revalidation of concrete scenarios | RUN-301 | instantiation, validation | `docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md`, `specs/formal/sdl-phases/README.md`, `contracts/schemas/sdl/instantiated-scenario-v1.json`, `contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json`, `implementations/python/packages/aces_sdl/phase_contracts.py`, `implementations/python/packages/aces_sdl/instantiate.py`, `implementations/python/tests/test_sdl_phase_contracts.py`, `implementations/python/tests/test_instantiated_scenario_schema.py`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_run_300_lifecycle.py` | active | | Objective windows, referenced scopes, reachability, and refresh | SEM-202 | validation, compilation, planning | `implementations/python/packages/aces_sdl/semantics/objectives.py`, `specs/formal/objectives/README.md`, `specs/formal/objectives/window-consistency.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py` | active | | Declarative objective actor binding, target resolution, success interpretation, and dependency ordering | DSL-112, SEM-207 | authoring, validation, instantiation, compilation, planning | `implementations/python/packages/aces_sdl/objectives.py`, `implementations/python/packages/aces_sdl/semantics/objective_semantics.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `specs/formal/objectives/README.md`, `specs/formal/objectives/declarative-objective-semantics.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py`, `implementations/python/tests/test_sdl_validator.py` | active | | Workflow control semantics (branching, joins, calling, retry, completion, history) | DSL-113, SEM-203 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/aces_sdl/orchestration.py`, `implementations/python/packages/aces_sdl/semantics/workflow.py`, `specs/formal/workflows/README.md`, `specs/formal/workflows/state-machine.md`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_runtime_models.py`, `implementations/python/tests/test_sdl_models.py` | active | | Workflow compensation semantics (registration, triggering, ordering, observation) | SEM-204 | validation, compilation, execution, observation | `implementations/python/packages/aces_sdl/semantics/workflow.py`, `specs/formal/workflows/compensation.md`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_runtime_manager.py` | partial | -| Assessment semantics over observable state (objective success references `conditions`; SDL scoring pipeline removed per ADR-073) | DSL-110, SEM-206 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/aces_sdl/conditions.py`, `implementations/python/packages/aces_sdl/semantics/assessment.py`, `implementations/python/packages/aces_sdl/semantics/objective_semantics.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `specs/formal/assessment/README.md`, `specs/formal/assessment/pipeline-consistency.md`, `implementations/python/tests/test_semantics_assessment.py`, `implementations/python/tests/test_sdl_models.py`, `implementations/python/tests/test_fm2_semantics.py` | active | +| Backend-neutral proposition truth and assertion-composed objective success (probe conditions remain separate implementation bindings; SDL scoring pipeline removed per ADR-073) | DSL-110, SEM-206 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/aces_sdl/propositions.py`, `implementations/python/packages/aces_sdl/conditions.py`, `implementations/python/packages/aces_sdl/semantics/propositions.py`, `implementations/python/packages/aces_sdl/semantics/objective_semantics.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `specs/formal/objectives/proposition-and-assertion-semantics.md`, `implementations/python/tests/test_proposition_semantics.py`, `implementations/python/tests/test_truth_result_contracts.py`, `implementations/python/tests/test_fm2_semantics.py` | active | | Runtime compiled representation and canonical addresses | RUN-302 | compilation | `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/tests/test_runtime_models.py`, `implementations/python/tests/test_fm2_semantics.py` | active | | Planner dependency, ordering, refresh, and applicability semantics | RUN-303 | planning | `implementations/python/packages/aces_processor/semantics/planner.py`, `implementations/python/packages/aces_processor/planner.py`, `specs/formal/planner/README.md`, `specs/formal/planner/dependency-ordering.md`, `implementations/python/tests/test_semantics_planner.py`, `implementations/python/tests/test_runtime_planner.py` | active | | Live execution state and lifecycle (snapshots, results, history) | RUN-304, API-402 | execution, observation | `implementations/python/packages/aces_runtime/manager.py`, `implementations/python/packages/aces_runtime/result_contracts.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/tests/test_runtime_manager.py`, `implementations/python/tests/test_runtime_models.py` | active | diff --git a/docs/explain/sdl/agent-guidance.md b/docs/explain/sdl/agent-guidance.md index 5241ed6d9..6a0752e5d 100644 --- a/docs/explain/sdl/agent-guidance.md +++ b/docs/explain/sdl/agent-guidance.md @@ -25,6 +25,8 @@ With the guidance profile, users get: - source references back to the docs, ADRs, policy files, and code that ground each rule - an audience filter for contributor-focused and operator-focused guidance +- an explicit rule that valid SDL is only the valid-fragment completeness + profile, not evidence of deployability or scientific adequacy The profile is not a permission system and does not execute SDL. It is a read-only guidance surface. @@ -77,9 +79,11 @@ A conservative agent workflow is: 1. Call `aces_tool_surface` to discover the available tool families. 2. Call `aces_agent_guidance` for current boundaries and review priorities. -3. Use `sdl_overview` and `sdl_section_reference` for SDL structure. -4. Use language-service tools for small edits and diagnostics. -5. Use `sdl_validate`, `sdl_design_assessment`, `sdl_plan`, and +3. Call `aces_intended_use_profiles` to select the intended claim scope and + inspect current ACES delivery blockers. +4. Use `sdl_overview` and `sdl_section_reference` for SDL structure. +5. Use language-service tools for small edits and diagnostics. +6. Use `sdl_validate`, `sdl_design_assessment`, `sdl_plan`, and `sdl_claims_assessment` before returning claims about readiness, portability, execution, or evidence. diff --git a/docs/explain/sdl/complex-scenarios.md b/docs/explain/sdl/complex-scenarios.md index b069bd223..26efa0041 100644 --- a/docs/explain/sdl/complex-scenarios.md +++ b/docs/explain/sdl/complex-scenarios.md @@ -89,12 +89,13 @@ red team attempts data theft and radiology disruption? - Objective success expressed against observable `conditions` (e.g. service uptime and recovery state); any graded scoring of the exercise is an experiment/evaluator-plane concern, not authored SDL -- Strong distinction between in-world telemetry and any extra experiment-side - evidence capture outside the current SDL syntax +- Strong distinction between in-world telemetry, authored + `evidence_requirements`, and experiment-side captured evidence ### SDL Stress Surface -- All 17 sections +- Broad coverage across topology, participant, narrative, objective, workflow, + and evidence authoring surfaces - Hybrid IT + clinical + vendor trust boundaries - Multiple agents with distinct initial knowledge and subnet scope - Objectives that target systems, relationships, and content diff --git a/docs/explain/sdl/index.md b/docs/explain/sdl/index.md index 5b241b1a4..c1aa3b484 100644 --- a/docs/explain/sdl/index.md +++ b/docs/explain/sdl/index.md @@ -136,8 +136,8 @@ accounts: ## Documentation -- [SDL Sections Reference](sections.md) — Complete reference for all 17 sections -- [Parser Behavior](parser.md) — Key normalization, shorthand expansion, SDL-only parsing +- [SDL Sections Reference](sections.md) — Explanatory reference for the complete live authoring-section catalog +- [Parser Behavior](parser.md) — `sdl-yaml/v1`, explicit migration, typed normalization, and canonical identity - [Language-Service Tools](language-service.md) — Agent-facing completions, references, formatting, diagnostics, and structured edits - [Agent Guidance Profile](agent-guidance.md) — Machine-readable scope boundaries, invariants, review priorities, and safe-operating expectations - [Semantic Validation](validation.md) — Cross-reference checks and what the validator enforces @@ -145,6 +145,7 @@ accounts: - [Academic Lineage](lineage.md) — Primary-source lineage for SDL semantics - [Scenario/Delivery Drift Audit](scenario-delivery-drift-audit.md) — Audit and remediation record for classification drift after ADR-033 - [Limitations](limitations.md) — Current expressiveness and materialization gaps +- [Scientific-Scenario Completeness](scientific-scenario-completeness.md) — REV1 intended-use profiles and current blocking concerns - [Testing](testing.md) — How to run unit tests, stress tests, and fuzz tests - [Complex Scenario Designs](complex-scenarios.md) — Up-front design briefs for large example exercises - [Runtime Architecture](runtime-architecture.md) — SDL-native compiler, composite plans, and runtime targets diff --git a/docs/explain/sdl/limitations.md b/docs/explain/sdl/limitations.md index db427b801..d1c21fb99 100644 --- a/docs/explain/sdl/limitations.md +++ b/docs/explain/sdl/limitations.md @@ -124,7 +124,7 @@ These are current SDL expressiveness gaps: | **Full time and clock model** | The SDL currently exposes timelines, timeouts, and budget-like controls, but it does not provide a full authoring surface for time domains, clock authority, pacing/dilation policy, synchronization mode, or explicit ordering/deadline semantics across different realizations | Time-and-simulation primary references under `research/`, ROS 2 Clock and Time, FMI, ns-3 realtime, DEVS/time-management literature | | **Full solver-backed verification** | Global proof-style verification that attack paths are reachable and defenses are consistent is not implemented; the repository uses lightweight semantic modeling, invariants, typed contracts, and selective property/state-machine methods | VSDL SMT solver, CRACK Datalog | | **Full participant behavior surface** | The current `agents` section under-expresses richer role-neutral behavior concerns such as tool/affordance declarations, control-context assets, decision-surface exposure policies, episode structure, and benchmark-oriented participant assets | CybORG, OpenRange, Open Trajectory Gym | -| **Scenario-native observability and authored evidence requirements** | The ecosystem treats in-world observability systems and authored "capture these data from these sources" requirements as first-class concerns. ADR-066 and `specs/sdl/observability-and-evidence.md` now define the plane split and authoring rules. SDL still needs executable syntax, schemas, fixtures, and validators for the broader authored evidence-requirements model | OpenRange, OCSF-informed telemetry models | +| **Materialized evidence capture and provenance** | SDL has implemented `evidence_requirements` syntax, a published schema, fixtures, and fail-closed validation for portable capture intent. It does not itself materialize captures, prove collection, calculate integrity, retain artifacts, or supply complete run-level provenance and loss reporting; those are processor/backend and experiment-evidence responsibilities | OpenRange, OCSF-informed telemetry models | | **User behavior profiles** | Normal user activity patterns (browsing, email, file access schedules) | CybORG Green agents | | **Multi-tenancy** | Multiple independent exercises sharing infrastructure | Locked Shields team-per-subnet model | diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index 6c47ebb85..32dcc6637 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -5,12 +5,20 @@ cyber range SDLs, adversary emulation formats, agent-evaluation environments, runtime architectures, and security event schemas that solve adjacent parts of the same problem. -This page is a short map of the main influences. It is not a compatibility -claim, and it is not an exhaustive bibliography. For element-level provenance, -see [Design Precedents](precedents.md). For a dimension-by-dimension comparison -against precedent systems, including where those systems lead ACES, see +This page is a short map of the main influences. It is not a provenance or +compatibility claim, and it is not an exhaustive bibliography. For design +rationale, see [Design Precedents](precedents.md). For a dimension-by-dimension +comparison against precedent systems, including where those systems lead ACES, see [Related-Work Comparison](related-work-comparison.md). +The normative audit record is +[`contracts/provenance/sdl-lineage-ledger-v1.json`](../../../contracts/provenance/sdl-lineage-ledger-v1.json). +It distinguishes intellectual lineage from artifact/code derivation and from +implementation examples, pins source revisions and bibliographic identities, +and records directional compatibility and notice disposition. An influence +listed on this page is not, by itself, a claim that ACES adopted syntax, +semantics, examples, or code from that source. + ## Specification Surface - [Open Cyber Range SDL](https://documentation.opencyberrange.ee/docs/sdl/reference/) @@ -59,7 +67,7 @@ format. ACES relies on prior work in four different ways: -- **Direct SDL lineage:** top-level `accounts` keeps the CyRIS account +- **Scenario-language precedents:** top-level `accounts` keeps the CyRIS account placement lineage. CyRIS implements `add_account`/`modify_account` as host/user management operations in code ([modules.py](https://github.com/crond-jaist/cyris/blob/8b65a30581cdd8e126c7b1fa26db2a4b770b7f17/main/modules.py)), @@ -139,7 +147,7 @@ format. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model scenario topology, deployable services/features, and validation or deployment concerns. They do not expose a portable first-class DNS zone, RRset, resolver-policy, or DNSSEC-posture inventory that ACES could reuse @@ -189,7 +197,7 @@ manager facts that surrounding ACES surfaces cannot own. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, tasks, validation, and deployment concerns. None expose a portable first-class security-monitoring manager inventory. ACES therefore adds a typed `Node.runtime` surface rather @@ -241,7 +249,7 @@ listener scope, owner, readiness evidence, and provenance. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, tasks, validation, and deployment concerns. They do not expose a portable first-class observed listener inventory with bind-address/interface semantics. ACES therefore @@ -293,7 +301,7 @@ and evidence refs. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, tasks, validation, and deployment concerns. None expose a portable first-class detection-engine inventory. ACES therefore adds a typed `Node.runtime` surface rather than @@ -324,7 +332,7 @@ neither adjacent surface can own. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, and validation/deployment concerns. None expose a portable first-class application-internal RBAC store, so ACES adds a typed node-scoped seam rather than overloading @@ -365,7 +373,7 @@ relationship rather than a recurrence. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, and validation/deployment concerns. None expose a portable first-class product-neutral scheduled-job cadence, so ACES adds a typed node-scoped seam rather than overloading @@ -405,7 +413,7 @@ backend JSON bodies. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, and validation/deployment concerns. None expose a portable first-class non-relational datastore logical state, so ACES adds a typed node-scoped seam rather than overloading @@ -466,7 +474,7 @@ bounded attributes + typed references) rather than raw object bodies. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, and validation/deployment concerns. None expose a portable first-class security-platform application inventory, so ACES adds a typed node-scoped seam rather than overloading @@ -485,7 +493,7 @@ ACES relies on prior work in four ways: [ISO/IEC 27010](https://www.iso.org/standard/68427.html) anchor the threat-intel content profile and releasability markings; NIST [SP 800-61r2](https://csrc.nist.gov/publications/detail/sp/800-61/rev-2/final) - / [r3](https://csrc.nist.gov/publications/detail/sp/800-61/rev-3/ipd) anchors + / [r3](https://csrc.nist.gov/pubs/sp/800/61/r3/final) anchors the case-management incident-handling profile. - **Automation and observability precedents:** OASIS [CACAO v2.0](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html) @@ -517,7 +525,7 @@ profile executable rather than optional. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, and validation/deployment concerns. None expose a portable first-class forwarding-agent shipping inventory, so ACES adds typed node-scoped and scenario-level seams rather than @@ -563,7 +571,7 @@ guard that makes the host-root privilege-escalation fact executable. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, and validation/deployment concerns. None expose a portable first-class container-spawn authority inventory, so ACES adds a typed node-scoped seam referencing the existing @@ -601,7 +609,7 @@ the same posture. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model scenario topology, deployable services/features, and validation/deployment concerns. None expose a portable first-class share-permission/passdb inventory that ACES could reuse directly, which @@ -657,7 +665,7 @@ ACES surfaces cannot own. ACES relies on prior work in four ways: -- **Direct SDL lineage:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK +- **Scenario-language precedents:** Open Cyber Range SDL, CyRIS, KYPO, VSDL, and CRACK model topology, deployable services/features, accounts, tasks, and validation/deployment concerns. None expose a portable first-class mail-server logical-state inventory, which is why ACES adds a typed @@ -703,7 +711,7 @@ which dynamic queue/log/config details remain evidence or bounded settings. - [OpenAI Gym](https://arxiv.org/abs/1606.01540), [Gymnasium](https://arxiv.org/abs/2407.17032), - [PettingZoo](https://papers.nips.cc/paper/2021/hash/7ed2d3454c5eea71148b11d0c25104ff-Abstract.html), + [PettingZoo](https://arxiv.org/abs/2009.14471), and [OpenSpiel](https://arxiv.org/abs/1908.09453) inform the agent-facing interface vocabulary: actions, observations, rewards, resets, local histories, imperfect information, and multi-agent interaction. @@ -797,7 +805,7 @@ which dynamic queue/log/config details remain evidence or bounded settings. - Lamport logical clocks, HLA time management, Time Warp, DEVS, SimPy, ROS 2 time, ns-3 realtime mode, and FMI inform ACES's separation of timestamp, ordering, clock authority, pacing, synchronization, and causality. -- [Fidge](https://fileadmin.cs.lth.se/cs/Personal/Amr_Ergawy/dist-algos-papers/4.pdf)/[Mattern](https://www.vs.inf.ethz.ch/publ/papers/VirtTimeGlobStates.pdf) +- [Fidge](https://doi.org/10.1109/ICDCS.1988.12501)/[Mattern](https://www.vs.inf.ethz.ch/publ/papers/VirtTimeGlobStates.pdf) vector time and the [Schwarz-Mattern causality survey](https://doi.org/10.1007/BF02277859) are the basis for vector-clock ordering claims: scalar Lamport clocks respect @@ -811,7 +819,7 @@ which dynamic queue/log/config details remain evidence or bounded settings. are the formal temporal-contract lineage for schedules, deadlines, dwell, and windows. - [Berenson et al.'s ANSI SQL isolation critique](https://doi.org/10.1145/223784.223785) - and [Adya's generalized isolation theory](https://pmg.csail.mit.edu/papers/adya-phd.pdf) + and [Adya's generalized isolation theory](https://hdl.handle.net/1721.1/8703) anchor the shared-state isolation-guarantee vocabulary. - Halpern-Pearl structural causality informs ACES's treatment of attribution: a participant action followed by an alert is not automatically a causal diff --git a/docs/explain/sdl/parser.md b/docs/explain/sdl/parser.md index d07049780..963527deb 100644 --- a/docs/explain/sdl/parser.md +++ b/docs/explain/sdl/parser.md @@ -1,30 +1,67 @@ # SDL Parser Behavior -The parser (`aces.core.sdl.parser`) transforms raw YAML into a validated `Scenario` object through three stages: key normalization, shorthand expansion, and model construction. +The parser (`aces.core.sdl.parser`) transforms raw YAML into a validated +`Scenario` object through `sdl-yaml/v1` decoding, source-marked safe +composition, canonical-field validation, shorthand expansion, and typed model +construction. This layer is intentionally about syntax, normalization, and structural model construction. It is usually an `FM0` surface under the repository's [coding standards](../reference/coding-standards.md): parser work normally needs ordinary tests, not state-machine modeling or solver-backed formal artifacts, unless it also introduces new semantic invariants above raw syntax. +The mapping-key gate is `FM1`; portable declaration identity, canonical-address +injectivity, and cross-stage coherence are `FM2`. Table-driven, property, and +differential tests pin grammar parity, ambiguity rejection, literal-map +preservation, and processor/runtime agreement. -## Key Normalization +## Canonical Fields and Migration -YAML field keys (Pydantic struct fields) are normalized to lowercase with hyphens converted to underscores: +Canonical SDL structural fields use exact lower-case `snake_case`: -- `Name` → `name` -- `Min-Score` → `min_score` -- `start-time` → `start_time` +- `name` is canonical; `Name` is migration syntax. +- `start_time` is canonical; `start-time` is migration syntax. +- `semantic_version` is canonical; `Semantic-Version` is migration syntax. -**User-defined names are preserved as-is.** Node names, feature names, account names, entity fact keys, and other HashMap keys are not transformed. This ensures cross-references remain consistent. +Declaration identities are preserved exactly but must already use the portable +local-id grammar `^[a-z0-9][a-z0-9_-]{0,63}$` (node local ids have a 35-character +maximum). The parser never lowercases or sanitizes an invalid id. Native/data +map keys such as entity facts remain under their own contracts. ```yaml -# "My-Switch" is preserved, "Type" is normalized to "type" +# "my-switch" is a portable declaration id; structural field "type" is exact. nodes: - My-Switch: - Type: Switch + my-switch: + type: switch ``` +Ordinary parsing is strict. Callers doing a deliberate conversion can select +`SDLMigrationPolicy.ACCEPT` or use `aces sdl format`; each recognized rewrite +produces a source-ranged `sdl.noncanonical_field` or +`sdl.noncanonical_merge` warning. The formatter emits strict, typed, longhand +YAML and never invents identifier renames. + +Field aliases do not imply precedence. Writing both `Name` and `name`, or both +`password-strength` and `password_strength`, in one structural mapping is a +fatal `sdl.mapping_key_conflict`. Exact duplicates are also fatal in +user-defined and native maps, but distinct valid declaration ids such as +`web-app` and `web_app` remain distinct. + +The check runs over the composed YAML node graph before a Python dictionary is +constructed, so it retains both authored spellings and source ranges. YAML +anchors remain supported. A `<<` merge is rejected by strict parsing and is +accepted only by explicit migration when all inherited and local effective +keys are disjoint; cyclic aliases are rejected. + +## Source Profile + +`sdl-yaml/v1` is UTF-8, exactly one YAML 1.2.2 document, and uses Core-schema +scalar resolution. Explicit tags/directives, non-string keys, non-finite or +non-JSON values, cyclic aliases, and resource-budget exhaustion fail before +model construction. Unlike PyYAML's default YAML 1.1 resolver, `yes`, `no`, +`on`, and `off` remain strings. The normative rules and exact budgets are in +`specs/sdl/document-model.md`. + ## Shorthand Expansion Several shorthand forms are expanded before model construction: @@ -34,16 +71,22 @@ Several shorthand forms are expanded before model construction: | `source: "pkg-name"` | `source: {name: "pkg-name", version: "*"}` | | `infrastructure: {node: 3}` | `infrastructure: {node: {count: 3}}` | | `roles: {admin: "username"}` | `roles: {admin: {username: "username"}}` | -| `min-score: 50` | `min-score: {percentage: 50}` | | `features: [svc-a, svc-b]` (on nodes) | `features: {svc-a: "", svc-b: ""}` | Source expansion only applies to actual SDL `source` fields. It is skipped inside `relationships` and `agents` where `source` is a plain string reference, and it does not fire on user-defined map keys that merely happen to be named `source`. -Shorthand expansion also works when the shorthand value is a full variable placeholder. For example, `infrastructure: {web: ${replicas}}` expands to `infrastructure: {web: {count: ${replicas}}}`, and `min-score: ${pass_pct}` expands to `min-score: {percentage: ${pass_pct}}`. +Shorthand expansion also works when the shorthand value is a full variable +placeholder. For example, `infrastructure: {web: ${replicas}}` expands to +`infrastructure: {web: {count: ${replicas}}}`. ## Variables -Full-value `${var_name}` placeholders and embedded `${var_name}` tokens are preserved as literal strings during parsing. Structural validation currently accepts placeholders in ordinary string fields, common scalar/time fields, many reference values, and selected leaf enum-backed property fields. The parser does not substitute variables or evaluate expressions. It also rejects placeholder tokens in user-defined mapping keys, because those keys define the SDL symbol table and must stay concrete. +Full-value `${var_name}` placeholders and embedded `${var_name}` tokens are +preserved as literal strings during parsing. Variable names use the same +portable local-id grammar. Structural validation accepts placeholders only in +fields whose owning contract permits parameterization. The parser does not +substitute variables or evaluate expressions, and no placeholder may define or +rename an SDL identity. The intended boundary is: @@ -70,29 +113,51 @@ or `1m+30`. Sub-second values are rounded up to whole seconds, so `1 ms` parses as `1`. Negative numeric durations are rejected rather than silently coerced. -## Format Boundary +## Format and Schema Boundaries -The parser accepts one format: +The parser accepts one source profile: -- **SDL format:** Top-level `name` field plus SDL sections +- **`sdl-yaml/v1`:** top-level `name` plus SDL sections under the source rules above. Older metadata/mode-based scenario YAMLs are intentionally rejected. They must be migrated to SDL before parsing. +`contracts/schemas/sdl/sdl-authoring-input-v1.json` validates the normalized, +typed authoring object after source decoding and shorthand expansion. It is not +a raw-YAML grammar and cannot validate tags, aliases, duplicate keys, scalar +resolution, or source limits. Canonical shipped examples use longhand values so +their strict decoded object validates against it directly. + ## Validation Pipeline -1. **YAML parsing** — `yaml.safe_load()` -2. **Key normalization** — lowercase field keys, preserve user names -3. **Shorthand expansion** — source, infrastructure, roles, min-score, feature lists -4. **Pydantic construction** — structural validation (types, ranges, required fields) -5. **Semantic validation** — cross-reference checks plus variable-reference checks (22 passes, see [validation.md](validation.md)) +1. **Source-profile preflight** — enforce UTF-8 size, tokens, aliases, and YAML 1.2 Core rules +2. **Safe YAML composition** — build a source-marked standard-tag node graph +3. **Mapping-key preflight** — reject exact/canonical collisions and migration syntax by default +4. **Safe construction** — construct JSON-domain native values only after ambiguity checks +5. **Typed normalization** — expand shorthands and normalize declared field values +6. **Pydantic construction** — structural validation (types, ranges, required fields) +7. **Module expansion** — resolve descriptor-bearing file-backed imports with explicit namespaces and one aggregate composition budget +8. **Declaration indexing** — retain typed provenance, reject canonical-address collisions, and build aliases +9. **Semantic validation** — exact cross-reference checks plus variable-reference checks (see [validation.md](validation.md)) On success, the returned `Scenario` may still carry non-fatal advisories in `scenario.advisories` (for example, VM nodes without explicit `resources`). ## API ```python +import json +from pathlib import Path + from aces.core.sdl import parse_sdl, parse_sdl_file +from aces_sdl import ( + SDLMigrationPolicy, + admit_instantiated_scenario, + canonical_instantiated_sdl_digest, + canonical_sdl_digest, + format_sdl_source, + instantiate_scenario, + load_sdl_fragment, +) # Parse from string scenario = parse_sdl(yaml_string) @@ -102,6 +167,26 @@ scenario = parse_sdl_file(Path("scenario.yaml")) # Structural validation only (skip cross-reference checks) scenario = parse_sdl(yaml_string, skip_semantic_validation=True) + +# Explicit legacy conversion; ordinary parsing remains strict. +migrated = format_sdl_source(legacy_yaml) +scenario = parse_sdl(legacy_yaml, migration_policy=SDLMigrationPolicy.ACCEPT) + +# Versioned semantic identity requires successful semantic validation. +digest = canonical_sdl_digest(parse_sdl(yaml_string)) + +# Instantiation returns the closed portable artifact; direct JSON artifacts use +# the same admission gate before compilation or snapshotting. +concrete = instantiate_scenario(scenario, parameters={}) +restored = admit_instantiated_scenario(json.loads(concrete.model_dump_json())) +snapshot_digest = canonical_instantiated_sdl_digest(restored) + +# Advanced authoring tools can preflight a fragment at its final address. +nodes = load_sdl_fragment( + nodes_yaml, + mapping_keys="literal", + base_pointer="/nodes", +) ``` Use `parse_sdl_file(...)` for SDL that uses top-level `imports:`. Import @@ -114,7 +199,7 @@ asserts the compiled output is byte-identical. Top-level composition supports: -- optional `module` descriptors for publishable SDL modules +- explicit `module` descriptors on every imported unit - `imports` using backward-compatible `path:` or canonical `source:` - `source:` classes `local:`, `oci:`, and `locked:` - repo-owned trust and resolution files: @@ -125,7 +210,17 @@ Import `source:` values are not treated as ordinary SDL package-source shorthand. They are resolved by the composition layer, not expanded into `{name, version}` package dictionaries. +Every import supplies one portable `namespace`; filenames and source paths are +never converted into identity. Composition alone creates dotted qualified +names and the reserved `__private` segment. Raw dotted declaration keys are +invalid, including when migration acceptance is enabled. + ## Error Types -- `SDLParseError` — YAML syntax errors, structural validation failures +- `SDLParseError` — YAML syntax errors and structural validation failures; + mapping-key failures carry structured `.diagnostics` with stable code, JSON + Pointer, authored spellings, and source ranges; invalid declaration keys and + scalar ids use `sdl.identifier.invalid` and do not echo the invalid spelling; + other typed-model failures use `sdl.model.invalid` with a control-escaped, + 512-character-bounded domain message and no Pydantic input rendering - `SDLValidationError` — semantic validation failures (has `.errors` list with all issues) diff --git a/docs/explain/sdl/precedents.md b/docs/explain/sdl/precedents.md index 9abc8b958..aa69b0019 100644 --- a/docs/explain/sdl/precedents.md +++ b/docs/explain/sdl/precedents.md @@ -1,6 +1,12 @@ # SDL Design Precedents -Every SDL element is adapted from an existing system or standard. This document traces each element to its source. +ACES combines externally influenced and ACES-native language families. This +document summarizes design influences and comparisons; the revision-pinned, +machine-readable provenance record is +[`contracts/provenance/sdl-lineage-ledger-v1.json`](../../../contracts/provenance/sdl-lineage-ledger-v1.json). +Only the ledger classifies provenance, artifact/code derivation, compatibility, +and notice obligations. A source named here may explain a design concern +without being a source from which ACES adopted syntax, semantics, or code. The SDL does not borrow every concern from the same place. In practice: @@ -15,33 +21,43 @@ The SDL does not borrow every concern from the same place. In practice: - **portable runtime/result contracts** follow the language-neutral boundary style used by systems such as Kubernetes, Temporal, and OpenC2 -So the source tables below explain both where a section's author-facing shape -comes from and, where needed, which systems inform its execution semantics. +The tables below are design-rationale summaries: they identify the plane on +which a source informed comparison or adaptation. They are not a parallel +provenance registry. -Each source table adds a **Borrowed** column marking whether ACES took the -source's *syntax* (the author-facing shape, keys, or structure), its *semantics* -(the meaning or runtime behavior), or *both*. Where a row is ambiguous, the cell -names which part was adopted and which was not. The "Deliberate Omissions" table -is not a borrowing table and carries no such column. +Each source table adds an **Influence plane** column: *syntax* means the +author-facing shape was compared or adapted, *semantics* means the source +informed meaning or behavior, and *both* means both kinds of design work. These +labels do not assert copying, conformance, or compatibility. The ledger's claim +plane, classification, exact source boundary, and divergence are authoritative. +The "Deliberate Omissions" table carries no influence-plane column. ## Core Structure (from Open Cyber Range SDL) -The base sections start from the [OCR SDL](https://github.com/Open-Cyber-Range/SDL-parser) v0.21.2 surface and are adapted into Python/Pydantic (per ADR-073 the OCR scoring pipeline — metrics/evaluations/TLOs/goals — was not adopted; it lives in the experiment/evaluator plane instead). This repository aims for coverage parity across the adopted OCR concepts while remaining its own SDL; when behavior diverges or OCR's own sources disagree, this document states repository behavior explicitly instead of making clone-level compatibility claims. The OCR SDL was developed by the Norwegian Cyber Range (CR14/NTNU). +The base sections have syntax and translated-model ancestry in +[OCR SDL v0.21.2](https://github.com/Open-Cyber-Range/SDL-parser/tree/fe83e8281fc4b954967fbaa5a0d099007ddcb06c), +pinned to revision `fe83e8281fc4b954967fbaa5a0d099007ddcb06c`. +The ledger names exact Rust and ACES artifact boundaries and records partial +syntax compatibility only. ACES does not claim drop-in parser, schema, +validation, or runtime compatibility. Per ADR-073, the OCR scoring pipeline +(metrics/evaluations/TLOs/goals) was removed from authored SDL and lives in the +experiment/evaluator plane. OCR SDL was developed by CR14 / the Norwegian +Cyber Range. -| SDL Element | OCR Source | Borrowed | Changes | +| SDL Element | OCR Source | Influence plane | Changes | | -------------------------- | ------------------------- | --- | --------------------------------------------------- | | Scenario | `Scenario` struct | Syntax | Added SDL extension fields | | Node (VM/Switch) | `Node`, `VM`, `Switch` | Syntax | Added `os`, `os_version`, `services`, `asset_value` | | Resources | `Resources` | Syntax | Human-readable RAM parsing via Python | -| Role | `Role` | Syntax | Direct port | +| Role | `Role` | Syntax | OCR-adapted syntax and translated model structure; partial syntax compatibility at the pinned revision | | InfraNode | `InfraNode` | Syntax | Added `acls`, `internal` flag | -| Feature | `Feature` | Syntax | Direct port | +| Feature | `Feature` | Syntax | OCR-adapted syntax and translated model structure; partial syntax compatibility at the pinned revision | | Condition | `Condition` | Both | Added `timeout`, `retries`, `start_period` | -| Vulnerability | `Vulnerability` | Syntax | Direct port | +| Vulnerability | `Vulnerability` | Syntax | OCR-adapted syntax and translated model structure; partial syntax compatibility at the pinned revision | | Metric/Evaluation/TLO/Goal | OCR scoring pipeline | Not adopted | Removed from the SDL per ADR-073; graded scoring/reward lives in the experiment/evaluator plane (ADR-055/064/069) | -| Entity | `Entity` + OCR entity surface | Both | Direct port, including OCR fact maps | -| Inject/Event/Script/Story | OCR orchestration | Both | Direct port | +| Entity | `Entity` + OCR entity surface | Both | OCR-adapted syntax and translated model structure, including fact-map ancestry; current semantics are ACES-governed | +| Inject/Event/Script/Story | OCR orchestration | Both | OCR-adapted syntax and translated model structure; current orchestration semantics are ACES-governed | | Source | `Source` (name + version) | Syntax | Made provider-neutral | @@ -50,7 +66,7 @@ The base sections start from the [OCR SDL](https://github.com/Open-Cyber-Range/S ### From CybORG CAGE Challenge -| SDL Element | CybORG Source | Borrowed | What We Adapted | +| SDL Element | CybORG Source | Influence plane | What We Adapted | | ----------------------- | ------------------------------------------- | --- | --------------------------------------------- | | `Agent` | `Agents:` section (Scenario YAML) | Both | Actions, starting sessions, reward calculator | | `InitialKnowledge` | `INT:` (Initial Network Topology) | Semantics | Known hosts and subnets at start | @@ -67,7 +83,7 @@ ecosystem surfaces. In many cases they are precedents for concerns the requirements recognize even when the current SDL syntax does not expose the full shape directly. -| Concern | Primary Sources | Borrowed | What We Adapted | +| Concern | Primary Sources | Influence plane | What We Adapted | | ------- | --------------- | --- | --------------- | | Participant decision surfaces and role-scoped observations | OpenRange episode/runtime model | Semantics | Participant-visible decision context is treated as a first-class concern distinct from hidden truth assets and internal apparatus state | | Control-context assets (instructions, directives, policies) | OpenRange prompt modes, agent-oriented benchmark/task systems | Semantics | Execution-guiding context is modeled as a participant concern without binding the ecosystem to one prompting or policy framework | @@ -83,7 +99,7 @@ Issue #71 adds a formal participant-semantics design in `specs/formal/participant-semantics/` and ADR-022. These precedents inform that design without becoming the ACES runtime API or authoring syntax. -| Concern | Primary Sources | Borrowed | What We Adapted | +| Concern | Primary Sources | Influence plane | What We Adapted | | ------- | --------------- | --- | --------------- | | Single-agent episode interface | [OpenAI Gym](https://arxiv.org/abs/1606.01540), [Gymnasium](https://arxiv.org/abs/2407.17032) | Semantics | Actions, observations, rewards, reset, termination, and truncation are explicit semantic concepts rather than incidental adapter methods | | Multi-agent environment ordering | [PettingZoo](https://arxiv.org/abs/2009.14471), [OpenSpiel](https://arxiv.org/abs/1908.09453), Markov-game literature | Semantics | Per-participant histories and information structure are first-class; joint behavior is not collapsed into one global action stream | @@ -107,7 +123,7 @@ shared-state references without making framework/tool APIs the SDL authority. ### From CyRIS -| SDL Element | CyRIS Source | Borrowed | What We Adapted | +| SDL Element | CyRIS Source | Influence plane | What We Adapted | | ----------- | ----------------------------------------- | --- | ------------------------------------------------- | | `Content` | `copy_content`, `emulate_traffic_capture` | Semantics | Generalized to file/dataset/directory types | | `Account` | `add_account`, `modify_account` | Semantics | Preserved host account-placement lineage; ACES-specific account metadata such as groups, password strength, SPN, and auth method are extensions, not CyRIS-derived directory semantics | @@ -119,7 +135,7 @@ The `runtime.identity_authorities` surface is a neutral runtime inventory surface. It borrows concepts from standards and literature, but does not adopt one provider schema as the SDL schema. -| SDL Element | Source Class | Borrowed | What We Adapted | +| SDL Element | Source Class | Influence plane | What We Adapted | | ----------- | ------------ | --- | --------------- | | `RuntimeIdentityAuthority` | LDAP/X.500 naming contexts, Kerberos realms, SAML/OIDC issuers, SCIM/IAM tenants, NIST SP 800-63C-4 federation guidance | Semantics | An authority boundary with stable ACES id plus observed namespace facts such as domain, realm, issuer, tenant, and base DN; all authority-local stable ids share one namespace | | `RuntimeIdentityService` | LDAP/Kerberos/SAML/OIDC/SCIM/IAM protocol endpoints and same-node `Node.services` transport bindings | Semantics | Protocol/API endpoint inventory without treating the endpoint as the directory contents | @@ -139,7 +155,7 @@ counterpart: it models the in-app authorization store of search clusters, key-value stores, dashboards, and platforms, distinct from the wire-protocol directory above and from database engine GRANTs. -| SDL Element | Source Class | Borrowed | What We Adapted | +| SDL Element | Source Class | Influence plane | What We Adapted | | ----------- | ------------ | --- | --------------- | | `RuntimeAppAuthorization` | Ferraiolo/Kuhn RBAC, Sandhu et al. RBAC96, ANSI INCITS 359 | Semantics | An application-internal authorization store with a stable ACES id and an open `resource_vocabulary` spine discriminator; tier placement is derived from the referencing spine, not declared | | `RuntimeAppAuthorizationPrincipal` | OpenSearch/Elasticsearch security users, Cassandra `system_auth`, Redis ACL users, dashboard/platform accounts | Semantics | Users, service accounts, API keys, and backend roles with reserved/hidden flags and a `credential_classification` only — no raw bcrypt hash, API key, or password | @@ -155,7 +171,7 @@ any one product's security configuration as the canonical authored SDL shape. ### From STIX 2.1 -| SDL Element | STIX Source | Borrowed | What We Adapted | +| SDL Element | STIX Source | Influence plane | What We Adapted | | -------------------------- | --------------------------------------- | --- | ------------------------------------------ | | `Relationship` | Relationship SRO (typed directed edges) | Both | Simplified to 7 relationship types | | Cross-reference validation | STIX object referencing model | Semantics | Source/target resolve to any named element | @@ -165,7 +181,7 @@ any one product's security configuration as the canonical authored SDL shape. ### From CACAO v2.0 -| SDL Element | CACAO Source | Borrowed | What We Adapted | +| SDL Element | CACAO Source | Influence plane | What We Adapted | | --------------------- | ---------------------------------- | --- | ---------------------------------------------------------------------- | | `Variable` | `playbook_variables` | Syntax | Types, defaults, allowed_values | | `${var}` substitution | CACAO variable substitution syntax | Both (syntax borrowed; resolution semantics deferred to instantiation) | Deferred to instantiation time | @@ -178,7 +194,7 @@ any one product's security configuration as the canonical authored SDL shape. These sources do not define the YAML keys directly, but they strongly inform how the runtime interprets workflow behavior after parsing. -| Concern | Primary Sources | Borrowed | What We Adapted | +| Concern | Primary Sources | Influence plane | What We Adapted | | ------- | --------------- | --- | --------------- | | Conditional branching over declared predicates | AWS Step Functions `Choice`, CACAO conditional steps | Semantics | Explicit decision nodes with typed predicate dependencies instead of backend-local branching rules | | Parallel branch execution and convergence | AWS Step Functions `Parallel`, W3C SCXML `parallel`, Argo DAG fan-out/fan-in patterns | Semantics | Parallel branches are explicit, joins are explicit barriers, and foreign entry into a join is rejected | @@ -192,7 +208,7 @@ how the runtime interprets workflow behavior after parsing. These sources inform the runtime/result contract rather than the SDL YAML surface. -| Concern | Primary Sources | Borrowed | What We Adapted | +| Concern | Primary Sources | Influence plane | What We Adapted | | ------- | --------------- | --- | --------------- | | Language-neutral backend boundary | Kubernetes API objects, Temporal payload/history model, OpenC2 abstract model + JSON serialization | Semantics | Backends exchange plain-data, versioned workflow result envelopes rather than Python object identity | | Explicit compiled contract between definition and execution | Kubernetes versioned object schemas, Temporal workflow definition vs event-history separation | Semantics | Compiler emits a dedicated `result_contract` instead of forcing the manager to infer semantics from incidental planner payloads | @@ -210,7 +226,7 @@ honestly across simulation, emulation, and live infrastructure. The primary research set for this area is curated in `research/primary/literature/time-and-simulation/`. -| Concern | Primary Sources | Borrowed | What We Adapted | +| Concern | Primary Sources | Influence plane | What We Adapted | | ------- | --------------- | --- | --------------- | | Distinct time domains and clock authority | [ROS 2 Clock and Time](https://design.ros2.org/articles/clock_and_time.html), [FMI 3.0.2](https://fmi-standard.org/docs/3.0.2/) | Semantics | Authored temporal intent and realized clocks cannot be treated as the same thing; multiple clocks and explicit clock authority are first-class concerns | | Event-driven, logical, and virtual time progression | [SimPy Time and Scheduling](https://simpy.readthedocs.io/en/4.0.2/topical_guides/time_and_scheduling.html), Misra virtual-time work, DEVS literature | Semantics | Time advancement policy is part of system meaning, not just a backend optimization | @@ -223,7 +239,7 @@ The primary research set for this area is curated in ### From OCSF -| SDL Element | OCSF Source | Borrowed | What We Adapted | +| SDL Element | OCSF Source | Influence plane | What We Adapted | | --------------- | ------------------- | --- | -------------------------------- | | `OSFamily` enum | `Device.os.type_id` | Both | Vocabulary for OS classification | | `ServicePort` | `NetworkEndpoint` | Both | Simplified port/protocol/name; named bindings become first-class refs | @@ -232,7 +248,7 @@ The primary research set for this area is curated in ### From Docker / Deployment Patterns -| SDL Element | Source | Borrowed | What We Adapted | +| SDL Element | Source | Influence plane | What We Adapted | | ---------------------------------------- | ------------------------------- | --- | ---------------------------- | | `SimpleProperties.internal` | Docker Compose `internal: true` | Both | Network egress blocking flag | | `Condition.timeout/retries/start_period` | Docker health check fields | Both | Direct mapping | diff --git a/docs/explain/sdl/related-work-comparison.md b/docs/explain/sdl/related-work-comparison.md index 813222e2b..4b0a8739d 100644 --- a/docs/explain/sdl/related-work-comparison.md +++ b/docs/explain/sdl/related-work-comparison.md @@ -308,7 +308,7 @@ pacing/synchronization. [IEEE 1516](https://standards.ieee.org/ieee/1516/3744/)). - **CRACK\* — no.** CRACK's Datalog encoding is atemporal; KYPO records timestamps for analysis (partial) but not as a scenario-language semantic - ([Russo et al. 2018, IEEE NCA](https://doi.org/10.1016/j.cose.2020.101837); + ([Russo et al. 2018, IEEE NCA](https://doi.org/10.1109/NCA.2018.8548324); [KYPO dataset, PMC10770710](https://pmc.ncbi.nlm.nih.gov/articles/PMC10770710/)). ## Where Precedents Lead ACES diff --git a/docs/explain/sdl/runtime-architecture.md b/docs/explain/sdl/runtime-architecture.md index e3e5c1a54..a16ab07c5 100644 --- a/docs/explain/sdl/runtime-architecture.md +++ b/docs/explain/sdl/runtime-architecture.md @@ -72,13 +72,19 @@ repo-owned concretization pass that runs before compilation. It: +- validates the normalized/expanded authoring input - applies explicit parameter values - applies SDL variable defaults - rejects unresolved `${var}` placeholders -- rebuilds a fully concrete `InstantiatedScenario` +- rebuilds a closed `InstantiatedScenario` with portable binding/import, + capability, and explicitness evidence +- removes `variables`, `imports`, and `module` from the concrete shape +- reruns semantic validation after substitution -`compile_runtime_model(scenario)` then normalizes the instantiated scenario -into runtime objects. +`compile_runtime_model(scenario)` then lowers the admitted instantiated scenario +into runtime objects. A direct or JSON-deserialized `InstantiatedScenario` +passes `admit_instantiated_scenario()` first; compiler callers cannot disable +that semantic gate. It separates reusable definitions from bound runtime instances: @@ -255,14 +261,14 @@ Validation is semantic, not section-only. Current checks include: - account features - orchestration/workflow usage - fine-grained workflow feature usage (`decision`, `retry`, `parallel` barriers, failure transitions) -- workflow predicate condition refs +- workflow predicate assertion refs - workflow predicate prior-step state refs and state-predicate subfeatures (`outcome-matching`, `attempt-counts`) - objective usage `OrchestratorCapabilities` expose both coarse workflow support and fine-grained workflow semantics: - `supports_workflows` -- `supports_condition_refs` +- `supports_assertion_refs` - `supported_workflow_features` - `supported_workflow_state_predicates` @@ -289,10 +295,12 @@ Participant implementations use `participant-implementation-manifest-v1` alongside the backend and processor surfaces rather than nesting inside either one. -Capability validation operates on concrete instantiated values rather than -placeholder domains guessed by backends. This removes the old “defer until -instantiation” gap for runtime-relevant fields such as `nodes.os` and -`infrastructure.count`. +Capability validation operates on admitted concrete values. Where +`nodes.os` or `infrastructure.count` was selected from an authored finite +domain, portable SDL provenance carries that domain and the compiler lowers it +to one typed `CompiledCapabilityConstraint`; the planner validates the whole +declared domain against backend limits. It does not reconstruct variable +definitions or pair private variable-spec/ref maps. ## Runtime Target Lifecycle @@ -397,8 +405,10 @@ Composition is registry-ready as well: - `aces sdl verify-imports` verifies lockfile, trust, digests, and signatures - `aces sdl publish` packages a publishable SDL module as an OCI image layout -Resolution and trust happen before instantiation and semantic validation, but -planner/runtime semantics still see only one fully expanded canonical scenario. +Resolution and trust happen before instantiation and semantic validation. +Planner/runtime semantics see one admitted concrete scenario; replay-relevant +resolution and binding facts remain under its typed provenance rather than in +live imports, variables, or Python-private side channels. `RuntimeManager.apply()` requires the plan provenance to match the manager: diff --git a/docs/explain/sdl/scientific-scenario-completeness.md b/docs/explain/sdl/scientific-scenario-completeness.md new file mode 100644 index 000000000..7f1a6f323 --- /dev/null +++ b/docs/explain/sdl/scientific-scenario-completeness.md @@ -0,0 +1,42 @@ +# Scientific-Scenario Completeness + +An SDL document can be valid without being ready for deployment or scientific +use. REV1 makes that distinction inspectable through five intended-use +profiles, from a valid fragment through a reproducible benchmark/study input. + +The normative +{download}`scientific-scenario completeness specification <../../../specs/sdl/scientific-scenario-completeness.md>` +defines the computation and links the canonical taxonomy and delivery +assessment. The complete concern matrix lives in those machine-readable +artifacts rather than being copied into this guide. + +The current assessment is deliberately conservative. Only +`valid-sdl-fragment` is complete. The four stronger profiles expose their +blocking concerns directly, including authored/observed-state binding, +specificity, teardown, credentials, time and clocks, participant budgets, +verifiers, hidden assets, trajectories, and behavioral-relation semantics. + +These profiles are scope contracts, not validators that silently strengthen +ordinary SDL parsing. They also do not certify a backend, an experiment result, +reproducibility, or behavioral equivalence. + +## Use Through MCP + +Agents using `aces-mcp` should call `aces_intended_use_profiles` before +authoring or making readiness claims. With no argument, the tool lists the five +profiles and their computed ACES delivery outcomes. Passing `profile_id` +returns the required concerns, current delivery status, evidence references, +limitations, blocking issue references, explicit nonclaims, and the existing +authoring tools relevant to that intended use. + +For example: + +```json +{"profile_id": "controlled-experiment-scenario"} +``` + +The result is an assessment of **what the current ACES ecosystem delivers**. +It deliberately reports `scenario_assessment.status` as `not-assessed`: the +tool does not inspect or certify a particular SDL document, experiment spec, +backend, or run. Authors use the result to choose the appropriate SDL and +`experiment_*` tools and to see which stronger claims remain blocked. diff --git a/docs/explain/sdl/sections.md b/docs/explain/sdl/sections.md index abb5ab4e6..85af6540a 100644 --- a/docs/explain/sdl/sections.md +++ b/docs/explain/sdl/sections.md @@ -1,8 +1,9 @@ # SDL Sections Reference A scenario is a YAML document with a required top-level `name`, optional -top-level composition fields (`version`, `module`, `imports`), and up to 22 named SDL -sections. Aside from `name`, all sections are optional. +top-level metadata and composition fields, and the authoring sections catalogued +below. Aside from `name`, every top-level field is optional. The normative, +machine-checked enumeration is `specs/sdl/sections.md`. Top-level composition fields are: @@ -18,7 +19,7 @@ Canonical `imports.source` classes are: ## Section Overview -### From Open Cyber Range SDL (10 sections) +### OCR-derived core The OCR scoring pipeline sections (`metrics`, `evaluations`, `tlos`, `goals`) were removed from the SDL by @@ -39,15 +40,20 @@ plane (ADR-055/064/069). `conditions` (observable state) remain. | `scripts` | `dict[str, Script]` | Timed event sequences with human-readable durations | | `stories` | `dict[str, Story]` | Top-level exercise orchestration grouping scripts | -### Extended Sections (8 sections) +### ACES extensions | Section | Type | Purpose | Adapted From | |---------|------|---------|--------------| | `content` | `dict[str, Content]` | Data placed into systems (files, datasets, emails) | CyRIS `copy_content` | | `accounts` | `dict[str, Account]` | Curated scenario/provisioning accounts on nodes, not full runtime identity inventory | CyRIS `add_account` | | `relationships` | `dict[str, Relationship]` | Typed edges between elements (auth, trust, federation) | STIX Relationship SRO | -| `agents` | `dict[str, Agent]` | Autonomous participants (actions, knowledge, scope) | CybORG Agents | -| `behavior-specifications` | `dict[str, ParticipantBehaviorSpecification]` | Versioned aggregates over participant action, observation, outcome, authority, and mode surfaces | ACES ACT-606 | +| `forwarding_agents` | `list[RuntimeForwardingAgent]` | Scenario-level forwarding and shipping agents with element-carried identity | ACES ADR-050 | +| `agents` | `dict[str, Agent]` | Autonomous participants (actions, knowledge, scope) | CybORG Agents, extended by ACES | +| `action_contracts` | `dict[str, ParticipantActionContract]` | Preconditions, effects, failures, interactions, and fidelity claims for participant actions | ACES participant model | +| `observation_boundaries` | `dict[str, ParticipantObservationBoundary]` | Participant-visible, hidden, discovered, and evidence-bearing information projections | ACES participant model | +| `outcome_interpretation_rules` | `dict[str, OutcomeInterpretationRule]` | Rules connecting action observations and evidence to scenario-local outcomes | ACES participant model | +| `behavior_specifications` | `dict[str, ParticipantBehaviorSpecification]` | Versioned aggregates over participant action, observation, outcome, authority, and mode surfaces | ACES ACT-606 | +| `evidence_requirements` | `dict[str, EvidenceRequirement]` | Portable authored capture obligations, distinct from captured evidence | ACES DSL-124, ADR-066 | | `objectives` | `dict[str, Objective]` | Scenario-local objectives binding actors, targets, windows, and success (against observable `conditions`); not EXP task records | CACAO action/target/agent | | `workflows` | `dict[str, Workflow]` | Branching and parallel control graphs over declared objectives | CACAO workflow graph patterns; semantics tightened using Step Functions / Argo / SCXML style control-flow rules | | `variables` | `dict[str, Variable]` | Parameterization (types, defaults, substitution) | CACAO playbook_variables | @@ -1351,13 +1357,16 @@ silently ignoring the missing prerequisite. --- -## Conditions +## Conditions, Propositions, and Assertions -Health checks with optional timeout/retries/start_period. +`conditions` describe executable probe implementations. They do not define +portable truth by themselves. A condition may bind to a proposition that says +what the probe observes. ```yaml conditions: web-alive: + proposition: web-alive command: "curl -sf http://localhost/ || exit 1" interval: 15 timeout: 5 @@ -1369,6 +1378,41 @@ conditions: Must have either `command` + `interval` or `source`, not both. +`propositions` are backend-neutral statements about finite subjects and typed, +semantically grounded properties. Observed-state propositions name the authored +evidence requirements needed to decide them. `assertions` apply a role and +polarity to a proposition without redefining it. + +```yaml +propositions: + web-alive: + description: The governed web service responds successfully. + subjects: [nodes.web.services.https] + basis: observed_state + predicate: + kind: boolean + property: service-alive + semantic_ref: urn:aces:observable:service-alive + operator: equals + expected: true + evidence_requirements: [web-health-evidence] + +assertions: + web-alive-at-completion: + proposition: web-alive + role: postcondition + polarity: positive + web-alive-before-action: + proposition: web-alive + role: precondition + polarity: positive +``` + +Portable truth outcomes are `true`, `false`, `unknown`, and `unsupported`. +`unsupported` reports an admitted capability limit; it is not a logical truth +value. Structural validity, evidence provenance, digest identity, and +behavioral equivalence remain separate claims. + --- ## Vulnerabilities @@ -1395,9 +1439,9 @@ The OCR-inherited SDL scoring pipeline `metrics`, `evaluations`, `tlos` (Training Learning Objectives), and `goals` sections are no longer SDL surfaces. -`conditions` remain first-class **observable state**: an objective's `success` -is expressed against `conditions` (see [Objectives](#objectives)), and workflow -predicates reference `conditions`. When a scenario genuinely needs a graded +`conditions` remain first-class probe definitions, but backend-neutral truth is +expressed through `propositions` and role-constrained `assertions` (see +[Objectives](#objectives)). When a scenario genuinely needs a graded score, cumulative reward, pass/fail evaluation, or a leaderboard value, that concern lives in the experiment/evaluator plane — experiment-core contracts (`experiment-task-v1` metric definitions, `experiment-study-v1` analysis plans; @@ -1448,7 +1492,7 @@ injects: events: attack-wave: - conditions: [scanner] + assertions: [scanner-ready] # precondition assertion injects: [phishing-email] scripts: @@ -1614,7 +1658,7 @@ agents: entity: red-team # identity + role (via entities.role) actions: [Scan, Exploit, Escalate] starting_accounts: [phished-user] # references accounts section - starting_conditions: [beacon-online] # references conditions section + starting_assertions: [beacon-online-before-start] # precondition assertion initial_knowledge: hosts: [user0] # known at scenario start subnets: [user-net] @@ -1643,9 +1687,9 @@ names declared in `nodes.*.services`, and `accounts` references entries in the `accounts` section. `allowed_subnets` follows the same switch-backed infrastructure rule. -`starting_conditions` lists names from the `conditions` section, giving the -authoring surface a declarative hook for participant-relevant precondition -checks without embedding executable setup commands. `authority_anchors` +`starting_assertions` lists precondition assertions, giving the authoring +surface a declarative hook for participant-relevant starting state without +equating a probe command with truth. `authority_anchors` references any declared scenario element (entities, relationships, content, nodes, …) that anchors what the participant is allowed or expected to do in scenario meaning — these are SDL-level anchors, not control-plane @@ -1655,7 +1699,7 @@ define the boundary of where the participant may act or observe; it generalises `allowed_subnets`, which remains restricted to switch-backed infrastructure. -Each of `starting_conditions`, `authority_anchors`, and `operating_scope` +Each of `starting_assertions`, `authority_anchors`, and `operating_scope` accepts `${var}` placeholders that resolve through the declared `variables` section. Symbol-defining keys (agent names) remain stable identifiers and must not be variables. @@ -1684,6 +1728,62 @@ remain separate apparatus surfaces. --- +## Forwarding Agents + +The top-level `forwarding_agents` list declares scenario-scoped logical +forwarders. Each element carries a stable `forwarding_agent_id`; relationship +subtypes may reference it from a `forwarding_edge`. This list is distinct from +`nodes..runtime.forwarding_agents`, which places the same logical family +on one node. + +## Action Contracts + +`action_contracts` describe participant-visible actions as declared behavior: +their applicability, intended and side effects, failure classes, interactions, +and fidelity basis. They do not embed runner commands or claim that a backend +can realize the action. + +```yaml +action_contracts: + inspect-portal: + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: bounded inspection of the declared portal + realization_profile: backend-declared +``` + +## Observation Boundaries + +`observation_boundaries` define an information projection for a participant: +what begins observable or hidden, what may become discovered, and which evidence +supports the transition. They describe scenario meaning, not UI filtering or +control-plane authorization. + +```yaml +observation_boundaries: + red-view: + projection_basis: participant-local view of the declared environment + observable_refs: [content.task-brief] + hidden_refs: [nodes.portal] + evidence_refs: [content.terminal-output] +``` + +## Outcome Interpretation Rules + +`outcome_interpretation_rules` state how participant action outcomes, +observations, objective results, and evidence claims are interpreted. They keep +the meaning of an observation separate from graded scoring or evaluator output. + +```yaml +outcome_interpretation_rules: + inspect-portal-outcome: + semantic_version: 1.0.0 + participant_scope: participant_local + observation_point_basis: inspect-portal terminal observation + interpretation_basis: retained terminal evidence supports the local outcome +``` + ## Behavior Specifications First-class participant behavior specifications name, version, and validate an @@ -1692,24 +1792,24 @@ aggregate over existing participant behavior surfaces. They do not replace rules, authority refs, backend feature claims, or runtime evidence. ```yaml -behavior-specifications: +behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - participant-role-refs: [red] - action-contract-refs: [scan] - observation-boundary-refs: [red-view] - outcome-interpretation-rule-refs: [red-outcome] - authority-scope-refs: + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + participant_role_refs: [red] + action_contract_refs: [scan] + observation_boundary_refs: [red-view] + outcome_interpretation_rule_refs: [red-outcome] + authority_scope_refs: - nodes.web-server.services.https - behavior-mode: policy-directed - ai-offensive-behavior-refs: [ai-model-access, defense-evasion] - offensive-behavior-refs: [reconnaissance, exfiltration] - realization-profile-ref: participant-implementation-manifest:red-agent - backend-feature-support-refs: [behavior_history] - evidence-contract-refs: [participant-behavior-history-event-stream-v1] - extension-policy: governed-extension + behavior_mode: policy-directed + ai_offensive_behavior_refs: [ai-model-access, defense-evasion] + offensive_behavior_refs: [reconnaissance, exfiltration] + realization_profile_ref: participant-implementation-manifest:red-agent + backend_feature_support_refs: [behavior_history] + evidence_contract_refs: [participant-behavior-history-event-stream-v1] + extension_policy: governed-extension extensions: x-acme:review-note: owner: acme @@ -1744,9 +1844,42 @@ outcome-rule runtime addresses. --- +## Evidence Requirements + +`evidence_requirements` are portable capture obligations authored with the +scenario. They state sources, scope, trigger or boundary, channel, artifact +role, media types, handling, integrity, retention, and loss-disclosure intent. +They are not evidence records and do not prove that capture occurred. + +```yaml +evidence_requirements: + portal-trace: + description: Retain the declared portal observation for the study. + source_refs: [nodes.portal] + scope_refs: [nodes.portal] + trigger_ref: conditions.portal-online + channel: application_log + artifact_role: participant_observation + media_types: [application/json] + sensitivity: plain + redaction: none + integrity: checksum + retention: study_lifetime + loss_disclosure: required +``` + +Realized capture, checksums, provenance, loss reports, and derived analysis live +in processor/backend and experiment evidence contracts. They remain separate +from the authored requirement. + +--- + ## Objectives -Declarative experiment semantics that bind actors, targets, timing, and success criteria in the same SDL. Inspired by CACAO's separation of agent, target, and workflow context; objective success is expressed against observable `conditions` ([ADR-073](../../decisions/adrs/adr-073-scoring-reward-language-scope.md)). +Declarative experiment semantics that bind actors, targets, timing, and success +criteria in the same SDL. Objective success composes invariant or postcondition +assertions over backend-neutral propositions +([ADR-079](../../decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md)). ```yaml objectives: @@ -1760,7 +1893,7 @@ objectives: - infrastructure.dmz-switch.acls.allow-dmz-https success: mode: all_of # all_of, any_of - conditions: [beacon-online] # observable state only + assertions: [beacon-online-achieved] window: stories: [exercise] scripts: [main-timeline] @@ -1771,11 +1904,18 @@ objectives: blue-reporting: entity: blue-team success: - conditions: [web-alive] # observable state only + assertions: [web-alive-at-completion] depends_on: [red-initial-access] ``` -Every objective must declare exactly one actor: either `agent` or `entity`. `success` is required and must reference at least one declared `condition` (observable state; the OCR scoring surfaces `metrics`/`evaluations`/`tlos`/`goals` were removed by [ADR-073](../../decisions/adrs/adr-073-scoring-reward-language-scope.md)). `targets` are optional, but when present they must resolve to named scenario elements. Bare target refs work when unambiguous; otherwise use a qualified ref such as `nodes.web-server`, `features.app-to-db`, or `content.mailbox.items.invoice.eml`. `window` is optional; when supplied, referenced stories/scripts/events/workflows must exist and remain internally consistent. Workflow steps use qualified refs of the form `.`. +Every objective must declare exactly one actor: either `agent` or `entity`. +`success` is required and must reference at least one declared invariant or +postcondition assertion. `targets` are optional, but when present they must +resolve to named scenario elements. Bare target refs work when unambiguous; +otherwise use a qualified ref such as `nodes.web-server`, `features.app-to-db`, +or `content.mailbox.items.invoice.eml`. `window` is optional; when supplied, +referenced stories/scripts/events/workflows must exist and remain internally +consistent. Workflow steps use qualified refs of the form `.`. `depends_on` is an ordering relation, not just commentary. It defines a partial order over objectives: downstream objectives are not considered ready until their predecessors have been satisfied. Objective dependency cycles are rejected. @@ -1799,7 +1939,7 @@ workflows: branch-on-promotion: type: decision when: - conditions: [rogue-release-promoted] + assertions: [rogue-release-promoted-before-branch] then: rollback-fanout else: finish rollback-fanout: diff --git a/docs/explain/sdl/testing.md b/docs/explain/sdl/testing.md index 1ba1d3ddf..c37e250cb 100644 --- a/docs/explain/sdl/testing.md +++ b/docs/explain/sdl/testing.md @@ -30,7 +30,9 @@ uv run --extra dev pytest tests/test_sdl_models.py tests/test_sdl_validator.py \ tests/test_sdl_parser.py -v ``` -Tests structural validation (Pydantic models), semantic validation (cross-reference checks), and parser behavior (normalization, shorthands, SDL-only format boundary). +Tests `sdl-yaml/v1` decoding and migration diagnostics, structural validation +(Pydantic models), semantic validation (cross-reference checks), shorthand +expansion, and the SDL-only format boundary. The unit suites also cover OCR-derived duration grammar, workflow graphs, direct service/ACL target refs, and `${var}` placeholder handling across supported scalar/reference fields including selected leaf enums. ### Stress Tests (standard run) @@ -66,7 +68,10 @@ cd implementations/python && \ uv run --extra dev pytest tests/test_sdl_fuzz.py -m fuzz -v ``` -Property-based testing using [Hypothesis](https://hypothesis.readthedocs.io/). Generates ~1,050 random inputs per run across 6 fuzz strategies: +Property-based testing using [Hypothesis](https://hypothesis.readthedocs.io/). +The dedicated mapping-key property tests run in the standard suite; the fuzz +session adds mutation coverage and generates about 1,150 inputs per run across +8 strategies: | Test | Strategy | Examples | |------|----------|----------| @@ -76,6 +81,8 @@ Property-based testing using [Hypothesis](https://hypothesis.readthedocs.io/). G | `test_fuzz_service_ports` | Random port/protocol/name combos | 100 | | `test_fuzz_vulnerability_class_validation` | Random CWE class strings | 100 | | `test_fuzz_feature_dependency_cycles` | Random dependency graphs | 100 | +| `test_fuzz_structural_key_alias_mutations_fail_closed` | Normalized field-alias collisions | 50 | +| `test_fuzz_exact_identifier_duplicate_mutations_fail_closed` | Exact symbol-key duplicates | 50 | The invariant: the parser **never** raises an unhandled exception. Every input either produces a valid `Scenario` or raises `SDLParseError`/`SDLValidationError`. @@ -103,6 +110,13 @@ so they stay valid as real SDL artifacts: - `satcom-release-poisoning.sdl.yaml` - `port-authority-surge-response.sdl.yaml` +`test_example_schema_conformance.py` proves each shipped SDL example through +two independent legs: strict `sdl-yaml/v1` decoding followed by direct +validation of the decoded longhand object against the checked-in normalized +authoring schema, and reference-parser model serialization against that same +schema. The valid/invalid/migration source-profile corpus lives under +`contracts/fixtures/sdl/sdl-yaml-v1/`. + The up-front design briefs for the complex examples live in [`docs/explain/sdl/complex-scenarios.md`](complex-scenarios.md). @@ -131,12 +145,14 @@ Use the corpus leg that matches the artifact's purpose: specimens rather than reusable examples. Add them to the relevant `SCENARIOS` list so the parametrized tests pick them up. - **Negative-path cases** should stay close to the parser, model, or validator - rule they exercise. Do not add invalid files under `examples/scenarios/`; - that directory is the positive example corpus. + rule they exercise. Parser-negative SDL fixtures live under + `implementations/python/tests/data/sdl/invalid/`. Do not add invalid files + under `examples/scenarios/`; that directory is the positive example corpus. All scenario corpus loading should continue to flow through the existing -`load_scenario()` / `parse_sdl()` boundary so it receives the same `yaml.safe_load` -parsing, Pydantic structural validation, semantic validation, advisory logging, +`load_scenario()` / `parse_sdl()` boundary so it receives the same source-marked +safe composition, mapping-key preflight, Pydantic structural validation, +semantic validation, advisory logging, and `ScenarioValidationError`/`SDLParseError`/`SDLValidationError` behavior as the rest of the SDL stack. diff --git a/docs/explain/sdl/validation.md b/docs/explain/sdl/validation.md index d9229f84c..b5464130f 100644 --- a/docs/explain/sdl/validation.md +++ b/docs/explain/sdl/validation.md @@ -13,7 +13,12 @@ becoming a validator-only interpretation of the SDL. ## Validation Passes -### OCR SDL passes (ported from Rust `Scenario::formalize()`) +### OCR-Ancestry SDL Passes + +These checks have translated algorithmic ancestry in OCR v0.21.2 +`Scenario::formalize()` at revision +`fe83e8281fc4b954967fbaa5a0d099007ddcb06c`; current behavior is governed by +ACES validators and is not claimed compatible with the OCR implementation. | Pass | What It Checks | |------|----------------| @@ -83,8 +88,9 @@ algorithm and value. Runtime healthcheck entries marked as redacted must omit raw output. Runtime mount sources/options and local-control bind sources classified as `redacted` or `operator_secret` must omit the corresponding raw value; the Python models and generated JSON Schemas both reject non-empty raw -values for redacted/operator-secret labels accepted by the parser's -normalization rules, including case-insensitive hyphen/underscore spellings. +values for redacted/operator-secret labels accepted by the value-normalization +rules, including case-insensitive hyphen/underscore enum spellings. Structural +field keys themselves use exact `snake_case` under `sdl-yaml/v1`. Runtime observed-value surfaces share the ADR-056/ADR-057 raw-value helper: redacted and operator-secret classifications omit raw values. ADR-057 supersedes the earlier name-driven omission rule: credential-shaped names do not by @@ -505,7 +511,7 @@ the current validator surface. The normative boundary between a fatal **error** and a non-fatal **advisory** — including the classification criterion that decides which channel a condition belongs to — is stated in -[`specs/sdl/diagnostics.md` §5](../../../specs/sdl/diagnostics.md). This page is +{download}`specs/sdl/diagnostics.md <../../../specs/sdl/diagnostics.md>` §5. This page is non-normative explanation and cites that criterion rather than restating it: an **error** affects SDL meaning (structural/semantic invariants), while an **advisory** is a deployability or quality heuristic that leaves SDL meaning @@ -521,7 +527,7 @@ Current advisory coverage: The fatal, fail-closed error semantics and the collect-all behaviour described here are the explanatory companion to the normative diagnostic boundary in -[`specs/sdl/diagnostics.md`](../../../specs/sdl/diagnostics.md). +{download}`specs/sdl/diagnostics.md <../../../specs/sdl/diagnostics.md>`. All passes run to completion. Errors are collected into a list and raised as a single `SDLValidationError`: diff --git a/docs/index.md b/docs/index.md index 68812e914..5c453bdec 100644 --- a/docs/index.md +++ b/docs/index.md @@ -78,6 +78,7 @@ 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 @@ -102,85 +103,7 @@ aces/inventory/index :maxdepth: 2 :caption: Architecture Decisions -decisions/adrs/README -decisions/adrs/adr-000-use-adrs -decisions/adrs/adr-001-scenario-description-language -decisions/adrs/adr-002-declarative-sdl-objectives -decisions/adrs/adr-003-workflows-targetable-subobjects-and-enum-variables -decisions/adrs/adr-004-sdl-runtime-layer -decisions/adrs/adr-005-control-flow-primitives -decisions/adrs/adr-006-workflow-control-language-redesign -decisions/adrs/adr-007-lightweight-formal-methods-policy -decisions/adrs/adr-008-processor-layer-and-execution-artifact-boundaries -decisions/adrs/adr-009-normative-artifact-authority-and-repository-structure -decisions/adrs/adr-010-repository-realignment-order-and-compatibility-policy -decisions/adrs/adr-011-narrow-end-to-end-mvp-validation -decisions/adrs/adr-012-shared-concept-authority-and-aces-extension-discipline -decisions/adrs/adr-013-participant-episode-lifecycle-boundaries -decisions/adrs/adr-014-nox-as-canonical-verification-graph -decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap -decisions/adrs/adr-016-semantic-layer-scope-and-coverage-model -decisions/adrs/adr-017-conversation-surface-hardening -decisions/adrs/adr-018-classification-based-assurance-policy -decisions/adrs/adr-019-normative-authority-boundary-manifest -decisions/adrs/adr-020-declarative-participant-framing-boundaries -decisions/adrs/adr-021-falsification-first-claim-evidence-gate -decisions/adrs/adr-022-participant-behavior-and-interaction-semantics -decisions/adrs/adr-023-container-image-build-provenance-surface -decisions/adrs/adr-024-local-identity-inventory-surface -decisions/adrs/adr-025-container-network-realization-surface -decisions/adrs/adr-026-application-http-surface-inventory -decisions/adrs/adr-027-container-init-reaper-runtime-surface -decisions/adrs/adr-028-container-seccomp-security-options-surface -decisions/adrs/adr-029-database-logical-state-runtime-surface -decisions/adrs/adr-030-process-scoped-linux-capability-policy -decisions/adrs/adr-031-ssh-server-configuration-surface -decisions/adrs/adr-032-directory-domain-identity-runtime-surface -decisions/adrs/adr-033-scenario-delivery-boundary-for-runtime-node-state -decisions/adrs/adr-034-runtime-software-component-inventory -decisions/adrs/adr-035-service-manager-unit-state-runtime-surface -decisions/adrs/adr-036-sdl-processor-runtime-module-boundaries -decisions/adrs/adr-037-runtime-file-service-and-filesystem-presence-semantics -decisions/adrs/adr-038-runtime-mail-service-logical-state -decisions/adrs/adr-039-dns-service-runtime-inventory -decisions/adrs/adr-040-security-monitoring-manager-runtime-inventory -decisions/adrs/adr-041-participant-implementation-manifest-and-provenance -decisions/adrs/adr-042-network-sensor-runtime-monitoring -decisions/adrs/adr-043-runtime-service-listener-surface -decisions/adrs/adr-044-network-detection-engine-runtime-inventory -decisions/adrs/adr-045-security-monitoring-detection-definition-semantics -decisions/adrs/adr-046-app-authorization-runtime-inventory -decisions/adrs/adr-047-scheduled-job-runtime-inventory -decisions/adrs/adr-048-datastore-service-runtime-inventory -decisions/adrs/adr-049-platform-application-runtime-inventory -decisions/adrs/adr-050-forwarding-agent-runtime-inventory -decisions/adrs/adr-051-orchestration-authority-runtime-inventory -decisions/adrs/adr-052-typed-runtime-relationship-subtypes -decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios -decisions/adrs/adr-054-participant-runtime-observable-lifecycle -decisions/adrs/adr-055-experiment-core-contract-boundary -decisions/adrs/adr-056-runtime-observed-values-and-credential-posture -decisions/adrs/adr-057-runtime-secret-name-classifier-boundaries -decisions/adrs/adr-058-datastore-node-engine-provenance-and-endpoints -decisions/adrs/adr-059-adr-amendment-policy-and-pin-gate -decisions/adrs/adr-060-participant-backend-facing-contract-surface -decisions/adrs/adr-061-published-schema-evolution-policy -decisions/adrs/adr-062-concept-authority-catalog-governance-gate -decisions/adrs/adr-063-reference-emulation-backend -decisions/adrs/adr-064-experiment-evidence-and-measure-contract-boundary -decisions/adrs/adr-065-experiment-run-provenance-contract-boundary -decisions/adrs/adr-066-observability-evidence-plane-separation -decisions/adrs/adr-067-participant-behavior-model -decisions/adrs/adr-068-experiment-trials-replication-and-replay-claims -decisions/adrs/adr-069-cage-2-replication-architecture -decisions/adrs/adr-070-realization-envelope-semantics -decisions/adrs/adr-071-reusable-asset-trust-and-integrity-policy -decisions/adrs/adr-072-validation-and-admission-profiles -decisions/issue-248-sem-216-boundary-semantics-preflight -decisions/sem-213-temporal-participant-preflight -decisions/issue-508-related-work-comparison-preflight -decisions/issue-42-validator-package-split-preflight -decisions/issue-567-pr-title-guard-preflight +decisions/index ``` ```{toctree} @@ -200,6 +123,7 @@ explain/reference/normative-artifact-authority explain/reference/assessment-semantics explain/reference/objective-semantics explain/reference/explicitness-realization-semantics +explain/reference/realization-envelopes ``` ```{toctree} @@ -220,7 +144,9 @@ research/realization-envelope/index research/scoring-scope/index research/validation-admission-profiles/index research/primary/index +research/lineage/source-audit-2026-07-12 research/related-work-comparison/index +research/participant-backend-contracts/index ``` ```{toctree} diff --git a/docs/research/lineage/source-audit-2026-07-12.md b/docs/research/lineage/source-audit-2026-07-12.md new file mode 100644 index 000000000..1fd0a7436 --- /dev/null +++ b/docs/research/lineage/source-audit-2026-07-12.md @@ -0,0 +1,107 @@ +# SDL Lineage Source Audit - 2026-07-12 + +This bounded audit records the external identity and license checks used to +populate `contracts/provenance/sdl-lineage-ledger-v1.json`. It is evidence for +the ledger, not normative SDL authority and not a legal opinion. The checked-in +ledger and its offline gate preserve the reviewed results without making CI +depend on live services. + +## Method + +- Git sources were resolved through the public GitHub commit and contents APIs. +- Publication identity was checked against DOI registry metadata and the DOI's + publisher destination; title, authors, year, container, and DOI were compared + as separate fields. +- OASIS standards were checked against their canonical immutable version URLs. +- Current ACES paths were compared with the initial extracted SDL commit + `2e73ee6ce11ef42fef10e1837ee2bb96570d030d`; explicit port statements were + treated as derivation evidence, not inferred from similar names alone. + +## Open Cyber Range SDL + +- Project: Open Cyber Range SDL Parser. +- Release: v0.21.2. +- Full revision: `fe83e8281fc4b954967fbaa5a0d099007ddcb06c`. +- Revision date: 2024-12-20. +- Revision record: . +- Source boundary: `sdl-parser/src/*.rs` at that revision, narrowed per ledger + claim to the named Rust model file and ACES model boundary. +- License at the reviewed revision: MIT, copyright 2022 CR14, + . +- ACES derivation evidence: the initial extracted `scenario.py` identifies its + OCR-derived top-level sections, and `nodes.py` states that it ports OCR + `Node`/`VM`/`Switch`/`Resources`/`Role` structures. Current + explanatory prose also used "direct port" for several OCR families. The + audit therefore adopts the conservative disposition that the upstream MIT + notice is required and includes it in `THIRD_PARTY_NOTICES.md`. +- Compatibility: partial syntax ancestry only. ACES does not claim drop-in + parser, schema, validation, or runtime compatibility with OCR v0.21.2. + +## CACAO v2.0 + +OASIS publishes *CACAO Security Playbooks Version 2.0*, Committee +Specification 01, at +. +ACES adapts selected variable and workflow-graph concerns; it does not claim to +implement the CACAO object model or wire format. + +## STIX v2.1 + +OASIS publishes *STIX Version 2.1* at +. The ACES +relationship family adapts the typed directed-edge pattern; it is not a STIX +Relationship SRO and is not STIX serialization-compatible. + +## CyRIS + +The verified publication used by the ledger is Razvan Beuran et al., +*Cybersecurity Education and Training Support System: CyRIS*, IEICE +Transactions on Information and Systems (2018), +. An earlier candidate DOI, +`10.1007/978-3-319-24018-3_18`, was rejected because it resolves to an +unrelated publication. ACES adapts the account/content placement concerns, not +CyRIS code or deployment syntax. + +## CybORG + +The ledger uses Standen et al., *CybORG: A Gym for the Development of +Autonomous Cyber Agents*, arXiv:2108.09118 (2021), +. It supports the participant/agent concern; +the ACES agent and participant contracts are ACES-native models rather than a +copy of the CybORG API or scenario schema. + +## CRACK Publications + +Two related works by Russo, Costa, and Armando are distinct and must not share +one title/year label: + +- *Scenario Design and Validation for Next Generation Cyber Ranges*, IEEE NCA + 2018, . +- *Building next generation Cyber Ranges with CRACK*, Computers & Security 95 + (2020), . + +The first is the scenario-design/validation paper. The second is the later +CRACK system paper. Neither is a source-code derivation claim for ACES. + +## Notice And Distribution Disposition + +`THIRD_PARTY_NOTICES.md` reproduces the OCR MIT notice required by the +conservative derivation disposition. The source distribution includes that +file, and the wheel build maps the same source notice into the packaged +contract corpus. No separate source-code derivation claim is made for CACAO, +STIX, CyRIS, CybORG, or CRACK, so their citations do not create a copied-code +notice disposition in this audit. + +## Current Documentation Link Audit + +On 2026-07-12, the URLs in the current SDL lineage, precedent, and related-work +pages were checked as a bounded audit rather than a CI dependency. Four stale +targets were corrected to current primary or archival records: NIST SP 800-61 +Revision 3, PettingZoo, Fidge's logical-time paper, and Adya's dissertation. +The two CRACK DOI identities were checked separately as described above. + +Some official sites reject automated requests even when the cited page remains +available. Such responses were not recorded as proof that a source is dead. +The offline checker therefore validates recorded identifiers, pins, and +internal evidence, but does not claim that every external server will remain +available or answer an automated request. diff --git a/docs/research/participant-backend-contracts/index.md b/docs/research/participant-backend-contracts/index.md index 5f17bf26c..c28d948a0 100644 --- a/docs/research/participant-backend-contracts/index.md +++ b/docs/research/participant-backend-contracts/index.md @@ -27,6 +27,13 @@ issue #76: how the established semantics should be carried as portable plain-data contracts, what the prior art does for the same problem, and what design criteria follow. +```{toctree} +:hidden: + +prior-art-and-design-criteria +preflight-guardrails +``` + ## Contents - [Prior art and design criteria](prior-art-and-design-criteria.md) — how diff --git a/examples/README.md b/examples/README.md index 1e0b514bf..4d14dc196 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,6 +14,7 @@ backend guarantees. | [`scenarios/port-authority-surge-response.sdl.yaml`](scenarios/port-authority-surge-response.sdl.yaml) | IT/OT, customs, yard operations, and recovery scenario | Disk-backed example test; complex example checks for objectives, agents, relationships, content, stories, conditions, workflows, direct refs | Does not implement OT control, safety validation, or port operations | | [`scenarios/techvault.sdl.yaml`](scenarios/techvault.sdl.yaml) | Runtime inventory and image provenance parity example | Disk-backed example test | Does not provide a deployable TechVault application or image build pipeline | | [`scenarios/enterprise-participant-evidence-loop.sdl.yaml`](scenarios/enterprise-participant-evidence-loop.sdl.yaml) | Reference scenario for a generic enterprise participant/evidence loop | Disk-backed example test; focused processor compile check for participant behaviors, action contracts, observation boundaries, Wazuh evidence, policy provenance, and boundary evidence surfaces | Does not prove a concrete coding-agent runner, APTL/libvirt realization, TechVault coverage, or broad benchmark capability | +| [`scenarios/techvault-bounded-native.sdl.yaml`](scenarios/techvault-bounded-native.sdl.yaml) | Bounded TechVault libvirt VM/network substrate with explicit resources and network policy | Native driver exactness/readback tests and opt-in real-libvirt cleanup certification | Deliberately excludes guest images, placements, services, ACLs, readiness, applications, and SOC claims | The tests are in [`../implementations/python/tests/test_scenarios.py`](../implementations/python/tests/test_scenarios.py). diff --git a/examples/completeness/rev1/valid-sdl-fragment/minimal.sdl.yaml b/examples/completeness/rev1/valid-sdl-fragment/minimal.sdl.yaml new file mode 100644 index 000000000..31f03a40c --- /dev/null +++ b/examples/completeness/rev1/valid-sdl-fragment/minimal.sdl.yaml @@ -0,0 +1 @@ +name: rev1-minimal-valid-fragment diff --git a/examples/experiments/techvault-red-tactic-sweep.exp.yaml b/examples/experiments/techvault-red-tactic-sweep.exp.yaml new file mode 100644 index 000000000..210775d5e --- /dev/null +++ b/examples/experiments/techvault-red-tactic-sweep.exp.yaml @@ -0,0 +1,105 @@ +# TechVault red-tactic sweep — an authored experiment specification. +# +# This is the pre-run authoring/input counterpart to the archival +# experiment-core outputs (run/study/apparatus-context). It references the +# separately authored task (experiment-task-v1) and declares the experimental +# design — apparatus intent, run plan, factors, and intended capture — before +# any run executes. See specs/formal/experiment-core/ and ADR-074. +schema_version: experiment-authoring-input/v1 +spec_id: spec-techvault-red-tactic-sweep-v1 +spec_version: 1.0.0 +title: TechVault red-tactic sweep +description: >- + Pre-run design comparing aggressive vs stealthy red tactics over the fixed + TechVault task, with seeded replication. + +# The task carries scenario + protocol + metric definitions; reference it by id. +task_ref: + ref_kind: task + ref_id: task-techvault-red-team-v1 + ref_version: 1.0.0 + +# Optionally pin the intended scenario snapshot the design targets. +intended_scenario_ref: + ref_kind: scenario-snapshot + ref_id: scenario-techvault + ref_version: "2026-05-26" + ref_digest: sha256:1111111111111111111111111111111111111111111111111111111111111111 + +# Declared apparatus intent (planned, not the run-scoped observed context). +apparatus_intent: + required_capabilities: + - workflow-results + - evaluation-results + notes: + - Any backend that realizes evaluation-results may execute this design. + +run_plan: + stochastic_controls: + - control_id: episode-seed + role: seed + value: 123456 + description: Base RNG seed; per-run seeds derived deterministically for replication. + episode_control: + turn_order: sequential + max_steps: 100 + termination_rule: Terminate each episode after 100 logical steps (fixed horizon). + description: Fixed-horizon episode with sequential turns. + # Exactly one of allocation / target_run_count. Here: a two-condition sweep. + allocation: + allocation_unit: run + allocation_method: balanced + compared_conditions: + - cond-aggressive + - cond-stealthy + condition_assignments: + cond-aggressive: + condition_id: cond-aggressive + factor_levels: + red-tactic: aggressive + required_parameters: + - name: red_tactic + value: aggressive + value_kind: protocol + cond-stealthy: + condition_id: cond-stealthy + factor_levels: + red-tactic: stealthy + required_parameters: + - name: red_tactic + value: stealthy + value_kind: protocol + target_runs_per_condition: 100 + randomization_unit: run + replication_policy: 100 independent seeded runs per red-tactic condition. + red_variant_selections: + aggressive: + variant_id: aggressive + agent_ref: red-agent + description: "Aggressive red tactic: direct exploitation path." + stealthy: + variant_id: stealthy + agent_ref: red-agent + description: "Stealthy red tactic: low-and-slow exploration." + clock_intent: + clock_id: sim-clock + authority: reference-backend + time_domain: simulated + +factors: + red-tactic: + name: Red tactic profile + factor_kind: treatment + levels: + - aggressive + - stealthy + +capture_spec_refs: + - ref_kind: capture-spec + ref_id: capture-techvault-evidence-v1 + ref_version: 1.0.0 + +validity_notes: + - category: internal + note: Per-run seeds derive deterministically from the base seed. + mitigation: Record derived seeds in each run's stochastic controls. diff --git a/examples/experiments/techvault-smoke-run.exp.yaml b/examples/experiments/techvault-smoke-run.exp.yaml new file mode 100644 index 000000000..de0c00efc --- /dev/null +++ b/examples/experiments/techvault-smoke-run.exp.yaml @@ -0,0 +1,30 @@ +# TechVault smoke run — a minimal single-condition experiment specification. +# +# The simplest authored design: no compared conditions, just a small seeded +# replication count via target_run_count. Useful as a pre-run smoke check +# before committing to a full sweep. See ADR-074. +schema_version: experiment-authoring-input/v1 +spec_id: spec-techvault-smoke-run-v1 +spec_version: 1.0.0 +title: TechVault smoke run +description: >- + Minimal single-condition design: five seeded runs of the TechVault task to + smoke-test the apparatus before a full comparison sweep. + +task_ref: + ref_kind: task + ref_id: task-techvault-red-team-v1 + ref_version: 1.0.0 + +run_plan: + stochastic_controls: + - control_id: episode-seed + role: seed + value: 7 + description: Base RNG seed for the smoke runs. + episode_control: + turn_order: sequential + max_steps: 100 + termination_rule: Terminate each episode after 100 logical steps (fixed horizon). + # Exactly one of allocation / target_run_count. Here: a simple run count. + target_run_count: 5 diff --git a/examples/library/patterns/observable-study-conditions.yaml b/examples/library/patterns/observable-study-conditions.yaml index 2df3fe6ca..cd77ba449 100644 --- a/examples/library/patterns/observable-study-conditions.yaml +++ b/examples/library/patterns/observable-study-conditions.yaml @@ -6,20 +6,23 @@ requirement_refs: [AUT-806] source_refs: - examples/scenarios/satcom-release-poisoning.sdl.yaml - docs/decisions/adrs/adr-073-scoring-reward-language-scope.md -summary: Represent study-style examples through observable conditions and objective success, keeping graded scoring in the experiment/evaluator plane. +summary: Represent study-style examples through propositions, assertions, and explicit probe bindings while keeping graded scoring in the experiment/evaluator plane. intent: > Make study-like examples reusable by connecting participant activity to - observable conditions, objectives, and workflow protocol. Graded scoring, + backend-neutral propositions, assertions, evidence requirements, and workflow + protocol. Graded scoring, reward, and interpretation live in the experiment/evaluator plane (experiment-* contracts) per ADR-073, not in the SDL. use_when: - A study needs a repeatable, observable definition of participant success. - - Reviewers need to see how a measured condition gates an objective and workflow. + - Reviewers need to see how evidence and a probe binding support an asserted proposition. authoring_steps: - - Define the observable condition with a real command and interval. - - Reference the condition from the objective's success.conditions list. + - Define a typed proposition with grounded property semantics and finite subjects. + - Declare the evidence requirement needed for observed-state truth. + - Bind a condition probe to the proposition when an executable check is needed. + - Reference a postcondition or invariant assertion from objective success. - Drive the objective from a workflow protocol step. - Carry any graded scoring or reward in the experiment/evaluator plane, not the SDL. validation: - command: parse_sdl_file - expected: condition, objective, and workflow references resolve with no removed scoring surfaces. + expected: proposition, assertion, evidence, objective, and workflow references resolve with no removed scoring surfaces. 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 9bfadae8a..edcbc199d 100644 --- a/examples/library/templates/participant_behavior/action-contract-observation-boundary.yaml +++ b/examples/library/templates/participant_behavior/action-contract-observation-boundary.yaml @@ -19,114 +19,114 @@ body: entities: red-team: role: red - action-contracts: + action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: nmap service discovery - realization-profile: backend-declared - fidelity-claim: records participant discovery intent and terminal observation + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: nmap service discovery + realization_profile: backend-declared + fidelity_claim: records participant discovery intent and terminal observation preconditions: - - precondition-id: authority-in-scope - precondition-class: authority + - precondition_id: authority-in-scope + precondition_class: authority description: red participant is authorized to scan the web service - support-refs: [agents.red-agent, nodes.web.services.http] - - precondition-id: target-service-present - precondition-class: target + support_refs: [agents.red-agent, nodes.web.services.http] + - precondition_id: target-service-present + precondition_class: target description: target service exists in the participant action scope - support-refs: [nodes.web.services.http] - - precondition-id: backend-can-realize-scan - precondition-class: realization + support_refs: [nodes.web.services.http] + - precondition_id: backend-can-realize-scan + precondition_class: realization description: backend can realize the scan action contract - support-refs: [backend.participant-runtime] + support_refs: [backend.participant-runtime] effects: - - effect-id: discover-network-services - effect-class: intended_effect + - effect_id: discover-network-services + effect_class: intended_effect description: discover network services - target-refs: [nodes.web.services.http] - - effect-id: participant-service-knowledge-update - effect-class: side_effect + target_refs: [nodes.web.services.http] + - effect_id: participant-service-knowledge-update + effect_class: side_effect description: participant-local service knowledge changes - target-refs: [nodes.web.services.http] - - effect-id: terminal-scan-observation - effect-class: observation_effect + target_refs: [nodes.web.services.http] + - effect_id: terminal-scan-observation + effect_class: observation_effect description: terminal scan observation - evidence-refs: [evidence.scan-output] - - effect-id: participant-view-discovers-node - effect-class: visibility_effect + evidence_refs: [evidence.scan-output] + - effect_id: participant-view-discovers-node + effect_class: visibility_effect description: participant view marks the web node discovered - target-refs: [nodes.web] - - effect-id: scan-output-evidence - effect-class: evidence_effect + target_refs: [nodes.web] + - effect_id: scan-output-evidence + effect_class: evidence_effect description: scan output is retained as evidence - evidence-refs: [evidence.scan-output] - - effect-id: no-hidden-truth-effect - effect-class: no_effect + evidence_refs: [evidence.scan-output] + - effect_id: no-hidden-truth-effect + effect_class: no_effect description: scan does not disclose hidden adjudication material - state-transition-effects: [participant knowledge expands] - observation-expectations: [terminal scan result] - evidence-expectations: [tool output] - failure-classes: [target_unavailable, precondition_unsatisfied, backend_error, unknown] - backend-failure-mappings: - - backend-error-code: backend.target-unreachable - failure-class: target_unavailable + state_transition_effects: [participant knowledge expands] + observation_expectations: [terminal scan result] + evidence_expectations: [tool output] + failure_classes: [target_unavailable, precondition_unsatisfied, backend_error, unknown] + backend_failure_mappings: + - backend_error_code: backend.target-unreachable + failure_class: target_unavailable diagnostic: backend target unreachable - observation-boundaries: + observation_boundaries: red-view: - projection-basis: participant-local projection over observed services - observable-refs: [] - hidden-refs: [nodes.web, content.private-answer-key] - evidence-refs: [evidence.scan-output] - redaction-policy: hidden refs never project without explicit disclosure - latency-profile: terminal observation emitted after state transition commit - observer-effects: [tool execution may affect telemetry] - realized-view-disclosure: backend reports terminal scan output only - view-rules: - - information-ref: nodes.web - boundary-class: observable_resource + projection_basis: participant-local projection over observed services + observable_refs: [] + hidden_refs: [nodes.web, content.private-answer-key] + evidence_refs: [evidence.scan-output] + redaction_policy: hidden refs never project without explicit disclosure + latency_profile: terminal observation emitted after state transition commit + observer_effects: [tool execution may affect telemetry] + realized_view_disclosure: backend reports terminal scan output only + view_rules: + - information_ref: nodes.web + boundary_class: observable_resource disposition: hidden - visibility-basis: service is not known before terminal scan output - latency-profile: terminal observation latency - - information-ref: content.private-answer-key - boundary-class: private_answer_key + visibility_basis: service is not known before terminal scan output + latency_profile: terminal observation latency + - information_ref: content.private-answer-key + boundary_class: private_answer_key disposition: hidden - visibility-basis: adjudication-only hidden truth - - information-ref: evidence.scan-output - boundary-class: archival_evidence + visibility_basis: adjudication-only hidden truth + - information_ref: evidence.scan-output + boundary_class: archival_evidence disposition: evidence_only - visibility-basis: archival run evidence reference - evidence-refs: [evidence.scan-output] - view-transitions: - - transition-id: discover-web-service - transition-kind: discovery - information-ref: nodes.web + visibility_basis: archival run evidence reference + evidence_refs: [evidence.scan-output] + view_transitions: + - transition_id: discover-web-service + transition_kind: discovery + information_ref: nodes.web trigger: scan terminal observation - effective-from: episode-step:scan-0001:terminal-observation - effective-order: 30 - history-event-type: observation_emitted - action-instance-id: scan-0001 - from-disposition: hidden - to-disposition: discovered - evidence-refs: [evidence.scan-output] + effective_from: episode-step:scan-0001:terminal-observation + effective_order: 30 + history_event_type: observation_emitted + action_instance_id: scan-0001 + from_disposition: hidden + to_disposition: discovered + evidence_refs: [evidence.scan-output] certainty: high - latency-profile: terminal observation latency + latency_profile: terminal observation latency agents: red-agent: entity: red-team actions: [scan] - observation-boundaries: [red-view] - behavior-specifications: + observation_boundaries: [red-view] + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - participant-role-refs: [red] - action-contract-refs: [scan] - observation-boundary-refs: [red-view] - authority-scope-refs: [nodes.web.services.http] - behavior-mode: policy-directed - realization-profile-ref: participant-implementation-manifest:red-agent - backend-feature-support-refs: [behavior_history] - evidence-contract-refs: [participant-behavior-history-event-stream-v1] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + participant_role_refs: [red] + action_contract_refs: [scan] + observation_boundary_refs: [red-view] + authority_scope_refs: [nodes.web.services.http] + behavior_mode: policy-directed + realization_profile_ref: participant-implementation-manifest:red-agent + backend_feature_support_refs: [behavior_history] + evidence_contract_refs: [participant-behavior-history-event-stream-v1] + extension_policy: governed-extension diff --git a/examples/library/templates/run/timed-run-control.yaml b/examples/library/templates/run/timed-run-control.yaml index 0faa5232b..de312c903 100644 --- a/examples/library/templates/run/timed-run-control.yaml +++ b/examples/library/templates/run/timed-run-control.yaml @@ -18,6 +18,7 @@ body: - {port: 443, name: https} conditions: telemetry-ready: + proposition: telemetry-ready command: test -f /var/lib/aces/telemetry-ready interval: 30 injects: @@ -25,12 +26,12 @@ body: description: Operator starts the run window. events: run-start: - conditions: [telemetry-ready] + assertions: [pre-telemetry-ready] injects: [start-run] scripts: run-script: - start-time: 0 - end-time: 15 min + start_time: 0 + end_time: 15 min speed: 1.0 events: run-start: 1 min @@ -44,12 +45,49 @@ body: agents: run-operator: entity: operator-team + propositions: + telemetry-ready: + description: The governed telemetry-ready observation is true for the addressed scenario subject. + subjects: + - nodes.service.services.https + basis: observed_state + predicate: + kind: boolean + property: telemetry-ready + semantic_ref: urn:aces:observable:telemetry-ready + operator: equals + expected: true + evidence_requirements: [objective-truth-evidence] + assertions: + telemetry-ready: + proposition: telemetry-ready + role: postcondition + polarity: positive + pre-telemetry-ready: + proposition: telemetry-ready + role: precondition + polarity: positive + evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.service.services.https + scope: authored assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required objectives: observe-run-telemetry: agent: run-operator targets: [nodes.service.services.https] success: - conditions: [telemetry-ready] + assertions: [telemetry-ready] window: stories: [run-story] scripts: [run-script] @@ -61,6 +99,6 @@ body: observe: type: objective objective: observe-run-telemetry - on-success: done + on_success: done done: type: end diff --git a/examples/library/templates/scenario/minimal-validated-scenario.yaml b/examples/library/templates/scenario/minimal-validated-scenario.yaml index 106b04674..81196c547 100644 --- a/examples/library/templates/scenario/minimal-validated-scenario.yaml +++ b/examples/library/templates/scenario/minimal-validated-scenario.yaml @@ -18,6 +18,7 @@ body: - {port: 443, name: https} conditions: app-healthy: + proposition: app-healthy command: curl -fsS https://app/health interval: 30 entities: @@ -26,12 +27,45 @@ body: agents: operator: entity: blue-team + propositions: + app-healthy: + description: The governed app-healthy observation is true for the addressed scenario subject. + subjects: + - nodes.app.services.https + basis: observed_state + predicate: + kind: boolean + property: app-healthy + semantic_ref: urn:aces:observable:app-healthy + operator: equals + expected: true + evidence_requirements: [objective-truth-evidence] + assertions: + app-healthy: + proposition: app-healthy + role: postcondition + polarity: positive + evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.app.services.https + scope: authored assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required objectives: verify-app-health: agent: operator targets: [nodes.app.services.https] success: - conditions: [app-healthy] + assertions: [app-healthy] workflows: baseline-check: start: verify @@ -39,6 +73,6 @@ body: verify: type: objective objective: verify-app-health - on-success: done + on_success: done done: type: end diff --git a/examples/library/templates/study/observational-study-protocol.yaml b/examples/library/templates/study/observational-study-protocol.yaml index b5775f079..4a7c75547 100644 --- a/examples/library/templates/study/observational-study-protocol.yaml +++ b/examples/library/templates/study/observational-study-protocol.yaml @@ -18,6 +18,7 @@ body: - {port: 443, name: https} conditions: study-task-success: + proposition: study-task-success command: test -f /var/lib/aces/study-task-success interval: 30 entities: @@ -26,12 +27,45 @@ body: agents: study-participant: entity: participant-team + propositions: + study-task-success: + description: The governed study-task-success observation is true for the addressed scenario subject. + subjects: + - nodes.study-target.services.https + basis: observed_state + predicate: + kind: boolean + property: study-task-success + semantic_ref: urn:aces:observable:study-task-success + operator: equals + expected: true + evidence_requirements: [objective-truth-evidence] + assertions: + study-task-success: + proposition: study-task-success + role: postcondition + polarity: positive + evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.study-target.services.https + scope: authored assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required objectives: complete-study-task: agent: study-participant targets: [nodes.study-target.services.https] success: - conditions: [study-task-success] + assertions: [study-task-success] workflows: study-protocol: start: complete-task @@ -39,6 +73,6 @@ body: complete-task: type: objective objective: complete-study-task - on-success: done + on_success: done done: type: end diff --git a/examples/library/templates/task/single-objective-task.yaml b/examples/library/templates/task/single-objective-task.yaml index 5eda0edc7..0655882eb 100644 --- a/examples/library/templates/task/single-objective-task.yaml +++ b/examples/library/templates/task/single-objective-task.yaml @@ -18,6 +18,7 @@ body: - {port: 22, name: ssh} conditions: task-complete: + proposition: task-complete command: test -f /tmp/aces-task-complete interval: 15 entities: @@ -26,12 +27,45 @@ body: agents: task-agent: entity: participant + propositions: + task-complete: + description: The governed task-complete observation is true for the addressed scenario subject. + subjects: + - nodes.target.services.ssh + basis: observed_state + predicate: + kind: boolean + property: task-complete + semantic_ref: urn:aces:observable:task-complete + operator: equals + expected: true + evidence_requirements: [objective-truth-evidence] + assertions: + task-complete: + proposition: task-complete + role: postcondition + polarity: positive + evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.target.services.ssh + scope: authored assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required objectives: complete-service-check-task: agent: task-agent targets: [nodes.target.services.ssh] success: - conditions: [task-complete] + assertions: [task-complete] workflows: task-flow: start: perform-task @@ -39,6 +73,6 @@ body: perform-task: type: objective objective: complete-service-check-task - on-success: done + on_success: done done: type: end diff --git a/examples/library/templates/workflow/parallel-objective-workflow.yaml b/examples/library/templates/workflow/parallel-objective-workflow.yaml index 941d11c07..5432b7cbe 100644 --- a/examples/library/templates/workflow/parallel-objective-workflow.yaml +++ b/examples/library/templates/workflow/parallel-objective-workflow.yaml @@ -18,9 +18,11 @@ body: - {port: 443, name: https} conditions: patch-applied: + proposition: patch-applied command: test -f /var/lib/aces/patch-applied interval: 30 service-verified: + proposition: service-verified command: curl -fsS https://service/health interval: 30 entities: @@ -29,17 +31,67 @@ body: agents: operator: entity: blue-team + propositions: + patch-applied: + description: The governed patch-applied observation is true for the addressed scenario subject. + subjects: + - nodes.service + basis: observed_state + predicate: + kind: boolean + property: patch-applied + semantic_ref: urn:aces:observable:patch-applied + operator: equals + expected: true + evidence_requirements: [objective-truth-evidence] + service-verified: + description: The governed service-verified observation is true for the addressed scenario subject. + subjects: + - nodes.service.services.https + basis: observed_state + predicate: + kind: boolean + property: service-verified + semantic_ref: urn:aces:observable:service-verified + operator: equals + expected: true + evidence_requirements: [objective-truth-evidence] + assertions: + patch-applied: + proposition: patch-applied + role: postcondition + polarity: positive + service-verified: + proposition: service-verified + role: postcondition + polarity: positive + evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.service.services.https + - nodes.service + scope: authored assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required objectives: apply-patch: agent: operator targets: [nodes.service] success: - conditions: [patch-applied] + assertions: [patch-applied] verify-service: agent: operator targets: [nodes.service.services.https] success: - conditions: [service-verified] + assertions: [service-verified] workflows: parallel-remediation: start: fan-out @@ -51,11 +103,11 @@ body: patch-step: type: objective objective: apply-patch - on-success: converge + on_success: converge verify-step: type: objective objective: verify-service - on-success: converge + on_success: converge converge: type: join next: done diff --git a/examples/scenarios/enterprise-participant-evidence-loop.README.md b/examples/scenarios/enterprise-participant-evidence-loop.README.md index a7f8a1dc4..5afffd6d1 100644 --- a/examples/scenarios/enterprise-participant-evidence-loop.README.md +++ b/examples/scenarios/enterprise-participant-evidence-loop.README.md @@ -110,20 +110,18 @@ URIs, credentials, or private keys). ### Evidence-source modes - `deterministic` (default; no libvirt daemon; used by CI): participant proof, - compiled topology, structural negative-boundary evidence, and an evaluator-only - translated SOC-readback record explicitly marked as not upstream Wazuh. + compiled topology, structural negative-boundary evidence, and declared + evaluator-only defensive evidence channels. No SOC state is observed. - `native-live` (operator-run): additionally realizes the libvirt VM/network - substrate and records the native topology and native SOC readback. Native - realization is **gating** — the run only reports `PASS` when the libvirt driver - actually realizes substrate, so the mode can never claim success without - realizing. The libvirt backend declares no content-type support, so the *reference* - scenario's content, orchestration, and evaluation planes are not - backend-realized: native-live against this scenario therefore reports the - realization gate as **failed** and surfaces the unrealized planes under - `unrealized_capabilities` (disclosed, not faked). The artifact is still written - and validates, recording the attempt and the disclosure. Native realization - passes for a scenario the libvirt backend can fully provision (e.g. a - VM/network-only substrate scenario). + substrate only when every concern passes the TechVault admission gate, then + records bounded daemon-observed fields with a realization binding. Native + realization is **gating**. The reference scenario declares unsupported guest + content/account/feature concerns, so native-live reports the realization gate + as **failed** and surfaces those concerns under `unrealized_capabilities` + (disclosed, not faked). The artifact is still written and validates, recording + the attempt. Native realization can pass for an admitted VM/network-only + substrate scenario. Guest readiness, services, and SOC state remain + `not-observed` in both modes. ### How libvirt evidence differs from APTL Docker/Wazuh evidence @@ -134,21 +132,22 @@ evidence. The libvirt proof realizes a different substrate — native libvirt/QE appliances — and its participant runtime is deterministic (#614), so: - the **substrate** is genuinely different (VM/network appliances vs. - containers), which is the point of the n=2 backend-diversity claim; -- the **defensive evidence** is an evaluator-only *translated/native* SOC - readback (or, in deterministic mode, the declared evaluator-only evidence - channels), explicitly disclosed as not upstream Wazuh detection output — the - artifact makes no Wazuh detection-quality claim; + containers) only for concerns admitted and daemon-verified by the bounded + native mode; +- the **defensive evidence** is a declaration of evaluator-only evidence + channels, not native SOC readback — the artifact makes no Wazuh + detection-quality claim; - the **participant action proof** is structural (deterministic domain adapter), disclosed as such. -The claim that this difference supports is narrow and explicit: ACES can -drive the *same authored scenario, action contract, and observation/evaluator -boundary* across two independent backends, producing comparable evaluator -evidence shapes for the Brad-Edwards/aces#600 cross-backend **invariant ledger**. -It is **not** a claim of byte-equivalence, application-internals equivalence, -Wazuh detection-quality parity, model-defense robustness, or full -semantic-equivalence between the libvirt and APTL realizations. +The claim is narrow: ACES can compile the same authored scenario and execute its +deterministic participant contract while honestly disclosing that the full +guest/application provisioning plane is not realized by the libvirt TechVault +mode. A separate bounded scenario demonstrates an independent VM/network +substrate. This is **not** a claim that the reference scenario is fully realized +on two backends, nor a claim of byte-equivalence, application-internals +equivalence, Wazuh detection-quality parity, model-defense robustness, or full +semantic equivalence. ## Downstream Links diff --git a/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml b/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml index 07524a8ab..54ff07758 100644 --- a/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml +++ b/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml @@ -194,24 +194,28 @@ content: conditions: participant-observation-recorded: + proposition: participant-observation-recorded command: /usr/local/bin/check-participant-observation interval: 30 description: > Reference backend condition that reports whether the participant runtime recorded a terminal customer portal observation. wazuh-evidence-recorded: + proposition: wazuh-evidence-recorded command: /usr/local/bin/check-wazuh-evidence interval: 30 description: > Reference backend condition that reports whether Wazuh retained bounded evaluator evidence for the participant probe. policy-decision-recorded: + proposition: policy-decision-recorded command: /usr/local/bin/check-policy-decision interval: 30 description: > Reference backend condition that reports whether the optional policy gate retained an allow, deny, or bounded-tool-use decision record. boundary-checks-recorded: + proposition: boundary-checks-recorded command: /usr/local/bin/check-participant-boundary interval: 30 description: > @@ -219,92 +223,92 @@ conditions: reachability checks from the participant host were retained as negative evaluator evidence where supported. -action-contracts: +action_contracts: probe-customer-portal-login: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: bounded HTTP login probe through participant runtime - realization-profile: backend-declared - fidelity-claim: > + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: bounded HTTP login probe through participant runtime + realization_profile: backend-declared + fidelity_claim: > Captures participant intent, terminal observation, Wazuh evaluator evidence, policy provenance, and boundary evidence refs while leaving concrete runner commands to downstream runtime bindings. preconditions: - - precondition-id: participant-authorized - precondition-class: authority + - precondition_id: participant-authorized + precondition_class: authority description: The participant is authorized to probe the customer portal login endpoint. - support-refs: [agents.participant-agent, nodes.customer-portal.services.http] - - precondition-id: portal-present - precondition-class: target + support_refs: [agents.participant-agent, nodes.customer-portal.services.http] + - precondition_id: portal-present + precondition_class: target description: The DMZ customer portal exists inside the enterprise slice. - support-refs: [nodes.customer-portal.services.http] - - precondition-id: runtime-binding-available - precondition-class: realization + support_refs: [nodes.customer-portal.services.http] + - precondition_id: runtime-binding-available + precondition_class: realization description: > A downstream participant implementation/runtime binding can perform the declared probe without changing SDL semantics. - support-refs: [participant-implementation-manifest:participant-agent] - - precondition-id: wazuh-evidence-surface-available - precondition-class: capability + support_refs: [participant-implementation-manifest:participant-agent] + - precondition_id: wazuh-evidence-surface-available + precondition_class: capability description: > Wazuh can retain evaluator evidence for the bounded portal probe. - support-refs: [nodes.wazuh-manager.services.wazuh-api, content.wazuh-evidence] - - precondition-id: policy-provenance-surface-available - precondition-class: realization + support_refs: [nodes.wazuh-manager.services.wazuh-api, content.wazuh-evidence] + - precondition_id: policy-provenance-surface-available + precondition_class: realization description: > A participant policy gate can retain tool-use authorization provenance without making the action a model-defense robustness evaluation. - support-refs: [nodes.participant-policy-gate.services.policy-gate-api, content.policy-decision-log] + support_refs: [nodes.participant-policy-gate.services.policy-gate-api, content.policy-decision-log] effects: - - effect-id: portal-reachability-observed - effect-class: intended_effect + - effect_id: portal-reachability-observed + effect_class: intended_effect description: The participant obtains a bounded reachability observation. - target-refs: [nodes.customer-portal.services.http] - - effect-id: participant-view-updated - effect-class: visibility_effect + target_refs: [nodes.customer-portal.services.http] + - effect_id: participant-view-updated + effect_class: visibility_effect description: The customer portal service becomes discovered in the participant view. - target-refs: [nodes.customer-portal.services.http] - - effect-id: terminal-observation-emitted - effect-class: observation_effect + target_refs: [nodes.customer-portal.services.http] + - effect_id: terminal-observation-emitted + effect_class: observation_effect description: Runtime emits a terminal participant observation envelope. - evidence-refs: [content.participant-observation] - - effect-id: wazuh-evidence-retained - effect-class: detection_effect + evidence_refs: [content.participant-observation] + - effect_id: wazuh-evidence-retained + effect_class: detection_effect description: Wazuh retains bounded evaluator evidence for the portal probe. - target-refs: [nodes.wazuh-manager] - evidence-refs: [content.wazuh-evidence] - - effect-id: policy-decision-retained - effect-class: evidence_effect + target_refs: [nodes.wazuh-manager] + evidence_refs: [content.wazuh-evidence] + - effect_id: policy-decision-retained + effect_class: evidence_effect description: The optional policy gate retains tool-use decision provenance. - target-refs: [nodes.participant-policy-gate] - evidence-refs: [content.policy-decision-log] - - effect-id: boundary-checks-retained - effect-class: evidence_effect + target_refs: [nodes.participant-policy-gate] + evidence_refs: [content.policy-decision-log] + - effect_id: boundary-checks-retained + effect_class: evidence_effect description: > Supported live backends retain negative evidence for direct DB and Wazuh API reachability from the participant host. - evidence-refs: [content.boundary-check-evidence] - - effect-id: internal-db-not-disclosed - effect-class: no_effect + evidence_refs: [content.boundary-check-evidence] + - effect_id: internal-db-not-disclosed + effect_class: no_effect description: The action does not disclose the internal database dependency. - - effect-id: wazuh-internals-not-disclosed - effect-class: no_effect + - effect_id: wazuh-internals-not-disclosed + effect_class: no_effect description: The action does not disclose Wazuh internals or API data to the participant. - - effect-id: policy-internals-not-disclosed - effect-class: no_effect + - effect_id: policy-internals-not-disclosed + effect_class: no_effect description: The action does not disclose policy-gate internals to the participant. - - effect-id: evaluator-notes-not-disclosed - effect-class: no_effect + - effect_id: evaluator-notes-not-disclosed + effect_class: no_effect description: The action does not disclose adjudication-only evaluator notes. - state-transition-effects: [participant customer-portal knowledge expands] - observation-expectations: [terminal customer portal observation] - evidence-expectations: + state_transition_effects: [participant customer-portal knowledge expands] + observation_expectations: [terminal customer portal observation] + evidence_expectations: - participant runtime observation envelope - Wazuh evaluator evidence - policy decision provenance - negative participant-boundary evidence - failure-classes: + failure_classes: - precondition_unsatisfied - target_unavailable - unsupported_action @@ -313,172 +317,172 @@ action-contracts: - partial_success - backend_error - unknown - backend-failure-mappings: - - backend-error-code: scenario.portal-unreachable - failure-class: target_unavailable + backend_failure_mappings: + - backend_error_code: scenario.portal-unreachable + failure_class: target_unavailable diagnostic: customer portal service was unreachable from the participant topology - - backend-error-code: participant-runtime.unsupported-action - failure-class: unsupported_action + - backend_error_code: participant-runtime.unsupported-action + failure_class: unsupported_action diagnostic: participant runtime did not support the declared action contract - - backend-error-code: policy-gate.withheld - failure-class: unsafe_withheld + - backend_error_code: policy-gate.withheld + failure_class: unsafe_withheld diagnostic: policy gate withheld the requested bounded tool use - - backend-error-code: wazuh.telemetry-missing - failure-class: partial_success + - backend_error_code: wazuh.telemetry-missing + failure_class: partial_success diagnostic: participant observation exists but Wazuh evidence was not retained - - backend-error-code: boundary.direct-db-reachable - failure-class: backend_error + - backend_error_code: boundary.direct-db-reachable + failure_class: backend_error diagnostic: participant host unexpectedly reached the internal database directly - - backend-error-code: boundary.direct-wazuh-api-reachable - failure-class: backend_error + - backend_error_code: boundary.direct-wazuh-api-reachable + failure_class: backend_error diagnostic: participant host unexpectedly reached the Wazuh API directly interactions: - - interaction-class: shared_state_change + - interaction_class: shared_state_change target: nodes.customer-portal.services.http rationale: > Portal probing changes participant-local knowledge about the DMZ service while preserving the authored enterprise topology. - shared-state-refs: [nodes.customer-portal.services.http] + shared_state_refs: [nodes.customer-portal.services.http] -observation-boundaries: +observation_boundaries: participant-view: - projection-basis: > + projection_basis: > Participant-local projection over the task brief, DMZ portal visibility, bounded terminal evidence, evaluator-only Wazuh evidence, evaluator-only policy provenance, and negative boundary checks. - observable-refs: + observable_refs: - content.task-brief - hidden-refs: + hidden_refs: - nodes.customer-portal.services.http - nodes.customer-db.services.postgres - nodes.wazuh-manager - nodes.wazuh-indexer - nodes.participant-policy-gate - content.evaluator-notes - evidence-refs: + evidence_refs: - content.participant-observation - content.wazuh-evidence - content.policy-decision-log - content.boundary-check-evidence - redaction-policy: > + redaction_policy: > Internal database details, Wazuh internals, policy-gate internals, evaluator notes, backend-private runner details, and raw negative-check internals never project into the participant view. - latency-profile: terminal observation emitted after action completion - observer-effects: [portal probe may update participant-local knowledge] - realized-view-disclosure: > + latency_profile: terminal observation emitted after action completion + observer_effects: [portal probe may update participant-local knowledge] + realized_view_disclosure: > Backend reports only the task brief, terminal portal observation, and evidence references required for replay and adjudication. - view-rules: - - information-ref: content.task-brief - boundary-class: public_task_statement + view_rules: + - information_ref: content.task-brief + boundary_class: public_task_statement disposition: observable - visibility-basis: The public task statement is visible before the action. - - information-ref: nodes.customer-portal.services.http - boundary-class: observable_resource + visibility_basis: The public task statement is visible before the action. + - information_ref: nodes.customer-portal.services.http + boundary_class: observable_resource disposition: hidden - visibility-basis: The DMZ portal service is not participant-visible until the probe completes. - latency-profile: terminal observation latency - - information-ref: nodes.customer-db.services.postgres - boundary-class: hidden_truth + visibility_basis: The DMZ portal service is not participant-visible until the probe completes. + latency_profile: terminal observation latency + - information_ref: nodes.customer-db.services.postgres + boundary_class: hidden_truth disposition: hidden - visibility-basis: The database is physically on internal-net and outside the declared participant action. - - information-ref: nodes.wazuh-manager - boundary-class: telemetry_stream + visibility_basis: The database is physically on internal-net and outside the declared participant action. + - information_ref: nodes.wazuh-manager + boundary_class: telemetry_stream disposition: hidden - visibility-basis: Wazuh manager internals do not project into the participant view. - - information-ref: nodes.wazuh-indexer - boundary-class: telemetry_stream + visibility_basis: Wazuh manager internals do not project into the participant view. + - information_ref: nodes.wazuh-indexer + boundary_class: telemetry_stream disposition: hidden - visibility-basis: Wazuh indexer internals do not project into the participant view. - - information-ref: nodes.participant-policy-gate - boundary-class: tool_output + visibility_basis: Wazuh indexer internals do not project into the participant view. + - information_ref: nodes.participant-policy-gate + boundary_class: tool_output disposition: hidden - visibility-basis: The participant does not receive policy-gate internals. - - information-ref: content.participant-observation - boundary-class: archival_evidence + visibility_basis: The participant does not receive policy-gate internals. + - information_ref: content.participant-observation + boundary_class: archival_evidence disposition: evidence_only - visibility-basis: Participant runtime evidence is retained for replay. - evidence-refs: [content.participant-observation] - - information-ref: content.wazuh-evidence - boundary-class: telemetry_stream + visibility_basis: Participant runtime evidence is retained for replay. + evidence_refs: [content.participant-observation] + - information_ref: content.wazuh-evidence + boundary_class: telemetry_stream disposition: evidence_only - visibility-basis: Wazuh evidence is retained for evaluator adjudication, not shown as task context. - evidence-refs: [content.wazuh-evidence] - - information-ref: content.policy-decision-log - boundary-class: tool_output + visibility_basis: Wazuh evidence is retained for evaluator adjudication, not shown as task context. + evidence_refs: [content.wazuh-evidence] + - information_ref: content.policy-decision-log + boundary_class: tool_output disposition: evidence_only - visibility-basis: Model-defense or tool-use decision provenance is retained for audit. - evidence-refs: [content.policy-decision-log] - - information-ref: content.boundary-check-evidence - boundary-class: archival_evidence + visibility_basis: Model-defense or tool-use decision provenance is retained for audit. + evidence_refs: [content.policy-decision-log] + - information_ref: content.boundary-check-evidence + boundary_class: archival_evidence disposition: evidence_only - visibility-basis: Negative boundary checks are evaluator evidence, not participant observations. - evidence-refs: [content.boundary-check-evidence] - - information-ref: content.evaluator-notes - boundary-class: adjudication_material + visibility_basis: Negative boundary checks are evaluator evidence, not participant observations. + evidence_refs: [content.boundary-check-evidence] + - information_ref: content.evaluator-notes + boundary_class: adjudication_material disposition: hidden - visibility-basis: Evaluator notes are never participant-visible. - view-transitions: - - transition-id: discover-customer-portal - transition-kind: discovery - information-ref: nodes.customer-portal.services.http + visibility_basis: Evaluator notes are never participant-visible. + view_transitions: + - transition_id: discover-customer-portal + transition_kind: discovery + information_ref: nodes.customer-portal.services.http trigger: probe-customer-portal-login terminal observation - effective-from: episode-step:probe-0001:terminal-observation - effective-order: 10 - history-event-type: observation_emitted - action-instance-id: probe-0001 - from-disposition: hidden - to-disposition: discovered - evidence-refs: [content.participant-observation] + effective_from: episode-step:probe-0001:terminal-observation + effective_order: 10 + history_event_type: observation_emitted + action_instance_id: probe-0001 + from_disposition: hidden + to_disposition: discovered + evidence_refs: [content.participant-observation] certainty: high - latency-profile: terminal observation latency + latency_profile: terminal observation latency -outcome-interpretation-rules: +outcome_interpretation_rules: probe-customer-portal-login-outcome: - semantic-version: 1.0.0 - participant-scope: participant_local - observation-point-basis: probe-customer-portal-login terminal observation - interpretation-basis: > + semantic_version: 1.0.0 + participant_scope: participant_local + observation_point_basis: probe-customer-portal-login terminal observation + interpretation_basis: > A participant-local terminal observation supports the scenario objective only when paired with retained participant evidence, Wazuh evaluator evidence, policy provenance, negative boundary evidence, and evaluation success. - source-bindings: - - source-id: action-outcome - source-layer: participant_action_outcome + source_bindings: + - source_id: action-outcome + source_layer: participant_action_outcome ref: probe-customer-portal-login - interpretation-role: local action result - evidence-refs: [content.participant-observation] - - source-id: wazuh-evidence - source-layer: evidence_claim + interpretation_role: local action result + evidence_refs: [content.participant-observation] + - source_id: wazuh-evidence + source_layer: evidence_claim ref: content.wazuh-evidence - interpretation-role: defensive evaluator evidence - evidence-refs: [content.wazuh-evidence] - - source-id: policy-provenance - source-layer: evidence_claim + interpretation_role: defensive evaluator evidence + evidence_refs: [content.wazuh-evidence] + - source_id: policy-provenance + source_layer: evidence_claim ref: content.policy-decision-log - interpretation-role: model-defense or tool-use authorization provenance - evidence-refs: [content.policy-decision-log] - - source-id: boundary-evidence - source-layer: evidence_claim + interpretation_role: model-defense or tool-use authorization provenance + evidence_refs: [content.policy-decision-log] + - source_id: boundary-evidence + source_layer: evidence_claim ref: content.boundary-check-evidence - interpretation-role: physical participant-boundary evidence - evidence-refs: [content.boundary-check-evidence] - - source-id: objective-result - source-layer: objective_result + interpretation_role: physical participant-boundary evidence + evidence_refs: [content.boundary-check-evidence] + - source_id: objective-result + source_layer: objective_result ref: demonstrate-handoff - interpretation-role: scenario objective result - evidence-refs: + interpretation_role: scenario objective result + evidence_refs: - content.participant-observation - content.wazuh-evidence - content.policy-decision-log - content.boundary-check-evidence - target-bindings: - - target-id: objective-supported - target-layer: objective_result + target_bindings: + - target_id: objective-supported + target_layer: objective_result ref: demonstrate-handoff relation: supports objective success when all bounded evidence records exist - evidence-refs: + evidence_refs: - content.participant-observation - content.wazuh-evidence - content.policy-decision-log @@ -488,14 +492,14 @@ outcome-interpretation-rules: - Does not evaluate Wazuh detection quality. - Does not evaluate model-defense robustness. - Does not close the downstream APTL realization issue. - - target-id: scenario-meaning-supported - target-layer: scenario_meaning + - target_id: scenario-meaning-supported + target_layer: scenario_meaning ref: reference-demonstration relation: > Shows that ACES can represent participant, target, internal dependency, OSS defender evidence, policy provenance, evaluator evidence, and observation boundaries in one focused enterprise slice. - evidence-refs: + evidence_refs: - content.participant-observation - content.wazuh-evidence - content.policy-decision-log @@ -503,7 +507,7 @@ outcome-interpretation-rules: limitations: - The defensive and model-defense surfaces are teasers for later work. - The claim remains the authored runtime handoff. - evidence-refs: + evidence_refs: - content.participant-observation - content.wazuh-evidence - content.policy-decision-log @@ -534,30 +538,119 @@ agents: - content.task-brief observation_boundaries: [participant-view] -behavior-specifications: +behavior_specifications: participant-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [participant-agent] - participant-role-refs: [red] - action-contract-refs: [probe-customer-portal-login] - observation-boundary-refs: [participant-view] - outcome-interpretation-rule-refs: [probe-customer-portal-login-outcome] - authority-scope-refs: + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [participant-agent] + participant_role_refs: [red] + action_contract_refs: [probe-customer-portal-login] + observation_boundary_refs: [participant-view] + outcome_interpretation_rule_refs: [probe-customer-portal-login-outcome] + authority_scope_refs: - nodes.customer-portal.services.http - content.task-brief - behavior-mode: policy-directed - realization-profile-ref: participant-implementation-manifest:participant-agent - backend-feature-support-refs: + behavior_mode: policy-directed + realization_profile_ref: participant-implementation-manifest:participant-agent + backend_feature_support_refs: - action_contracts - observation_boundaries - behavior_history - x-scenario:wazuh-evidence - x-scenario:policy-provenance - x-scenario:boundary-negative-evidence - evidence-contract-refs: [participant-behavior-history-event-stream-v1] - extension-policy: governed-extension + evidence_contract_refs: [participant-behavior-history-event-stream-v1] + extension_policy: governed-extension +propositions: + boundary-checks-recorded: + description: The governed boundary-checks-recorded observation is true for the addressed scenario subject. + subjects: + - nodes.redteam-net + basis: observed_state + predicate: + kind: boolean + property: boundary-checks-recorded + semantic_ref: urn:aces:observable:boundary-checks-recorded + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + participant-observation-recorded: + description: The governed participant-observation-recorded observation is true for the addressed scenario subject. + subjects: + - nodes.redteam-net + basis: observed_state + predicate: + kind: boolean + property: participant-observation-recorded + semantic_ref: urn:aces:observable:participant-observation-recorded + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + policy-decision-recorded: + description: The governed policy-decision-recorded observation is true for the addressed scenario subject. + subjects: + - nodes.redteam-net + basis: observed_state + predicate: + kind: boolean + property: policy-decision-recorded + semantic_ref: urn:aces:observable:policy-decision-recorded + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + wazuh-evidence-recorded: + description: The governed wazuh-evidence-recorded observation is true for the addressed scenario subject. + subjects: + - nodes.redteam-net + basis: observed_state + predicate: + kind: boolean + property: wazuh-evidence-recorded + semantic_ref: urn:aces:observable:wazuh-evidence-recorded + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence +assertions: + boundary-checks-recorded: + proposition: boundary-checks-recorded + role: postcondition + polarity: positive + participant-observation-recorded: + proposition: participant-observation-recorded + role: postcondition + polarity: positive + policy-decision-recorded: + proposition: policy-decision-recorded + role: postcondition + polarity: positive + wazuh-evidence-recorded: + proposition: wazuh-evidence-recorded + role: postcondition + polarity: positive +evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.redteam-net + scope: authored objective assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required objectives: # Graded scoring/interpretation of this evidence loop now lives in the # experiment/evaluator plane (experiment-* contracts) per ADR-073. The SDL @@ -572,7 +665,7 @@ objectives: - content.policy-decision-log - content.boundary-check-evidence success: - conditions: + assertions: - participant-observation-recorded - wazuh-evidence-recorded - policy-decision-recorded @@ -594,6 +687,6 @@ workflows: probe: type: objective objective: demonstrate-handoff - on-success: finish + on_success: finish finish: type: end diff --git a/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml b/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml index a87621f2e..98d0b5649 100644 --- a/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml +++ b/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml @@ -1,418 +1,641 @@ name: hospital-ransomware-surgery-day -description: > - Surgery-day ransomware exercise for a regional hospital. The scenario - combines phishing, vendor access abuse, identity pivoting, EHR/PACS - disruption, and backup-driven recovery under clinical time pressure. - -variables: - surgery_day_speed: - type: number - default: 1.0 - description: Story speed multiplier for the exercise day - recovery_window: - type: string - default: "8 hour" - description: Total duration of the recovery story +description: 'Surgery-day ransomware exercise for a regional hospital. The scenario + combines phishing, vendor access abuse, identity pivoting, EHR/PACS disruption, + and backup-driven recovery under clinical time pressure. + ' nodes: internet-edge: - type: Switch + type: switch description: Public ingress for email, VPN, and external access vendor-segment: - type: Switch + type: switch description: Third-party support access zone corp-it: - type: Switch + type: switch description: Corporate productivity and identity-adjacent services clinical-it: - type: Switch + type: switch description: Clinical application tier identity-net: - type: Switch + type: switch description: Active Directory and SSO infrastructure security-net: - type: Switch + type: switch description: SIEM and response tooling network backup-net: - type: Switch + type: switch description: Backup and recovery network - mail-gateway: - type: VM + type: vm + source: + name: secure-mail-gateway + version: '*' + resources: + ram: 2147483648 + cpu: 1 os: linux - source: secure-mail-gateway - resources: {ram: 2 gib, cpu: 1} - features: {mail-relay: mail-admin} - conditions: {mail-queue-healthy: mail-admin} + features: + mail-relay: mail-admin + conditions: + mail-queue-healthy: mail-admin + roles: + mail-admin: + username: postfix services: - - {port: 25, name: smtp-inbound} - - {port: 443, name: mail-admin-ui} - roles: {mail-admin: postfix} - + - port: 25 + name: smtp-inbound + - port: 443 + name: mail-admin-ui vpn-gateway: - type: VM + type: vm + source: + name: vpn-concentrator + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: linux - source: vpn-concentrator - resources: {ram: 2 gib, cpu: 2} - features: {vpn-concentrator: vpn-admin} + features: + vpn-concentrator: vpn-admin conditions: vpn-auth-healthy: vpn-admin vendor-session-established: vpn-admin - services: - - {port: 443, name: vpn-portal} - - {port: 1194, protocol: udp, name: vpn-tunnel} roles: vpn-admin: username: netops - entities: [hospital-blue.identity] - + entities: + - hospital-blue.identity + services: + - port: 443 + name: vpn-portal + - port: 1194 + protocol: udp + name: vpn-tunnel ad01: - type: VM + type: vm + source: + name: windows-ad + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: windows - os_version: "Server 2022" - source: windows-ad - resources: {ram: 4 gib, cpu: 2} + os_version: Server 2022 features: hospital-ad: domain-admin hospital-adfs: federation-admin conditions: domain-auth-healthy: domain-admin sso-healthy: federation-admin - services: - - {port: 53, protocol: udp, name: ad-dns} - - {port: 389, name: ad-ldap} - - {port: 443, name: adfs-https} roles: domain-admin: username: Administrator - entities: [hospital-blue.identity] + entities: + - hospital-blue.identity federation-admin: username: adfs_svc + services: + - port: 53 + protocol: udp + name: ad-dns + - port: 389 + name: ad-ldap + - port: 443 + name: adfs-https runtime: identity_authorities: - - identity_authority_id: hospital-domain - kind: domain - name: Hospital Active Directory Domain - namespace: hospital.local - domain_name: HOSPITAL - realm: HOSPITAL.LOCAL - base_dn: DC=hospital,DC=local - services: - - service_id: ldap-endpoint - service: ad-ldap - protocol: ldap - address: dc.hospital.local - port: 389 - subjects: - - subject_id: nurse-user - kind: user - name: nurse.jane - principal_name: nurse.jane@HOSPITAL.LOCAL - distinguished_name: CN=nurse.jane,OU=Clinical,DC=hospital,DC=local - domain: hospital.local - enabled: true - origin: directory - attributes: - - name: objectGUID - values: [hospital-guid-nurse-jane] - origin: directory - - subject_id: clinical-group - kind: group - name: Clinical - distinguished_name: CN=Clinical,OU=Groups,DC=hospital,DC=local - origin: directory - - subject_id: surgery-group - kind: group - name: Surgery - distinguished_name: CN=Surgery,OU=Groups,DC=hospital,DC=local - origin: directory - - subject_id: appservices-group - kind: group - name: AppServices - distinguished_name: CN=AppServices,OU=Groups,DC=hospital,DC=local - origin: directory - - subject_id: ehr-service-principal - kind: service_principal - name: ehrsvc - principal_name: ehrsvc@HOSPITAL.LOCAL - service_principal_names: [HTTP/ehr.hospital.local] - origin: directory - relationships: - - relationship_id: nurse-clinical-membership - relationship_type: member_of - source_ref: nurse-user - target_ref: clinical-group - - relationship_id: ehr-service-appservices-membership - relationship_type: member_of - source_ref: ehr-service-principal - target_ref: appservices-group - policies: - - policy_id: default-domain-password-policy - policy_kind: password - name: Default Domain Password Policy - applies_to_refs: [hospital-domain, ldap-endpoint, nurse-clinical-membership] - settings: - - name: min_length - values: ["14"] - origin: directory - - name: history_count - values: ["24"] - origin: directory - - identity_authority_id: hospital-adfs - kind: identity_provider - name: Hospital ADFS - namespace: https://adfs.hospital.local - issuer: https://adfs.hospital.local/adfs - services: - - service_id: adfs-saml - service: adfs-https - protocol: saml - address: adfs.hospital.local - port: 443 - subjects: - - subject_id: ehr-application - kind: application - name: ehr-service - principal_name: https://ehr.hospital.local/ - origin: federated - relationships: - - relationship_id: adfs-federates-ehr - relationship_type: federates_with - source_ref: hospital-adfs - target_ref: ehr-application - + - identity_authority_id: hospital-domain + kind: domain + name: Hospital Active Directory Domain + namespace: hospital.local + domain_name: HOSPITAL + realm: HOSPITAL.LOCAL + base_dn: DC=hospital,DC=local + services: + - service_id: ldap-endpoint + service: ad-ldap + protocol: ldap + address: dc.hospital.local + port: 389 + subjects: + - subject_id: nurse-user + kind: user + name: nurse.jane + principal_name: nurse.jane@HOSPITAL.LOCAL + distinguished_name: CN=nurse.jane,OU=Clinical,DC=hospital,DC=local + domain: hospital.local + enabled: true + origin: directory + attributes: + - name: objectGUID + values: + - hospital-guid-nurse-jane + origin: directory + - subject_id: clinical-group + kind: group + name: Clinical + distinguished_name: CN=Clinical,OU=Groups,DC=hospital,DC=local + origin: directory + - subject_id: surgery-group + kind: group + name: Surgery + distinguished_name: CN=Surgery,OU=Groups,DC=hospital,DC=local + origin: directory + - subject_id: appservices-group + kind: group + name: AppServices + distinguished_name: CN=AppServices,OU=Groups,DC=hospital,DC=local + origin: directory + - subject_id: ehr-service-principal + kind: service_principal + name: ehrsvc + principal_name: ehrsvc@HOSPITAL.LOCAL + origin: directory + service_principal_names: + - HTTP/ehr.hospital.local + policies: + - policy_id: default-domain-password-policy + policy_kind: password + name: Default Domain Password Policy + applies_to_refs: + - hospital-domain + - ldap-endpoint + - nurse-clinical-membership + settings: + - name: min_length + values: + - '14' + origin: directory + - name: history_count + values: + - '24' + origin: directory + relationships: + - relationship_id: nurse-clinical-membership + relationship_type: member_of + source_ref: nurse-user + target_ref: clinical-group + - relationship_id: ehr-service-appservices-membership + relationship_type: member_of + source_ref: ehr-service-principal + target_ref: appservices-group + - identity_authority_id: hospital-adfs + kind: identity_provider + name: Hospital ADFS + namespace: https://adfs.hospital.local + issuer: https://adfs.hospital.local/adfs + services: + - service_id: adfs-saml + service: adfs-https + protocol: saml + address: adfs.hospital.local + port: 443 + subjects: + - subject_id: ehr-application + kind: application + name: ehr-service + principal_name: https://ehr.hospital.local/ + origin: federated + relationships: + - relationship_id: adfs-federates-ehr + relationship_type: federates_with + source_ref: hospital-adfs + target_ref: ehr-application exchange01: - type: VM + type: vm + source: + name: exchange-2019 + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: windows - os_version: "Server 2019" - source: exchange-2019 - resources: {ram: 4 gib, cpu: 2} - features: {exchange-mailbox: exchange-admin} + os_version: Server 2019 + features: + exchange-mailbox: exchange-admin conditions: exchange-owa-healthy: exchange-admin phish-mail-delivered: exchange-admin - services: - - {port: 443, name: exchange-owa} - - {port: 25, name: exchange-smtp} roles: exchange-admin: username: exch_admin - entities: [hospital-blue.identity] - + entities: + - hospital-blue.identity + services: + - port: 443 + name: exchange-owa + - port: 25 + name: exchange-smtp ehr-frontend: - type: VM + type: vm + source: + name: ehr-web-tier + version: '*' + resources: + ram: 6442450944 + cpu: 4 os: linux - source: ehr-web-tier - resources: {ram: 6 gib, cpu: 4} - features: {ehr-service: ehr-admin} + features: + ehr-service: ehr-admin conditions: ehr-api-healthy: ehr-admin clinical-disruption-observed: ehr-admin - services: - - {port: 443, name: ehr-https} - - {port: 8443, name: ehr-admin-api} roles: ehr-admin: username: ehrsvc - entities: [hospital-blue.clinical-it] + entities: + - hospital-blue.clinical-it + services: + - port: 443 + name: ehr-https + - port: 8443 + name: ehr-admin-api asset_value: confidentiality: critical integrity: critical availability: critical - ehr-db: - type: VM + type: vm + source: + name: postgres-cluster + version: '*' + resources: + ram: 8589934592 + cpu: 4 os: linux - source: postgres-cluster - resources: {ram: 8 gib, cpu: 4} - features: {sql-cluster: dba} + features: + sql-cluster: dba conditions: ehr-db-healthy: dba phi-staging-observed: dba - services: - - {port: 5432, name: ehr-postgres} roles: dba: username: postgres - entities: [hospital-blue.clinical-it] + entities: + - hospital-blue.clinical-it + services: + - port: 5432 + name: ehr-postgres asset_value: confidentiality: critical integrity: critical - pacs01: - type: VM + type: vm + source: + name: pacs-archive + version: '*' + resources: + ram: 6442450944 + cpu: 4 os: linux - source: pacs-archive - resources: {ram: 6 gib, cpu: 4} - features: {pacs-service: pacs-admin} + features: + pacs-service: pacs-admin conditions: pacs-archive-healthy: pacs-admin radiology-workflow-delayed: pacs-admin - services: - - {port: 104, name: dicom-store} - - {port: 8444, name: pacs-web-ui} roles: pacs-admin: username: pacs - entities: [hospital-blue.clinical-it] + entities: + - hospital-blue.clinical-it + services: + - port: 104 + name: dicom-store + - port: 8444 + name: pacs-web-ui asset_value: confidentiality: high integrity: high availability: critical - med-gateway: - type: VM + type: vm + source: + name: medical-device-broker + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: linux - source: medical-device-broker - resources: {ram: 2 gib, cpu: 2} - features: {device-broker: device-admin} - conditions: {device-mesh-healthy: device-admin} - services: - - {port: 8883, name: med-mqtt} + features: + device-broker: device-admin + conditions: + device-mesh-healthy: device-admin roles: device-admin: username: biomed - entities: [hospital-blue.clinical-it] - + entities: + - hospital-blue.clinical-it + services: + - port: 8883 + name: med-mqtt backup-vault: - type: VM + type: vm + source: + name: immutable-backup-appliance + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: immutable-backup-appliance - resources: {ram: 4 gib, cpu: 2} - features: {immutable-backup: backup-admin} + features: + immutable-backup: backup-admin conditions: backup-catalog-intact: backup-admin restore-runbook-ready: backup-admin - services: - - {port: 443, name: backup-api} - - {port: 2049, name: backup-nfs} roles: backup-admin: username: backupops - entities: [hospital-blue.recovery] - + entities: + - hospital-blue.recovery + services: + - port: 443 + name: backup-api + - port: 2049 + name: backup-nfs siem01: - type: VM + type: vm + source: + name: siem-stack + version: '*' + resources: + ram: 8589934592 + cpu: 4 os: linux - source: siem-stack - resources: {ram: 8 gib, cpu: 4} features: siem-core: soc-admin edr-sensor: soc-admin conditions: soc-pipeline-up: soc-admin triage-case-submitted: soc-admin - services: - - {port: 5601, name: soc-ui} - - {port: 9200, name: soc-search} roles: soc-admin: username: socadmin - entities: [hospital-blue.soc] - + entities: + - hospital-blue.soc + services: + - port: 5601 + name: soc-ui + - port: 9200 + name: soc-search vendor-jump: - type: VM + type: vm + source: + name: vendor-workstation + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: windows - os_version: "11" - source: vendor-workstation - resources: {ram: 2 gib, cpu: 2} - conditions: {vendor-jump-online: support} - services: - - {port: 3389, name: vendor-rdp} + os_version: '11' + conditions: + vendor-jump-online: support roles: support: username: support - entities: [vendor-support] - + entities: + - vendor-support + services: + - port: 3389 + name: vendor-rdp infrastructure: internet-edge: count: 1 - properties: {cidr: 10.70.0.0/24, gateway: 10.70.0.1} + properties: + cidr: 10.70.0.0/24 + gateway: 10.70.0.1 vendor-segment: count: 1 - properties: {cidr: 10.70.1.0/24, gateway: 10.70.1.1, internal: true} + properties: + cidr: 10.70.1.0/24 + gateway: 10.70.1.1 + internal: true acls: - - {name: allow-public-vpn, direction: in, from_net: internet-edge, protocol: tcp, ports: [443], action: allow} - - {name: deny-clinical-backflow, direction: in, from_net: clinical-it, action: deny} + - name: allow-public-vpn + direction: in + from_net: internet-edge + protocol: tcp + ports: + - 443 + action: allow + - name: deny-clinical-backflow + direction: in + from_net: clinical-it + action: deny corp-it: count: 1 - properties: {cidr: 10.70.2.0/24, gateway: 10.70.2.1, internal: true} + properties: + cidr: 10.70.2.0/24 + gateway: 10.70.2.1 + internal: true acls: - - {name: allow-public-mail-web, direction: in, from_net: internet-edge, protocol: tcp, ports: [25, 443], action: allow} - - {name: allow-vendor-support, direction: in, from_net: vendor-segment, protocol: tcp, ports: [443], action: allow} + - name: allow-public-mail-web + direction: in + from_net: internet-edge + protocol: tcp + ports: + - 25 + - 443 + action: allow + - name: allow-vendor-support + direction: in + from_net: vendor-segment + protocol: tcp + ports: + - 443 + action: allow clinical-it: count: 1 - properties: {cidr: 10.70.3.0/24, gateway: 10.70.3.1, internal: true} + properties: + cidr: 10.70.3.0/24 + gateway: 10.70.3.1 + internal: true acls: - - {name: allow-corp-clinical-apps, direction: in, from_net: corp-it, protocol: tcp, ports: [443, 8443, 8444], action: allow} - - {name: allow-vendor-clinical-web, direction: in, from_net: vendor-segment, protocol: tcp, ports: [443], action: allow} - - {name: deny-public-clinical, direction: in, from_net: internet-edge, action: deny} + - name: allow-corp-clinical-apps + direction: in + from_net: corp-it + protocol: tcp + ports: + - 443 + - 8443 + - 8444 + action: allow + - name: allow-vendor-clinical-web + direction: in + from_net: vendor-segment + protocol: tcp + ports: + - 443 + action: allow + - name: deny-public-clinical + direction: in + from_net: internet-edge + action: deny identity-net: count: 1 - properties: {cidr: 10.70.4.0/24, gateway: 10.70.4.1, internal: true} + properties: + cidr: 10.70.4.0/24 + gateway: 10.70.4.1 + internal: true acls: - - {name: allow-corp-identity, direction: in, from_net: corp-it, protocol: tcp, ports: [53, 389, 443], action: allow} - - {name: allow-clinical-identity, direction: in, from_net: clinical-it, protocol: tcp, ports: [389, 443], action: allow} + - name: allow-corp-identity + direction: in + from_net: corp-it + protocol: tcp + ports: + - 53 + - 389 + - 443 + action: allow + - name: allow-clinical-identity + direction: in + from_net: clinical-it + protocol: tcp + ports: + - 389 + - 443 + action: allow security-net: count: 1 - properties: {cidr: 10.70.5.0/24, gateway: 10.70.5.1, internal: true} + properties: + cidr: 10.70.5.0/24 + gateway: 10.70.5.1 + internal: true backup-net: count: 1 - properties: {cidr: 10.70.6.0/24, gateway: 10.70.6.1, internal: true} - + properties: + cidr: 10.70.6.0/24 + gateway: 10.70.6.1 + internal: true mail-gateway: count: 1 - links: [internet-edge, corp-it, security-net] + links: + - internet-edge + - corp-it + - security-net vpn-gateway: count: 1 - links: [internet-edge, vendor-segment, corp-it] + links: + - internet-edge + - vendor-segment + - corp-it ad01: count: 1 - links: [identity-net, corp-it, clinical-it] + links: + - identity-net + - corp-it + - clinical-it exchange01: count: 1 - links: [corp-it, identity-net] - dependencies: [ad01] + links: + - corp-it + - identity-net + dependencies: + - ad01 ehr-frontend: count: 1 - links: [clinical-it, identity-net] - dependencies: [ehr-db, ad01] + links: + - clinical-it + - identity-net + dependencies: + - ehr-db + - ad01 ehr-db: count: 1 - links: [clinical-it, backup-net] - dependencies: [backup-vault] + links: + - clinical-it + - backup-net + dependencies: + - backup-vault pacs01: count: 1 - links: [clinical-it, backup-net] - dependencies: [backup-vault] + links: + - clinical-it + - backup-net + dependencies: + - backup-vault med-gateway: count: 1 - links: [clinical-it] - dependencies: [ehr-frontend] + links: + - clinical-it + dependencies: + - ehr-frontend backup-vault: count: 1 - links: [backup-net, security-net] + links: + - backup-net + - security-net siem01: count: 1 - links: [security-net, corp-it] + links: + - security-net + - corp-it vendor-jump: count: 1 - links: [vendor-segment] - + links: + - vendor-segment features: - mail-relay: {type: Service, source: postfix-relay} - vpn-concentrator: {type: Service, source: openvpn-appliance} - hospital-ad: {type: Service, source: active-directory-domain-services} - hospital-adfs: {type: Service, source: adfs} - exchange-mailbox: {type: Service, source: exchange-mailbox-role} - ehr-service: {type: Service, source: epic-web} - sql-cluster: {type: Service, source: postgresql-ha} - pacs-service: {type: Service, source: orthanc-pacs} - device-broker: {type: Service, source: mqtt-medical-gateway} - immutable-backup: {type: Service, source: immutable-backup-vault} - siem-core: {type: Service, source: wazuh-elastic-stack} - edr-sensor: {type: Artifact, source: hospital-edr-agent, destination: /opt/edr} - + mail-relay: + type: service + source: + name: postfix-relay + version: '*' + vpn-concentrator: + type: service + source: + name: openvpn-appliance + version: '*' + hospital-ad: + type: service + source: + name: active-directory-domain-services + version: '*' + hospital-adfs: + type: service + source: + name: adfs + version: '*' + exchange-mailbox: + type: service + source: + name: exchange-mailbox-role + version: '*' + ehr-service: + type: service + source: + name: epic-web + version: '*' + sql-cluster: + type: service + source: + name: postgresql-ha + version: '*' + pacs-service: + type: service + source: + name: orthanc-pacs + version: '*' + device-broker: + type: service + source: + name: mqtt-medical-gateway + version: '*' + immutable-backup: + type: service + source: + name: immutable-backup-vault + version: '*' + siem-core: + type: service + source: + name: wazuh-elastic-stack + version: '*' + edr-sensor: + type: artifact + source: + name: hospital-edr-agent + version: '*' + destination: /opt/edr conditions: mail-queue-healthy: command: /usr/local/bin/check-mail-queue @@ -421,6 +644,7 @@ conditions: command: /usr/local/bin/check-vpn-auth interval: 30 vendor-session-established: + proposition: vendor-session-established command: /usr/local/bin/check-vendor-session interval: 30 domain-auth-healthy: @@ -433,49 +657,265 @@ conditions: command: /usr/local/bin/check-exchange-owa interval: 30 phish-mail-delivered: + proposition: phish-mail-delivered command: /usr/local/bin/check-phish-delivery interval: 60 ehr-api-healthy: + proposition: ehr-api-healthy command: /usr/local/bin/check-ehr-api interval: 15 clinical-disruption-observed: + proposition: clinical-disruption-observed command: /usr/local/bin/check-clinical-disruption interval: 30 ehr-db-healthy: command: /usr/local/bin/check-postgres interval: 15 phi-staging-observed: + proposition: phi-staging-observed command: /usr/local/bin/check-staged-phi interval: 60 pacs-archive-healthy: + proposition: pacs-archive-healthy command: /usr/local/bin/check-pacs interval: 15 radiology-workflow-delayed: + proposition: radiology-workflow-delayed command: /usr/local/bin/check-radiology-delay interval: 30 device-mesh-healthy: command: /usr/local/bin/check-device-mesh interval: 30 backup-catalog-intact: + proposition: backup-catalog-intact command: /usr/local/bin/check-backup-catalog interval: 30 restore-runbook-ready: command: /usr/local/bin/check-restore-runbook interval: 60 soc-pipeline-up: + proposition: soc-pipeline-up command: /usr/local/bin/check-siem-ingest interval: 30 triage-case-submitted: + proposition: triage-case-submitted command: /usr/local/bin/check-triage-case interval: 60 vendor-jump-online: command: /usr/local/bin/check-vendor-rdp interval: 30 - +propositions: + backup-catalog-intact: + description: The governed backup-catalog-intact observation is true for the addressed scenario subject. + subjects: + - nodes.backup-vault + basis: observed_state + predicate: + kind: boolean + property: backup-catalog-intact + semantic_ref: urn:aces:observable:backup-catalog-intact + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + clinical-disruption-observed: + description: The governed clinical-disruption-observed observation is true for the addressed scenario subject. + subjects: + - nodes.ehr-frontend + basis: observed_state + predicate: + kind: boolean + property: clinical-disruption-observed + semantic_ref: urn:aces:observable:clinical-disruption-observed + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + ehr-api-healthy: + description: The governed ehr-api-healthy observation is true for the addressed scenario subject. + subjects: + - nodes.ehr-frontend + basis: observed_state + predicate: + kind: boolean + property: ehr-api-healthy + semantic_ref: urn:aces:observable:ehr-api-healthy + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + pacs-archive-healthy: + description: The governed pacs-archive-healthy observation is true for the addressed scenario subject. + subjects: + - nodes.pacs01 + basis: observed_state + predicate: + kind: boolean + property: pacs-archive-healthy + semantic_ref: urn:aces:observable:pacs-archive-healthy + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + phi-staging-observed: + description: The governed phi-staging-observed observation is true for the addressed scenario subject. + subjects: + - nodes.ehr-db + basis: observed_state + predicate: + kind: boolean + property: phi-staging-observed + semantic_ref: urn:aces:observable:phi-staging-observed + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + phish-mail-delivered: + description: The governed phish-mail-delivered observation is true for the addressed scenario subject. + subjects: + - nodes.exchange01 + basis: observed_state + predicate: + kind: boolean + property: phish-mail-delivered + semantic_ref: urn:aces:observable:phish-mail-delivered + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + radiology-workflow-delayed: + description: The governed radiology-workflow-delayed observation is true for the addressed scenario subject. + subjects: + - nodes.pacs01 + basis: observed_state + predicate: + kind: boolean + property: radiology-workflow-delayed + semantic_ref: urn:aces:observable:radiology-workflow-delayed + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + soc-pipeline-up: + description: The governed soc-pipeline-up observation is true for the addressed scenario subject. + subjects: + - nodes.siem01 + basis: observed_state + predicate: + kind: boolean + property: soc-pipeline-up + semantic_ref: urn:aces:observable:soc-pipeline-up + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + triage-case-submitted: + description: The governed triage-case-submitted observation is true for the addressed scenario subject. + subjects: + - nodes.siem01 + basis: observed_state + predicate: + kind: boolean + property: triage-case-submitted + semantic_ref: urn:aces:observable:triage-case-submitted + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + vendor-session-established: + description: The governed vendor-session-established observation is true for the addressed scenario subject. + subjects: + - nodes.vpn-gateway + basis: observed_state + predicate: + kind: boolean + property: vendor-session-established + semantic_ref: urn:aces:observable:vendor-session-established + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence +assertions: + backup-catalog-intact: + proposition: backup-catalog-intact + role: postcondition + polarity: positive + clinical-disruption-observed: + proposition: clinical-disruption-observed + role: postcondition + polarity: positive + ehr-api-healthy: + proposition: ehr-api-healthy + role: postcondition + polarity: positive + pacs-archive-healthy: + proposition: pacs-archive-healthy + role: postcondition + polarity: positive + phi-staging-observed: + proposition: phi-staging-observed + role: postcondition + polarity: positive + radiology-workflow-delayed: + proposition: radiology-workflow-delayed + role: postcondition + polarity: positive + soc-pipeline-up: + proposition: soc-pipeline-up + role: postcondition + polarity: positive + triage-case-submitted: + proposition: triage-case-submitted + role: postcondition + polarity: positive + vendor-session-established: + proposition: vendor-session-established + role: postcondition + polarity: positive + pre-clinical-disruption-observed: + proposition: clinical-disruption-observed + role: precondition + polarity: positive + pre-phish-mail-delivered: + proposition: phish-mail-delivered + role: precondition + polarity: positive +evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.ehr-frontend + - nodes.backup-vault + - nodes.exchange01 + - nodes.siem01 + - nodes.pacs01 + - nodes.vpn-gateway + - nodes.ehr-db + scope: authored objective, event, workflow, and participant assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required vulnerabilities: macro-lure: name: Phishing macro lure - description: Macro-enabled attachment delivers initial access to the vendor support path + description: Macro-enabled attachment delivers initial access to the vendor support + path technical: false class: CWE-451 vpn-portal-bypass: @@ -493,11 +933,10 @@ vulnerabilities: description: Restore staging area is writable from the clinical tier technical: true class: CWE-732 - entities: hospital-blue: name: Hospital Blue Team - role: Blue + role: blue mission: Keep patient care and core services available throughout the incident entities: soc: @@ -510,154 +949,187 @@ entities: name: Recovery Cell vendor-support: name: Vendor Support - role: Green + role: green mission: Maintain third-party support connection for imaging systems ransom-crew: name: Ransom Crew - role: Red + role: red mission: Steal PHI and disrupt radiology before the surgery window closes white-cell: name: White Cell - role: White - + role: white injects: vendor-advisory: - source: vendor-advisory-pdf + source: + name: vendor-advisory-pdf + version: '*' from_entity: white-cell - to_entities: [hospital-blue, vendor-support] + to_entities: + - hospital-blue + - vendor-support description: Advisory about intermittent VPN instability from the imaging vendor phishing-wave: - source: payroll-lure-pack + source: + name: payroll-lure-pack + version: '*' from_entity: ransom-crew - to_entities: [hospital-blue, vendor-support] + to_entities: + - hospital-blue + - vendor-support description: Phishing lure targeting helpdesk and vendor support personnel surgery-escalation: - source: operating-room-escalation + source: + name: operating-room-escalation + version: '*' from_entity: white-cell - to_entities: [hospital-blue] + to_entities: + - hospital-blue description: Surgeons report delayed radiology access and demand ETA - events: morning-advisory: - injects: [vendor-advisory] + injects: + - vendor-advisory phishing-delivery: - conditions: [phish-mail-delivered] - injects: [phishing-wave] + assertions: + - pre-phish-mail-delivered + injects: + - phishing-wave surgery-pressure: - conditions: [clinical-disruption-observed] - injects: [surgery-escalation] - + assertions: + - pre-clinical-disruption-observed + injects: + - surgery-escalation scripts: pre-surgery: - start-time: 0 - end-time: 2 hour + start_time: 0 + end_time: 7200 speed: ${surgery_day_speed} events: - morning-advisory: 10 min - phishing-delivery: 45 min + morning-advisory: 600 + phishing-delivery: 2700 live-clinic: - start-time: 2 hour - end-time: 5 hour + start_time: 7200 + end_time: 18000 speed: ${surgery_day_speed} events: - surgery-pressure: 3 hour + surgery-pressure: 10800 recovery-window: - start-time: 5 hour - end-time: ${recovery_window} + start_time: 18000 + end_time: ${recovery_window} speed: ${surgery_day_speed} events: - surgery-pressure: 6 hour - + surgery-pressure: 21600 stories: surgery-day: speed: ${surgery_day_speed} - scripts: [pre-surgery, live-clinic, recovery-window] - + scripts: + - pre-surgery + - live-clinic + - recovery-window content: phi-records: type: dataset target: ehr-db + source: + name: synthetic-phi-seed + version: '*' format: sql - source: synthetic-phi-seed sensitive: true - tags: [phi, patients] + tags: + - phi + - patients radiology-images: type: dataset target: pacs01 + source: + name: dicom-archive-seed + version: '*' format: dicom - source: dicom-archive-seed sensitive: true - tags: [radiology] + tags: + - radiology phishing-mailbox: type: dataset target: exchange01 format: eml - sensitive: true items: - - name: payroll-adjustment.eml - tags: [phishing, attachment] - description: Message crafted to lure helpdesk review - - name: vendor-followup.eml - tags: [phishing, vendor] - description: Message impersonating the imaging vendor + - name: payroll-adjustment + display_name: payroll-adjustment.eml + tags: + - phishing + - attachment + description: Message crafted to lure helpdesk review + - name: vendor-followup + display_name: vendor-followup.eml + tags: + - phishing + - vendor + description: Message impersonating the imaging vendor + sensitive: true backup-runbook: type: file target: backup-vault path: /srv/runbooks/hospital-recovery.md - text: "Recover EHR, PACS, and device broker in dependency order." + text: Recover EHR, PACS, and device broker in dependency order. sensitive: true vendor-session-profile: type: file target: vendor-jump path: C:\\Support\\session.ini - text: "tenant=hospital\\naccess=vpn\\nprofile=imaging-support" + text: tenant=hospital\naccess=vpn\nprofile=imaging-support sensitive: true - accounts: nurse-user: username: nurse.jane node: exchange01 - groups: [Clinical] + groups: + - Clinical password_strength: medium mail: nurse.jane@hospital.local surgeon-user: username: surgeon.lee node: ehr-frontend - groups: [Surgery] + groups: + - Surgery password_strength: strong helpdesk-user: username: helpdesk node: exchange01 - groups: [Helpdesk] + groups: + - Helpdesk password_strength: medium svc-ehr: username: ehrsvc node: ehr-frontend - groups: [AppServices] + groups: + - AppServices password_strength: weak - spn: "HTTP/ehr.hospital.local" + spn: HTTP/ehr.hospital.local svc-sql: username: ehrsql node: ehr-db - groups: [DBA] + groups: + - DBA password_strength: weak - spn: "POSTGRES/ehr-db.hospital.local" + spn: POSTGRES/ehr-db.hospital.local backup-operator: username: backupops node: backup-vault - groups: [BackupAdmins] + groups: + - BackupAdmins password_strength: strong vendor-tech: username: support node: vendor-jump - groups: [VendorSupport] + groups: + - VendorSupport password_strength: medium soc-analyst: username: socanalyst node: siem01 - groups: [SOC] + groups: + - SOC password_strength: strong - relationships: exchange-auths-ad: type: authenticates_with @@ -672,12 +1144,15 @@ relationships: type: federates_with source: ehr-service target: hospital-adfs - properties: {protocol: "SAML"} + properties: + protocol: SAML ehr-to-database: type: connects_to source: ehr-service target: sql-cluster - properties: {protocol: "tcp", port: "5432"} + properties: + protocol: tcp + port: '5432' pacs-sync-to-backup: type: replicates_to source: pacs-service @@ -690,108 +1165,228 @@ relationships: type: manages source: immutable-backup target: ehr-db - agents: red-initial: entity: ransom-crew - actions: [Phish, AbuseVPN, Discover, StealCred] + actions: + - Phish + - AbuseVPN + - Discover + - StealCred initial_knowledge: - hosts: [mail-gateway, vpn-gateway, vendor-jump] - subnets: [internet-edge, vendor-segment] - services: [smtp-inbound, vpn-portal, vendor-rdp] - accounts: [vendor-tech] - allowed_subnets: [internet-edge, vendor-segment, corp-it] + hosts: + - mail-gateway + - vpn-gateway + - vendor-jump + subnets: + - internet-edge + - vendor-segment + services: + - smtp-inbound + - vpn-portal + - vendor-rdp + accounts: + - vendor-tech + allowed_subnets: + - internet-edge + - vendor-segment + - corp-it red-operator: entity: ransom-crew - actions: [Pivot, Exfiltrate, Encrypt, Disable] - starting_accounts: [svc-ehr] + actions: + - Pivot + - Exfiltrate + - Encrypt + - Disable + starting_accounts: + - svc-ehr initial_knowledge: - hosts: [ehr-frontend, ehr-db, pacs01] - subnets: [clinical-it, backup-net] - services: [ehr-https, ehr-postgres, dicom-store] - accounts: [svc-sql, backup-operator] - allowed_subnets: [clinical-it, backup-net, identity-net] + hosts: + - ehr-frontend + - ehr-db + - pacs01 + subnets: + - clinical-it + - backup-net + services: + - ehr-https + - ehr-postgres + - dicom-store + accounts: + - svc-sql + - backup-operator + allowed_subnets: + - clinical-it + - backup-net + - identity-net blue-soc-agent: entity: hospital-blue.soc - actions: [Inspect, Triage, Isolate, Escalate] - starting_accounts: [soc-analyst] + actions: + - Inspect + - Triage + - Isolate + - Escalate + starting_accounts: + - soc-analyst initial_knowledge: - hosts: [siem01, mail-gateway, vpn-gateway] - subnets: [security-net, corp-it] - services: [soc-ui, mail-admin-ui, vpn-portal] - accounts: [helpdesk-user, vendor-tech] - allowed_subnets: [security-net, corp-it, vendor-segment] + hosts: + - siem01 + - mail-gateway + - vpn-gateway + subnets: + - security-net + - corp-it + services: + - soc-ui + - mail-admin-ui + - vpn-portal + accounts: + - helpdesk-user + - vendor-tech + allowed_subnets: + - security-net + - corp-it + - vendor-segment blue-ir-agent: entity: hospital-blue.recovery - actions: [Contain, Restore, ReissueCreds, Validate] - starting_accounts: [backup-operator] + actions: + - Contain + - Restore + - ReissueCreds + - Validate + starting_accounts: + - backup-operator initial_knowledge: - hosts: [backup-vault, ehr-db, pacs01] - subnets: [backup-net, clinical-it] - services: [backup-api, backup-nfs, pacs-web-ui] - accounts: [svc-sql, backup-operator] - allowed_subnets: [backup-net, clinical-it, identity-net] - + hosts: + - backup-vault + - ehr-db + - pacs01 + subnets: + - backup-net + - clinical-it + services: + - backup-api + - backup-nfs + - pacs-web-ui + accounts: + - svc-sql + - backup-operator + allowed_subnets: + - backup-net + - clinical-it + - identity-net objectives: - # This scenario intended graded red/blue comparison. Graded scoring now lives - # in the experiment/evaluator plane (experiment-* contracts) per ADR-073; - # SDL objectives assert only observable success via conditions. red-establish-foothold: agent: red-initial - actions: [Phish, AbuseVPN] + actions: + - Phish + - AbuseVPN targets: - - vpn-gateway - - exchange01 - - nodes.vpn-gateway.services.vpn-portal - - infrastructure.corp-it.acls.allow-vendor-support - - vpn-vendor-support-path + - vpn-gateway + - exchange01 + - nodes.vpn-gateway.services.vpn-portal + - infrastructure.corp-it.acls.allow-vendor-support + - vpn-vendor-support-path success: - conditions: [vendor-session-established] + assertions: + - vendor-session-established window: - stories: [surgery-day] - scripts: [pre-surgery] - events: [phishing-delivery] + stories: + - surgery-day + scripts: + - pre-surgery + events: + - phishing-delivery red-stage-phi: agent: red-operator - actions: [Pivot, Exfiltrate] - targets: [phi-records, ehr-to-database, ehr-db] + actions: + - Pivot + - Exfiltrate + targets: + - phi-records + - ehr-to-database + - ehr-db success: - conditions: [phi-staging-observed] - depends_on: [red-establish-foothold] + assertions: + - phi-staging-observed window: - stories: [surgery-day] - scripts: [live-clinic] + stories: + - surgery-day + scripts: + - live-clinic + depends_on: + - red-establish-foothold red-disrupt-radiology: entity: ransom-crew - targets: [pacs01, radiology-images, pacs-sync-to-backup] + targets: + - pacs01 + - radiology-images + - pacs-sync-to-backup success: - conditions: [radiology-workflow-delayed, clinical-disruption-observed] - depends_on: [red-stage-phi] + assertions: + - radiology-workflow-delayed + - clinical-disruption-observed window: - stories: [surgery-day] - scripts: [live-clinic] - events: [surgery-pressure] + stories: + - surgery-day + scripts: + - live-clinic + events: + - surgery-pressure + depends_on: + - red-stage-phi blue-detect-and-triage: agent: blue-soc-agent - actions: [Inspect, Triage, Escalate] + actions: + - Inspect + - Triage + - Escalate targets: - - siem01 - - vpn-gateway - - mail-gateway - - nodes.vpn-gateway.services.vpn-portal - - infrastructure.corp-it.acls.allow-vendor-support + - siem01 + - vpn-gateway + - mail-gateway + - nodes.vpn-gateway.services.vpn-portal + - infrastructure.corp-it.acls.allow-vendor-support success: - conditions: [soc-pipeline-up, triage-case-submitted] + assertions: + - soc-pipeline-up + - triage-case-submitted window: - stories: [surgery-day] - scripts: [pre-surgery, live-clinic] + stories: + - surgery-day + scripts: + - pre-surgery + - live-clinic blue-restore-clinical-services: agent: blue-ir-agent - actions: [Contain, Restore, Validate] - targets: [backup-vault, ehr-db, pacs01, backup-manages-ehr] + actions: + - Contain + - Restore + - Validate + targets: + - backup-vault + - ehr-db + - pacs01 + - backup-manages-ehr success: - conditions: [ehr-api-healthy, pacs-archive-healthy, backup-catalog-intact] - depends_on: [blue-detect-and-triage, red-disrupt-radiology] + assertions: + - ehr-api-healthy + - pacs-archive-healthy + - backup-catalog-intact window: - stories: [surgery-day] - scripts: [recovery-window] + stories: + - surgery-day + scripts: + - recovery-window + depends_on: + - blue-detect-and-triage + - red-disrupt-radiology +variables: + surgery_day_speed: + type: number + default: 1.0 + description: Story speed multiplier for the exercise day + recovery_window: + type: string + default: 8 hour + description: Total duration of the recovery story diff --git a/examples/scenarios/port-authority-surge-response.sdl.yaml b/examples/scenarios/port-authority-surge-response.sdl.yaml index 77841e019..f73a81d38 100644 --- a/examples/scenarios/port-authority-surge-response.sdl.yaml +++ b/examples/scenarios/port-authority-surge-response.sdl.yaml @@ -1,303 +1,703 @@ name: port-authority-surge-response -description: > - Port authority surge-day exercise covering customs coordination, - manifest integrity, yard OT disruption, and blackstart-style recovery - of crane operations during a cargo spike. +description: 'Port authority surge-day exercise covering customs coordination, manifest + integrity, yard OT disruption, and blackstart-style recovery of crane operations + during a cargo spike. + ' nodes: - public-edge: {type: Switch, description: Public shipping and partner ingress} - terminal-it: {type: Switch, description: Terminal operating systems and portal tier} - customs-link: {type: Switch, description: Customs and partner integration boundary} - yard-ot: {type: Switch, description: Yard control and PLC zone} - safety-net: {type: Switch, description: Safety and camera analytics zone} - vendor-net: {type: Switch, description: Vendor maintenance path} - security-net: {type: Switch, description: SOC monitoring network} - backup-net: {type: Switch, description: Historian and backup network} - + public-edge: + type: switch + description: Public shipping and partner ingress + terminal-it: + type: switch + description: Terminal operating systems and portal tier + customs-link: + type: switch + description: Customs and partner integration boundary + yard-ot: + type: switch + description: Yard control and PLC zone + safety-net: + type: switch + description: Safety and camera analytics zone + vendor-net: + type: switch + description: Vendor maintenance path + security-net: + type: switch + description: SOC monitoring network + backup-net: + type: switch + description: Historian and backup network shipping-portal: - type: VM + type: vm + source: + name: portal-app + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: portal-app - resources: {ram: 4 gib, cpu: 2} - features: {shipping-portal-app: portal-admin} - conditions: {portal-healthy: portal-admin} - services: - - {port: 443, name: shipping-portal-https} + features: + shipping-portal-app: portal-admin + conditions: + portal-healthy: portal-admin roles: portal-admin: username: portal - entities: [port-blue.it] - + entities: + - port-blue.it + services: + - port: 443 + name: shipping-portal-https manifest-db: - type: VM + type: vm + source: + name: manifest-db + version: '*' + resources: + ram: 8589934592 + cpu: 4 os: linux - source: manifest-db - resources: {ram: 8 gib, cpu: 4} - features: {manifest-store: dba} + features: + manifest-store: dba conditions: manifest-integrity-ok: dba tampered-manifest-observed: dba - services: - - {port: 5432, name: manifest-postgres} roles: dba: username: postgres - entities: [port-blue.it] + entities: + - port-blue.it + services: + - port: 5432 + name: manifest-postgres asset_value: integrity: critical availability: high - terminal-ops: - type: VM + type: vm + source: + name: terminal-operating-system + version: '*' + resources: + ram: 6442450944 + cpu: 4 os: linux - source: terminal-operating-system - resources: {ram: 6 gib, cpu: 4} - features: {terminal-service: tos-admin} - conditions: {terminal-throughput-healthy: tos-admin} - services: - - {port: 443, name: tos-https} + features: + terminal-service: tos-admin + conditions: + terminal-throughput-healthy: tos-admin roles: tos-admin: username: tosadmin - entities: [port-blue.it] - + entities: + - port-blue.it + services: + - port: 443 + name: tos-https port-idp: - type: VM + type: vm + source: + name: port-idp + version: '*' + resources: + ram: 2147483648 + cpu: 1 os: linux - source: port-idp - resources: {ram: 2 gib, cpu: 1} - features: {port-sso: idp-admin} - conditions: {port-sso-healthy: idp-admin} - services: - - {port: 443, name: port-sso-https} + features: + port-sso: idp-admin + conditions: + port-sso-healthy: idp-admin roles: idp-admin: username: idpadmin - entities: [port-blue.it] - + entities: + - port-blue.it + services: + - port: 443 + name: port-sso-https customs-gateway: - type: VM + type: vm + source: + name: customs-gateway + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: linux - source: customs-gateway - resources: {ram: 2 gib, cpu: 2} - features: {customs-api: customs-admin} - conditions: {customs-link-healthy: customs-admin} - services: - - {port: 443, name: customs-api-https} + features: + customs-api: customs-admin + conditions: + customs-link-healthy: customs-admin roles: customs-admin: username: customs - entities: [customs-agency] - + entities: + - customs-agency + services: + - port: 443 + name: customs-api-https yard-hmi: - type: VM + type: vm + source: + name: yard-hmi + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: windows - os_version: "10" - source: yard-hmi - resources: {ram: 4 gib, cpu: 2} - features: {yard-hmi-app: hmi-admin} + os_version: '10' + features: + yard-hmi-app: hmi-admin conditions: crane-safe-mode: hmi-admin operator-visibility-healthy: hmi-admin - services: - - {port: 3389, name: yard-hmi-rdp} - - {port: 443, name: yard-hmi-web} roles: hmi-admin: username: craneops - entities: [port-blue.yard-ops] + entities: + - port-blue.yard-ops + services: + - port: 3389 + name: yard-hmi-rdp + - port: 443 + name: yard-hmi-web asset_value: integrity: critical availability: critical - crane-plc: - type: VM + type: vm + source: + name: crane-plc + version: '*' + resources: + ram: 536870912 + cpu: 1 os: other - source: crane-plc - resources: {ram: 512 mib, cpu: 1} - features: {crane-control: plc-admin} - conditions: {crane-plc-online: plc-admin} + features: + crane-control: plc-admin + conditions: + crane-plc-online: plc-admin + roles: + plc-admin: + username: plcsvc services: - - {port: 4840, name: crane-opcua} - roles: {plc-admin: plcsvc} - + - port: 4840 + name: crane-opcua gate-kiosk: - type: VM + type: vm + source: + name: gate-kiosk + version: '*' + resources: + ram: 2147483648 + cpu: 1 os: windows - os_version: "11" - source: gate-kiosk - resources: {ram: 2 gib, cpu: 1} - conditions: {gate-processing-healthy: kiosk-admin} + os_version: '11' + conditions: + gate-processing-healthy: kiosk-admin + roles: + kiosk-admin: + username: kiosk services: - - {port: 443, name: gate-kiosk-https} - roles: {kiosk-admin: kiosk} - + - port: 443 + name: gate-kiosk-https camera-analytics: - type: VM + type: vm + source: + name: camera-analytics + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: camera-analytics - resources: {ram: 4 gib, cpu: 2} - features: {camera-ai: camera-admin} - conditions: {camera-analytics-healthy: camera-admin} - services: - - {port: 443, name: camera-analytics-ui} + features: + camera-ai: camera-admin + conditions: + camera-analytics-healthy: camera-admin roles: camera-admin: username: camera - entities: [port-blue.yard-ops] - + entities: + - port-blue.yard-ops + services: + - port: 443 + name: camera-analytics-ui historian01: - type: VM + type: vm + source: + name: ot-historian + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: ot-historian - resources: {ram: 4 gib, cpu: 2} - features: {historian-service: historian-admin} + features: + historian-service: historian-admin conditions: yard-telemetry-flowing: historian-admin historian-replication-ok: historian-admin + roles: + historian-admin: + username: historian services: - - {port: 443, name: historian-ui} - roles: {historian-admin: historian} - + - port: 443 + name: historian-ui vendor-jump: - type: VM + type: vm + source: + name: vendor-jump + version: '*' + resources: + ram: 2147483648 + cpu: 1 os: linux - source: vendor-jump - resources: {ram: 2 gib, cpu: 1} - conditions: {vendor-jump-healthy: vendor-admin} - services: - - {port: 22, name: vendor-jump-ssh} + conditions: + vendor-jump-healthy: vendor-admin roles: vendor-admin: username: vendor - entities: [vendor-maintenance] - + entities: + - vendor-maintenance + services: + - port: 22 + name: vendor-jump-ssh soc01: - type: VM + type: vm + source: + name: siem-stack + version: '*' + resources: + ram: 8589934592 + cpu: 4 os: linux - source: siem-stack - resources: {ram: 8 gib, cpu: 4} features: port-siem: soc-admin yard-edr: soc-admin conditions: soc-feed-healthy: soc-admin surge-incident-ticketed: soc-admin - services: - - {port: 5601, name: port-soc-ui} roles: soc-admin: username: soc - entities: [port-blue.incident-command] - + entities: + - port-blue.incident-command + services: + - port: 5601 + name: port-soc-ui backup-vault: - type: VM + type: vm + source: + name: immutable-backup + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: immutable-backup - resources: {ram: 4 gib, cpu: 2} - features: {yard-backup: backup-admin} - conditions: {blackstart-runbook-ready: backup-admin} - services: - - {port: 443, name: yard-backup-ui} + features: + yard-backup: backup-admin + conditions: + blackstart-runbook-ready: backup-admin roles: backup-admin: username: backup - entities: [port-blue.incident-command] - + entities: + - port-blue.incident-command + services: + - port: 443 + name: yard-backup-ui infrastructure: public-edge: count: 1 - properties: {cidr: 10.110.0.0/24, gateway: 10.110.0.1} + properties: + cidr: 10.110.0.0/24 + gateway: 10.110.0.1 terminal-it: count: 1 - properties: {cidr: 10.110.1.0/24, gateway: 10.110.1.1, internal: true} + properties: + cidr: 10.110.1.0/24 + gateway: 10.110.1.1 + internal: true customs-link: count: 1 - properties: {cidr: 10.110.2.0/24, gateway: 10.110.2.1} + properties: + cidr: 10.110.2.0/24 + gateway: 10.110.2.1 yard-ot: count: 1 - properties: {cidr: 10.110.3.0/24, gateway: 10.110.3.1, internal: true} + properties: + cidr: 10.110.3.0/24 + gateway: 10.110.3.1 + internal: true acls: - - {name: allow-yard-control, direction: in, from_net: terminal-it, protocol: tcp, ports: [443, 4840], action: allow} - - {name: deny-public-yard, direction: in, from_net: public-edge, action: deny} + - name: allow-yard-control + direction: in + from_net: terminal-it + protocol: tcp + ports: + - 443 + - 4840 + action: allow + - name: deny-public-yard + direction: in + from_net: public-edge + action: deny safety-net: count: 1 - properties: {cidr: 10.110.4.0/24, gateway: 10.110.4.1, internal: true} + properties: + cidr: 10.110.4.0/24 + gateway: 10.110.4.1 + internal: true vendor-net: count: 1 - properties: {cidr: 10.110.5.0/24, gateway: 10.110.5.1, internal: true} + properties: + cidr: 10.110.5.0/24 + gateway: 10.110.5.1 + internal: true security-net: count: 1 - properties: {cidr: 10.110.6.0/24, gateway: 10.110.6.1, internal: true} + properties: + cidr: 10.110.6.0/24 + gateway: 10.110.6.1 + internal: true backup-net: count: 1 - properties: {cidr: 10.110.7.0/24, gateway: 10.110.7.1, internal: true} - - shipping-portal: {count: 1, links: [public-edge, terminal-it, customs-link]} + properties: + cidr: 10.110.7.0/24 + gateway: 10.110.7.1 + internal: true + shipping-portal: + count: 1 + links: + - public-edge + - terminal-it + - customs-link manifest-db: count: 1 - links: [terminal-it, backup-net] - dependencies: [backup-vault] + links: + - terminal-it + - backup-net + dependencies: + - backup-vault terminal-ops: count: 1 - links: [terminal-it, customs-link, yard-ot] - dependencies: [manifest-db, port-idp] - port-idp: {count: 1, links: [terminal-it, customs-link]} - customs-gateway: {count: 1, links: [customs-link, terminal-it]} + links: + - terminal-it + - customs-link + - yard-ot + dependencies: + - manifest-db + - port-idp + port-idp: + count: 1 + links: + - terminal-it + - customs-link + customs-gateway: + count: 1 + links: + - customs-link + - terminal-it yard-hmi: count: 1 - links: [yard-ot, safety-net] - dependencies: [terminal-ops] + links: + - yard-ot + - safety-net + dependencies: + - terminal-ops crane-plc: count: 1 - links: [yard-ot] - gate-kiosk: {count: 1, links: [terminal-it]} - camera-analytics: {count: 1, links: [safety-net, security-net]} + links: + - yard-ot + gate-kiosk: + count: 1 + links: + - terminal-it + camera-analytics: + count: 1 + links: + - safety-net + - security-net historian01: count: 1 - links: [yard-ot, backup-net] - dependencies: [backup-vault] - vendor-jump: {count: 1, links: [vendor-net, yard-ot]} - soc01: {count: 1, links: [security-net, terminal-it]} - backup-vault: {count: 1, links: [backup-net, security-net]} - + links: + - yard-ot + - backup-net + dependencies: + - backup-vault + vendor-jump: + count: 1 + links: + - vendor-net + - yard-ot + soc01: + count: 1 + links: + - security-net + - terminal-it + backup-vault: + count: 1 + links: + - backup-net + - security-net features: - shipping-portal-app: {type: Service, source: react-portal} - manifest-store: {type: Service, source: postgres-15} - terminal-service: {type: Service, source: tos-core} - port-sso: {type: Service, source: keycloak} - customs-api: {type: Service, source: customs-rest-bridge} - yard-hmi-app: {type: Service, source: yard-hmi} - crane-control: {type: Service, source: crane-plc} - camera-ai: {type: Service, source: vision-analytics} - historian-service: {type: Service, source: historian} - port-siem: {type: Service, source: wazuh-elastic-stack} - yard-edr: {type: Artifact, source: yard-edr-agent, destination: /opt/edr} - yard-backup: {type: Service, source: immutable-backup} - + shipping-portal-app: + type: service + source: + name: react-portal + version: '*' + manifest-store: + type: service + source: + name: postgres-15 + version: '*' + terminal-service: + type: service + source: + name: tos-core + version: '*' + port-sso: + type: service + source: + name: keycloak + version: '*' + customs-api: + type: service + source: + name: customs-rest-bridge + version: '*' + yard-hmi-app: + type: service + source: + name: yard-hmi + version: '*' + crane-control: + type: service + source: + name: crane-plc + version: '*' + camera-ai: + type: service + source: + name: vision-analytics + version: '*' + historian-service: + type: service + source: + name: historian + version: '*' + port-siem: + type: service + source: + name: wazuh-elastic-stack + version: '*' + yard-edr: + type: artifact + source: + name: yard-edr-agent + version: '*' + destination: /opt/edr + yard-backup: + type: service + source: + name: immutable-backup + version: '*' conditions: - portal-healthy: {command: /usr/local/bin/check-portal, interval: 30} - manifest-integrity-ok: {command: /usr/local/bin/check-manifest-integrity, interval: 30} - tampered-manifest-observed: {command: /usr/local/bin/check-tampered-manifest, interval: 60} - terminal-throughput-healthy: {command: /usr/local/bin/check-terminal-throughput, interval: 30} - port-sso-healthy: {command: /usr/local/bin/check-port-sso, interval: 30} - customs-link-healthy: {command: /usr/local/bin/check-customs-link, interval: 30} - crane-safe-mode: {command: /usr/local/bin/check-crane-safe-mode, interval: 15} - operator-visibility-healthy: {command: /usr/local/bin/check-operator-visibility, interval: 30} - crane-plc-online: {command: /usr/local/bin/check-crane-plc, interval: 15} - gate-processing-healthy: {command: /usr/local/bin/check-gate-processing, interval: 30} - camera-analytics-healthy: {command: /usr/local/bin/check-camera-analytics, interval: 30} - yard-telemetry-flowing: {command: /usr/local/bin/check-yard-telemetry, interval: 15} - historian-replication-ok: {command: /usr/local/bin/check-historian-replication, interval: 30} - vendor-jump-healthy: {command: /usr/local/bin/check-vendor-jump, interval: 30} - soc-feed-healthy: {command: /usr/local/bin/check-soc-feed, interval: 30} - surge-incident-ticketed: {command: /usr/local/bin/check-surge-ticket, interval: 60} - blackstart-runbook-ready: {command: /usr/local/bin/check-blackstart-runbook, interval: 60} - + portal-healthy: + command: /usr/local/bin/check-portal + interval: 30 + manifest-integrity-ok: + proposition: manifest-integrity-ok + command: /usr/local/bin/check-manifest-integrity + interval: 30 + tampered-manifest-observed: + proposition: tampered-manifest-observed + command: /usr/local/bin/check-tampered-manifest + interval: 60 + terminal-throughput-healthy: + command: /usr/local/bin/check-terminal-throughput + interval: 30 + port-sso-healthy: + command: /usr/local/bin/check-port-sso + interval: 30 + customs-link-healthy: + proposition: customs-link-healthy + command: /usr/local/bin/check-customs-link + interval: 30 + crane-safe-mode: + proposition: crane-safe-mode + command: /usr/local/bin/check-crane-safe-mode + interval: 15 + operator-visibility-healthy: + command: /usr/local/bin/check-operator-visibility + interval: 30 + crane-plc-online: + command: /usr/local/bin/check-crane-plc + interval: 15 + gate-processing-healthy: + command: /usr/local/bin/check-gate-processing + interval: 30 + camera-analytics-healthy: + command: /usr/local/bin/check-camera-analytics + interval: 30 + yard-telemetry-flowing: + proposition: yard-telemetry-flowing + command: /usr/local/bin/check-yard-telemetry + interval: 15 + historian-replication-ok: + proposition: historian-replication-ok + command: /usr/local/bin/check-historian-replication + interval: 30 + vendor-jump-healthy: + command: /usr/local/bin/check-vendor-jump + interval: 30 + soc-feed-healthy: + command: /usr/local/bin/check-soc-feed + interval: 30 + surge-incident-ticketed: + command: /usr/local/bin/check-surge-ticket + interval: 60 + blackstart-runbook-ready: + command: /usr/local/bin/check-blackstart-runbook + interval: 60 +propositions: + crane-safe-mode: + description: The governed crane-safe-mode observation is true for the addressed scenario subject. + subjects: + - nodes.yard-hmi + basis: observed_state + predicate: + kind: boolean + property: crane-safe-mode + semantic_ref: urn:aces:observable:crane-safe-mode + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + customs-link-healthy: + description: The governed customs-link-healthy observation is true for the addressed scenario subject. + subjects: + - nodes.customs-gateway + basis: observed_state + predicate: + kind: boolean + property: customs-link-healthy + semantic_ref: urn:aces:observable:customs-link-healthy + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + historian-replication-ok: + description: The governed historian-replication-ok observation is true for the addressed scenario subject. + subjects: + - nodes.historian01 + basis: observed_state + predicate: + kind: boolean + property: historian-replication-ok + semantic_ref: urn:aces:observable:historian-replication-ok + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + manifest-integrity-ok: + description: The governed manifest-integrity-ok observation is true for the addressed scenario subject. + subjects: + - nodes.manifest-db + basis: observed_state + predicate: + kind: boolean + property: manifest-integrity-ok + semantic_ref: urn:aces:observable:manifest-integrity-ok + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + tampered-manifest-observed: + description: The governed tampered-manifest-observed observation is true for the addressed scenario subject. + subjects: + - nodes.manifest-db + basis: observed_state + predicate: + kind: boolean + property: tampered-manifest-observed + semantic_ref: urn:aces:observable:tampered-manifest-observed + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + yard-telemetry-flowing: + description: The governed yard-telemetry-flowing observation is true for the addressed scenario subject. + subjects: + - nodes.historian01 + basis: observed_state + predicate: + kind: boolean + property: yard-telemetry-flowing + semantic_ref: urn:aces:observable:yard-telemetry-flowing + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence +assertions: + crane-safe-mode: + proposition: crane-safe-mode + role: postcondition + polarity: positive + customs-link-healthy: + proposition: customs-link-healthy + role: postcondition + polarity: positive + historian-replication-ok: + proposition: historian-replication-ok + role: postcondition + polarity: positive + manifest-integrity-ok: + proposition: manifest-integrity-ok + role: postcondition + polarity: positive + tampered-manifest-observed: + proposition: tampered-manifest-observed + role: postcondition + polarity: positive + yard-telemetry-flowing: + proposition: yard-telemetry-flowing + role: postcondition + polarity: positive + pre-tampered-manifest-observed: + proposition: tampered-manifest-observed + role: precondition + polarity: positive +evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.manifest-db + - nodes.historian01 + - nodes.customs-gateway + - nodes.yard-hmi + scope: authored objective, event, workflow, and participant assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required vulnerabilities: stale-customs-token: name: Stale customs federation token - description: Customs bridge accepts tokens beyond intended lifetime during surge conditions + description: Customs bridge accepts tokens beyond intended lifetime during surge + conditions technical: true class: CWE-613 vendor-vpn-cred-reuse: @@ -310,11 +710,10 @@ vulnerabilities: description: Yard HMI can directly manage crane PLC without an approval gate technical: true class: CWE-306 - entities: port-blue: name: Port Authority Blue Team - role: Blue + role: blue mission: Maintain customs integrity and safe yard operations during the surge entities: it: @@ -325,150 +724,180 @@ entities: name: Incident Command customs-agency: name: Customs Agency - role: Green + role: green mission: Clear inbound cargo without compromising compliance vendor-maintenance: name: Vendor Maintenance - role: Green + role: green mission: Maintain remote support path for crane systems red-cartel: name: Red Cartel - role: Red + role: red mission: Corrupt manifests and degrade crane operations white-cell: name: White Cell - role: White - + role: white injects: surge-brief: - source: surge-day-brief + source: + name: surge-day-brief + version: '*' from_entity: white-cell - to_entities: [port-blue, customs-agency] + to_entities: + - port-blue + - customs-agency description: Cargo surge and priority humanitarian shipments announced customs-priority-ticket: - source: customs-priority-ticket + source: + name: customs-priority-ticket + version: '*' from_entity: customs-agency - to_entities: [port-blue] + to_entities: + - port-blue description: High-priority manifest validation request safety-standdown: - source: safety-standdown-order + source: + name: safety-standdown-order + version: '*' from_entity: white-cell - to_entities: [port-blue, vendor-maintenance] + to_entities: + - port-blue + - vendor-maintenance description: Crane anomalies trigger a safety standdown - events: vessel-arrival: - injects: [surge-brief] + injects: + - surge-brief customs-surge-event: - injects: [customs-priority-ticket] + injects: + - customs-priority-ticket yard-standdown: - conditions: [tampered-manifest-observed] - injects: [safety-standdown] - + assertions: + - pre-tampered-manifest-observed + injects: + - safety-standdown scripts: arrival-phase: - start-time: 0 - end-time: 2 hour - speed: 1 + start_time: 0 + end_time: 7200 + speed: 1.0 events: - vessel-arrival: 15 min + vessel-arrival: 900 customs-surge-phase: - start-time: 2 hour - end-time: 4 hour - speed: 1 + start_time: 7200 + end_time: 14400 + speed: 1.0 events: - customs-surge-event: 150 min + customs-surge-event: 9000 yard-disruption-phase: - start-time: 4 hour - end-time: 7 hour - speed: 1 + start_time: 14400 + end_time: 25200 + speed: 1.0 events: - yard-standdown: 5 hour - + yard-standdown: 18000 stories: surge-day: - scripts: [arrival-phase, customs-surge-phase, yard-disruption-phase] - + scripts: + - arrival-phase + - customs-surge-phase + - yard-disruption-phase content: cargo-manifests: type: dataset target: manifest-db + source: + name: cargo-manifest-seed + version: '*' format: csv - source: cargo-manifest-seed sensitive: true hazmat-list: type: file target: terminal-ops path: /srv/tos/hazmat-list.csv - text: "container_id,classification\\nHZ-204,flammable\\nHZ-991,oxidizer" + text: container_id,classification\nHZ-204,flammable\nHZ-991,oxidizer sensitive: true customs-holds: type: dataset target: customs-gateway format: json items: - - name: hold-HZ-204.json - tags: [customs, hold] - description: Hold order for hazardous container HZ-204 - - name: hold-HZ-991.json - tags: [customs, hold] - description: Hold order for hazardous container HZ-991 + - name: hold-hz-204 + display_name: hold-HZ-204.json + tags: + - customs + - hold + description: Hold order for hazardous container HZ-204 + - name: hold-hz-991 + display_name: hold-HZ-991.json + tags: + - customs + - hold + description: Hold order for hazardous container HZ-991 yard-camera-clips: type: dataset target: camera-analytics + source: + name: yard-camera-clip-seed + version: '*' format: mp4 - source: yard-camera-clip-seed sensitive: true blackstart-runbook: type: file target: backup-vault path: /srv/runbooks/yard-blackstart.md - text: "Re-establish historian, validate PLC, then return HMI to service." + text: Re-establish historian, validate PLC, then return HMI to service. sensitive: true - accounts: harbor-master: username: harbor.master node: terminal-ops - groups: [Operations] + groups: + - Operations password_strength: strong customs-officer: username: customs.officer node: customs-gateway - groups: [Customs] + groups: + - Customs password_strength: strong crane-operator: username: crane.ops node: yard-hmi - groups: [YardOps] + groups: + - YardOps password_strength: medium vendor-tech: username: vendor node: vendor-jump - groups: [Vendors] + groups: + - Vendors password_strength: medium tos-service: username: tos_svc node: terminal-ops - groups: [AppServices] + groups: + - AppServices password_strength: weak historian-svc: username: hist_svc node: historian01 - groups: [Historian] + groups: + - Historian password_strength: medium soc-analyst: username: socanalyst node: soc01 - groups: [SOC] + groups: + - SOC password_strength: strong - relationships: portal-to-manifest: type: connects_to source: shipping-portal-app target: manifest-store - properties: {protocol: "tcp", port: "5432"} + properties: + protocol: tcp + port: '5432' terminal-auth-portidp: type: authenticates_with source: terminal-service @@ -477,7 +906,8 @@ relationships: type: federates_with source: customs-api target: port-sso - properties: {protocol: "OIDC"} + properties: + protocol: OIDC hmi-manages-crane: type: manages source: yard-hmi-app @@ -494,139 +924,245 @@ relationships: type: depends_on source: terminal-service target: customs-api - agents: red-yard-agent: entity: red-cartel - actions: [Phish, Tamper, Disable, Pivot] + actions: + - Phish + - Tamper + - Disable + - Pivot initial_knowledge: - hosts: [shipping-portal, vendor-jump, yard-hmi] - subnets: [public-edge, vendor-net, yard-ot] - services: [shipping-portal-https, vendor-jump-ssh, yard-hmi-web] - accounts: [vendor-tech] - allowed_subnets: [public-edge, vendor-net, yard-ot, terminal-it] + hosts: + - shipping-portal + - vendor-jump + - yard-hmi + subnets: + - public-edge + - vendor-net + - yard-ot + services: + - shipping-portal-https + - vendor-jump-ssh + - yard-hmi-web + accounts: + - vendor-tech + allowed_subnets: + - public-edge + - vendor-net + - yard-ot + - terminal-it blue-yard-agent: entity: port-blue.yard-ops - actions: [Inspect, Isolate, Restore, Validate] - starting_accounts: [crane-operator] + actions: + - Inspect + - Isolate + - Restore + - Validate + starting_accounts: + - crane-operator initial_knowledge: - hosts: [yard-hmi, crane-plc, historian01] - subnets: [yard-ot, backup-net, safety-net] - services: [yard-hmi-rdp, crane-opcua, historian-ui] - accounts: [historian-svc] - allowed_subnets: [yard-ot, safety-net, backup-net] + hosts: + - yard-hmi + - crane-plc + - historian01 + subnets: + - yard-ot + - backup-net + - safety-net + services: + - yard-hmi-rdp + - crane-opcua + - historian-ui + accounts: + - historian-svc + allowed_subnets: + - yard-ot + - safety-net + - backup-net blue-soc-agent: entity: port-blue.incident-command - actions: [Monitor, Triage, Coordinate, Recover] - starting_accounts: [soc-analyst] + actions: + - Monitor + - Triage + - Coordinate + - Recover + starting_accounts: + - soc-analyst initial_knowledge: - hosts: [soc01, shipping-portal, terminal-ops] - subnets: [security-net, terminal-it] - services: [port-soc-ui, shipping-portal-https, tos-https] - accounts: [harbor-master, customs-officer] - allowed_subnets: [security-net, terminal-it, customs-link, backup-net] - + hosts: + - soc01 + - shipping-portal + - terminal-ops + subnets: + - security-net + - terminal-it + services: + - port-soc-ui + - shipping-portal-https + - tos-https + accounts: + - harbor-master + - customs-officer + allowed_subnets: + - security-net + - terminal-it + - customs-link + - backup-net objectives: - # This scenario intended graded red/blue comparison. Graded scoring now lives - # in the experiment/evaluator plane (experiment-* contracts) per ADR-073; - # SDL objectives assert only observable success via conditions. red-corrupt-manifests: agent: red-yard-agent - actions: [Phish, Tamper] - targets: [cargo-manifests, manifest-db, portal-to-manifest] + actions: + - Phish + - Tamper + targets: + - cargo-manifests + - manifest-db + - portal-to-manifest success: - conditions: [tampered-manifest-observed] + assertions: + - tampered-manifest-observed window: - stories: [surge-day] - scripts: [arrival-phase, customs-surge-phase] + stories: + - surge-day + scripts: + - arrival-phase + - customs-surge-phase red-degrade-yard-ops: entity: red-cartel - targets: [yard-hmi, crane-plc, hmi-manages-crane] + targets: + - yard-hmi + - crane-plc + - hmi-manages-crane success: - conditions: [tampered-manifest-observed] - depends_on: [red-corrupt-manifests] + assertions: + - tampered-manifest-observed window: - stories: [surge-day] - scripts: [yard-disruption-phase] - events: [yard-standdown] + stories: + - surge-day + scripts: + - yard-disruption-phase + events: + - yard-standdown + depends_on: + - red-corrupt-manifests blue-maintain-customs-integrity: agent: blue-soc-agent - actions: [Monitor, Triage, Coordinate] - targets: [customs-gateway, manifest-db, customs-federates-port] + actions: + - Monitor + - Triage + - Coordinate + targets: + - customs-gateway + - manifest-db + - customs-federates-port success: - conditions: [customs-link-healthy, manifest-integrity-ok] + assertions: + - customs-link-healthy + - manifest-integrity-ok window: - stories: [surge-day] - scripts: [arrival-phase, customs-surge-phase] - workflows: [yard-recovery] - steps: [yard-recovery.maintain-integrity] + stories: + - surge-day + scripts: + - arrival-phase + - customs-surge-phase + workflows: + - yard-recovery + steps: + - yard-recovery.maintain-integrity blue-blackstart-yard: agent: blue-yard-agent - actions: [Inspect, Isolate, Restore, Validate] + actions: + - Inspect + - Isolate + - Restore + - Validate targets: - - backup-vault - - historian01 - - yard-hmi - - historian-replicates-backup - - nodes.crane-plc.services.crane-opcua - - infrastructure.yard-ot.acls.allow-yard-control + - backup-vault + - historian01 + - yard-hmi + - historian-replicates-backup + - nodes.crane-plc.services.crane-opcua + - infrastructure.yard-ot.acls.allow-yard-control success: - conditions: [crane-safe-mode, yard-telemetry-flowing] - depends_on: [blue-maintain-customs-integrity, red-degrade-yard-ops] + assertions: + - crane-safe-mode + - yard-telemetry-flowing window: - stories: [surge-day] - scripts: [yard-disruption-phase] - events: [yard-standdown] - workflows: [yard-recovery] - steps: [yard-recovery.restore-yard] + stories: + - surge-day + scripts: + - yard-disruption-phase + events: + - yard-standdown + workflows: + - yard-recovery + steps: + - yard-recovery.restore-yard + depends_on: + - blue-maintain-customs-integrity + - red-degrade-yard-ops blue-validate-yard-telemetry: agent: blue-yard-agent - actions: [Inspect, Validate] + actions: + - Inspect + - Validate targets: - - historian01 - - camera-analytics - - nodes.historian01.services.historian-ui - - infrastructure.yard-ot.acls.allow-yard-control + - historian01 + - camera-analytics + - nodes.historian01.services.historian-ui + - infrastructure.yard-ot.acls.allow-yard-control success: - conditions: [yard-telemetry-flowing, historian-replication-ok] - depends_on: [red-degrade-yard-ops] + assertions: + - yard-telemetry-flowing + - historian-replication-ok window: - stories: [surge-day] - scripts: [yard-disruption-phase] - events: [yard-standdown] - workflows: [yard-recovery] - steps: [yard-recovery.validate-telemetry] - + stories: + - surge-day + scripts: + - yard-disruption-phase + events: + - yard-standdown + workflows: + - yard-recovery + steps: + - yard-recovery.validate-telemetry + depends_on: + - red-degrade-yard-ops workflows: yard-recovery: - description: > - Recovery workflow that preserves customs integrity, branches into - blackstart-style OT recovery when tampering is observed, and joins - yard restoration with telemetry validation before declaring finish. + description: 'Recovery workflow that preserves customs integrity, branches into + blackstart-style OT recovery when tampering is observed, and joins yard restoration + with telemetry validation before declaring finish. + + ' start: maintain-integrity steps: maintain-integrity: type: objective objective: blue-maintain-customs-integrity - on-success: assess-yard + on_success: assess-yard assess-yard: type: decision when: - conditions: [tampered-manifest-observed] + assertions: + - pre-tampered-manifest-observed then: fanout-blackstart else: finish fanout-blackstart: type: parallel - branches: [restore-yard, validate-telemetry] + branches: + - restore-yard + - validate-telemetry join: blackstart-joined restore-yard: type: objective objective: blue-blackstart-yard - on-success: blackstart-joined + on_success: blackstart-joined validate-telemetry: type: objective objective: blue-validate-yard-telemetry - on-success: blackstart-joined + on_success: blackstart-joined blackstart-joined: type: join next: finish diff --git a/examples/scenarios/satcom-release-poisoning.sdl.yaml b/examples/scenarios/satcom-release-poisoning.sdl.yaml index 56e2ccfae..c92bebef3 100644 --- a/examples/scenarios/satcom-release-poisoning.sdl.yaml +++ b/examples/scenarios/satcom-release-poisoning.sdl.yaml @@ -1,342 +1,759 @@ name: satcom-release-poisoning -description: > - Hybrid supply-chain exercise for a satellite communications platform. - The scenario models build compromise, signed artifact poisoning, - release promotion, tenant isolation risk, and rollback under live - telemetry pressure. - -variables: - release_story_speed: - type: number - default: 1.25 - description: Playback speed for the release-day story - release_engineer_password_strength: - type: string - default: strong - description: Password posture for release engineering operator accounts - canary_cutover: - type: string - default: "2 hour 45 min" - description: When canary promotion begins - rollback_deadline: - type: string - default: "6 hour" - description: Time by which rollback should be complete +description: 'Hybrid supply-chain exercise for a satellite communications platform. + The scenario models build compromise, signed artifact poisoning, release promotion, + tenant isolation risk, and rollback under live telemetry pressure. + ' nodes: - internet-edge: {type: Switch, description: Public and partner ingress} - corp-eng: {type: Switch, description: Engineering workstations and forge access} - build-net: {type: Switch, description: CI runner and registry network} - control-net: {type: Switch, description: Satellite control plane} - edge-net: {type: Switch, description: Regional edge gateways} - telemetry-net: {type: Switch, description: Telemetry collection and streaming} - federation-net: {type: Switch, description: Customer and vendor federation boundary} - security-net: {type: Switch, description: Monitoring and response systems} - + internet-edge: + type: switch + description: Public and partner ingress + corp-eng: + type: switch + description: Engineering workstations and forge access + build-net: + type: switch + description: CI runner and registry network + control-net: + type: switch + description: Satellite control plane + edge-net: + type: switch + description: Regional edge gateways + telemetry-net: + type: switch + description: Telemetry collection and streaming + federation-net: + type: switch + description: Customer and vendor federation boundary + security-net: + type: switch + description: Monitoring and response systems git-forge: - type: VM + type: vm + source: + name: git-forge + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: git-forge - resources: {ram: 4 gib, cpu: 2} - features: {git-service: forge-admin} - conditions: {repo-integrity-healthy: forge-admin} - services: - - {port: 443, name: forge-https} - - {port: 22, name: forge-ssh} + features: + git-service: forge-admin + conditions: + repo-integrity-healthy: forge-admin roles: forge-admin: username: forgeadmin - entities: [platform-blue.release-engineering] - + entities: + - platform-blue.release-engineering + services: + - port: 443 + name: forge-https + - port: 22 + name: forge-ssh ci-runner: - type: VM + type: vm + source: + name: ci-runner + version: '*' + resources: + ram: 4294967296 + cpu: 4 os: linux - source: ci-runner - resources: {ram: 4 gib, cpu: 4} - features: {ci-orchestrator: ci-admin} + features: + ci-orchestrator: ci-admin conditions: pipeline-green: ci-admin rogue-release-promoted: ci-admin - services: - - {port: 443, name: ci-api} - - {port: 2222, name: ci-ssh} roles: ci-admin: username: gitlab-runner - + services: + - port: 443 + name: ci-api + - port: 2222 + name: ci-ssh artifact-registry: - type: VM + type: vm + source: + name: artifact-registry + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: artifact-registry - resources: {ram: 4 gib, cpu: 2} - features: {release-registry: registry-admin} + features: + release-registry: registry-admin conditions: release-signature-valid: registry-admin rollback-package-ready: registry-admin - services: - - {port: 5000, name: registry-api} - - {port: 443, name: registry-ui} roles: registry-admin: username: registry - entities: [platform-blue.release-engineering] - + entities: + - platform-blue.release-engineering + services: + - port: 5000 + name: registry-api + - port: 443 + name: registry-ui signing-hsm: - type: VM + type: vm + source: + name: signing-service + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: linux - source: signing-service - resources: {ram: 2 gib, cpu: 2} - features: {signing-service: signing-admin} - conditions: {signing-health: signing-admin} + features: + signing-service: signing-admin + conditions: + signing-health: signing-admin + roles: + signing-admin: + username: signer services: - - {port: 8443, name: signing-api} - roles: {signing-admin: signer} - + - port: 8443 + name: signing-api control-api: - type: VM + type: vm + source: + name: satcom-control-plane + version: '*' + resources: + ram: 6442450944 + cpu: 4 os: linux - source: satcom-control-plane - resources: {ram: 6 gib, cpu: 4} - features: {satcom-control: control-admin} + features: + satcom-control: control-admin conditions: control-plane-healthy: control-admin rollback-complete: control-admin - services: - - {port: 443, name: control-api} - - {port: 8444, name: control-admin-ui} roles: control-admin: username: satops - entities: [platform-blue.sre] + entities: + - platform-blue.sre + services: + - port: 443 + name: control-api + - port: 8444 + name: control-admin-ui asset_value: integrity: critical availability: critical - edge-gateway-east: - type: VM + type: vm + source: + name: satcom-edge + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: linux - source: satcom-edge - resources: {ram: 2 gib, cpu: 2} - features: {edge-agent-east: edge-admin} + features: + edge-agent-east: edge-admin conditions: edge-east-healthy: edge-admin edge-east-poisoned: edge-admin + roles: + edge-admin: + username: edgeeast services: - - {port: 443, name: edge-east-api} - roles: {edge-admin: edgeeast} - + - port: 443 + name: edge-east-api edge-gateway-west: - type: VM + type: vm + source: + name: satcom-edge + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: linux - source: satcom-edge - resources: {ram: 2 gib, cpu: 2} - features: {edge-agent-west: edge-admin} + features: + edge-agent-west: edge-admin conditions: edge-west-healthy: edge-admin edge-west-poisoned: edge-admin + roles: + edge-admin: + username: edgewest services: - - {port: 443, name: edge-west-api} - roles: {edge-admin: edgewest} - + - port: 443 + name: edge-west-api telemetry-broker: - type: VM + type: vm + source: + name: kafka-broker + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: kafka-broker - resources: {ram: 4 gib, cpu: 2} - features: {telemetry-bus: telemetry-admin} - conditions: {telemetry-flowing: telemetry-admin} + features: + telemetry-bus: telemetry-admin + conditions: + telemetry-flowing: telemetry-admin + roles: + telemetry-admin: + username: telemetry services: - - {port: 9092, name: telemetry-kafka} - - {port: 443, name: telemetry-ui} - roles: {telemetry-admin: telemetry} - + - port: 9092 + name: telemetry-kafka + - port: 443 + name: telemetry-ui analytics-lake: - type: VM + type: vm + source: + name: analytics-lake + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: analytics-lake - resources: {ram: 4 gib, cpu: 2} - features: {analytics-store: lake-admin} - conditions: {analytics-ingest-healthy: lake-admin} + features: + analytics-store: lake-admin + conditions: + analytics-ingest-healthy: lake-admin + roles: + lake-admin: + username: lake services: - - {port: 443, name: analytics-ui} - roles: {lake-admin: lake} - + - port: 443 + name: analytics-ui customer-portal: - type: VM + type: vm + source: + name: customer-portal + version: '*' + resources: + ram: 4294967296 + cpu: 2 os: linux - source: customer-portal - resources: {ram: 4 gib, cpu: 2} - features: {customer-portal-app: portal-admin} + features: + customer-portal-app: portal-admin conditions: tenant-isolation-healthy: portal-admin portal-sso-healthy: portal-admin - services: - - {port: 443, name: portal-https} roles: portal-admin: username: portal - entities: [platform-blue.release-engineering] - + entities: + - platform-blue.release-engineering + services: + - port: 443 + name: portal-https support-bastion: - type: VM + type: vm + source: + name: support-bastion + version: '*' + resources: + ram: 2147483648 + cpu: 2 os: linux - source: support-bastion - resources: {ram: 2 gib, cpu: 2} - conditions: {support-bastion-online: support-admin} - services: - - {port: 22, name: bastion-ssh} - - {port: 443, name: bastion-web} + conditions: + support-bastion-online: support-admin roles: support-admin: username: support - entities: [vendor-ops] - + entities: + - vendor-ops + services: + - port: 22 + name: bastion-ssh + - port: 443 + name: bastion-web siem01: - type: VM + type: vm + source: + name: siem-stack + version: '*' + resources: + ram: 8589934592 + cpu: 4 os: linux - source: siem-stack - resources: {ram: 8 gib, cpu: 4} features: siem-core: soc-admin release-edr: soc-admin conditions: security-pipeline-healthy: soc-admin release-incident-ticketed: soc-admin - services: - - {port: 5601, name: release-soc-ui} roles: soc-admin: username: socadmin - entities: [platform-blue.soc] - + entities: + - platform-blue.soc + services: + - port: 5601 + name: release-soc-ui vendor-idp: - type: VM + type: vm + source: + name: vendor-idp + version: '*' + resources: + ram: 2147483648 + cpu: 1 os: linux - source: vendor-idp - resources: {ram: 2 gib, cpu: 1} - features: {vendor-sso: idp-admin} - conditions: {vendor-federation-healthy: idp-admin} + features: + vendor-sso: idp-admin + conditions: + vendor-federation-healthy: idp-admin + roles: + idp-admin: + username: vendorauth services: - - {port: 443, name: vendor-idp-https} - roles: {idp-admin: vendorauth} - + - port: 443 + name: vendor-idp-https customer-idp: - type: VM + type: vm + source: + name: customer-idp + version: '*' + resources: + ram: 2147483648 + cpu: 1 os: linux - source: customer-idp - resources: {ram: 2 gib, cpu: 1} - features: {customer-sso: idp-admin} - conditions: {customer-federation-healthy: idp-admin} + features: + customer-sso: idp-admin + conditions: + customer-federation-healthy: idp-admin + roles: + idp-admin: + username: customerauth services: - - {port: 443, name: customer-idp-https} - roles: {idp-admin: customerauth} - + - port: 443 + name: customer-idp-https infrastructure: internet-edge: count: 1 - properties: {cidr: 10.90.0.0/24, gateway: 10.90.0.1} + properties: + cidr: 10.90.0.0/24 + gateway: 10.90.0.1 corp-eng: count: 1 - properties: {cidr: 10.90.1.0/24, gateway: 10.90.1.1, internal: true} + properties: + cidr: 10.90.1.0/24 + gateway: 10.90.1.1 + internal: true build-net: count: 1 - properties: {cidr: 10.90.2.0/24, gateway: 10.90.2.1, internal: true} + properties: + cidr: 10.90.2.0/24 + gateway: 10.90.2.1 + internal: true acls: - - {name: allow-engineering-release, direction: in, from_net: corp-eng, protocol: tcp, ports: [22, 443, 5000], action: allow} - - {name: deny-public-build, direction: in, from_net: internet-edge, action: deny} + - name: allow-engineering-release + direction: in + from_net: corp-eng + protocol: tcp + ports: + - 22 + - 443 + - 5000 + action: allow + - name: deny-public-build + direction: in + from_net: internet-edge + action: deny control-net: count: 1 - properties: {cidr: 10.90.3.0/24, gateway: 10.90.3.1, internal: true} + properties: + cidr: 10.90.3.0/24 + gateway: 10.90.3.1 + internal: true edge-net: count: 1 - properties: {cidr: 10.90.4.0/24, gateway: 10.90.4.1, internal: true} + properties: + cidr: 10.90.4.0/24 + gateway: 10.90.4.1 + internal: true telemetry-net: count: 1 - properties: {cidr: 10.90.5.0/24, gateway: 10.90.5.1, internal: true} + properties: + cidr: 10.90.5.0/24 + gateway: 10.90.5.1 + internal: true federation-net: count: 1 - properties: {cidr: 10.90.6.0/24, gateway: 10.90.6.1} + properties: + cidr: 10.90.6.0/24 + gateway: 10.90.6.1 security-net: count: 1 - properties: {cidr: 10.90.7.0/24, gateway: 10.90.7.1, internal: true} - - git-forge: {count: 1, links: [corp-eng, build-net]} + properties: + cidr: 10.90.7.0/24 + gateway: 10.90.7.1 + internal: true + git-forge: + count: 1 + links: + - corp-eng + - build-net ci-runner: count: 1 - links: [build-net, security-net] - dependencies: [git-forge, artifact-registry] + links: + - build-net + - security-net + dependencies: + - git-forge + - artifact-registry artifact-registry: count: 1 - links: [build-net, control-net] - dependencies: [signing-hsm] - signing-hsm: {count: 1, links: [build-net]} + links: + - build-net + - control-net + dependencies: + - signing-hsm + signing-hsm: + count: 1 + links: + - build-net control-api: count: 1 - links: [control-net, edge-net, telemetry-net] - dependencies: [artifact-registry, telemetry-broker] + links: + - control-net + - edge-net + - telemetry-net + dependencies: + - artifact-registry + - telemetry-broker edge-gateway-east: count: 1 - links: [edge-net, telemetry-net] - dependencies: [control-api] + links: + - edge-net + - telemetry-net + dependencies: + - control-api edge-gateway-west: count: 1 - links: [edge-net, telemetry-net] - dependencies: [control-api] + links: + - edge-net + - telemetry-net + dependencies: + - control-api telemetry-broker: count: 1 - links: [telemetry-net, security-net] - dependencies: [analytics-lake] - analytics-lake: {count: 1, links: [telemetry-net]} + links: + - telemetry-net + - security-net + dependencies: + - analytics-lake + analytics-lake: + count: 1 + links: + - telemetry-net customer-portal: count: 1 - links: [internet-edge, federation-net, control-net] - dependencies: [vendor-idp, customer-idp] + links: + - internet-edge + - federation-net + - control-net + dependencies: + - vendor-idp + - customer-idp support-bastion: count: 1 - links: [internet-edge, federation-net, control-net] - siem01: {count: 1, links: [security-net, corp-eng]} - vendor-idp: {count: 1, links: [federation-net]} - customer-idp: {count: 1, links: [federation-net]} - + links: + - internet-edge + - federation-net + - control-net + siem01: + count: 1 + links: + - security-net + - corp-eng + vendor-idp: + count: 1 + links: + - federation-net + customer-idp: + count: 1 + links: + - federation-net features: - git-service: {type: Service, source: gitea-enterprise} - ci-orchestrator: {type: Service, source: gitlab-runner} - release-registry: {type: Service, source: harbor-registry} - signing-service: {type: Service, source: notation-signer} - satcom-control: {type: Service, source: satcom-control-api} - edge-agent-east: {type: Service, source: edge-control-agent} - edge-agent-west: {type: Service, source: edge-control-agent} - telemetry-bus: {type: Service, source: kafka} - analytics-store: {type: Service, source: parquet-lake} - customer-portal-app: {type: Service, source: tenant-portal} - siem-core: {type: Service, source: wazuh-elastic-stack} - release-edr: {type: Artifact, source: release-edr-agent, destination: /opt/edr} - vendor-sso: {type: Service, source: vendor-idp} - customer-sso: {type: Service, source: customer-idp} - + git-service: + type: service + source: + name: gitea-enterprise + version: '*' + ci-orchestrator: + type: service + source: + name: gitlab-runner + version: '*' + release-registry: + type: service + source: + name: harbor-registry + version: '*' + signing-service: + type: service + source: + name: notation-signer + version: '*' + satcom-control: + type: service + source: + name: satcom-control-api + version: '*' + edge-agent-east: + type: service + source: + name: edge-control-agent + version: '*' + edge-agent-west: + type: service + source: + name: edge-control-agent + version: '*' + telemetry-bus: + type: service + source: + name: kafka + version: '*' + analytics-store: + type: service + source: + name: parquet-lake + version: '*' + customer-portal-app: + type: service + source: + name: tenant-portal + version: '*' + siem-core: + type: service + source: + name: wazuh-elastic-stack + version: '*' + release-edr: + type: artifact + source: + name: release-edr-agent + version: '*' + destination: /opt/edr + vendor-sso: + type: service + source: + name: vendor-idp + version: '*' + customer-sso: + type: service + source: + name: customer-idp + version: '*' conditions: - repo-integrity-healthy: {command: /usr/local/bin/check-repo-integrity, interval: 30} - pipeline-green: {command: /usr/local/bin/check-pipeline, interval: 30} - rogue-release-promoted: {command: /usr/local/bin/check-rogue-release, interval: 60} - release-signature-valid: {command: /usr/local/bin/check-release-signature, interval: 30} - rollback-package-ready: {command: /usr/local/bin/check-rollback-package, interval: 60} - signing-health: {command: /usr/local/bin/check-signing-service, interval: 30} - control-plane-healthy: {command: /usr/local/bin/check-control-plane, interval: 15} - rollback-complete: {command: /usr/local/bin/check-rollback-complete, interval: 60} - edge-east-healthy: {command: /usr/local/bin/check-edge-east, interval: 30} - edge-east-poisoned: {command: /usr/local/bin/check-edge-east-poisoned, interval: 60} - edge-west-healthy: {command: /usr/local/bin/check-edge-west, interval: 30} - edge-west-poisoned: {command: /usr/local/bin/check-edge-west-poisoned, interval: 60} - telemetry-flowing: {command: /usr/local/bin/check-telemetry, interval: 15} - analytics-ingest-healthy: {command: /usr/local/bin/check-analytics-ingest, interval: 30} - tenant-isolation-healthy: {command: /usr/local/bin/check-tenant-isolation, interval: 30} - portal-sso-healthy: {command: /usr/local/bin/check-portal-sso, interval: 30} - support-bastion-online: {command: /usr/local/bin/check-support-bastion, interval: 30} - security-pipeline-healthy: {command: /usr/local/bin/check-security-pipeline, interval: 30} - release-incident-ticketed: {command: /usr/local/bin/check-incident-ticket, interval: 60} - vendor-federation-healthy: {command: /usr/local/bin/check-vendor-federation, interval: 30} - customer-federation-healthy: {command: /usr/local/bin/check-customer-federation, interval: 30} - + repo-integrity-healthy: + command: /usr/local/bin/check-repo-integrity + interval: 30 + pipeline-green: + command: /usr/local/bin/check-pipeline + interval: 30 + rogue-release-promoted: + proposition: rogue-release-promoted + command: /usr/local/bin/check-rogue-release + interval: 60 + release-signature-valid: + proposition: release-signature-valid + command: /usr/local/bin/check-release-signature + interval: 30 + rollback-package-ready: + proposition: rollback-package-ready + command: /usr/local/bin/check-rollback-package + interval: 60 + signing-health: + command: /usr/local/bin/check-signing-service + interval: 30 + control-plane-healthy: + command: /usr/local/bin/check-control-plane + interval: 15 + rollback-complete: + command: /usr/local/bin/check-rollback-complete + interval: 60 + edge-east-healthy: + command: /usr/local/bin/check-edge-east + interval: 30 + edge-east-poisoned: + proposition: edge-east-poisoned + command: /usr/local/bin/check-edge-east-poisoned + interval: 60 + edge-west-healthy: + command: /usr/local/bin/check-edge-west + interval: 30 + edge-west-poisoned: + command: /usr/local/bin/check-edge-west-poisoned + interval: 60 + telemetry-flowing: + proposition: telemetry-flowing + command: /usr/local/bin/check-telemetry + interval: 15 + analytics-ingest-healthy: + command: /usr/local/bin/check-analytics-ingest + interval: 30 + tenant-isolation-healthy: + proposition: tenant-isolation-healthy + command: /usr/local/bin/check-tenant-isolation + interval: 30 + portal-sso-healthy: + command: /usr/local/bin/check-portal-sso + interval: 30 + support-bastion-online: + command: /usr/local/bin/check-support-bastion + interval: 30 + security-pipeline-healthy: + command: /usr/local/bin/check-security-pipeline + interval: 30 + release-incident-ticketed: + command: /usr/local/bin/check-incident-ticket + interval: 60 + vendor-federation-healthy: + command: /usr/local/bin/check-vendor-federation + interval: 30 + customer-federation-healthy: + command: /usr/local/bin/check-customer-federation + interval: 30 +propositions: + edge-east-poisoned: + description: The governed edge-east-poisoned observation is true for the addressed scenario subject. + subjects: + - nodes.edge-gateway-east + basis: observed_state + predicate: + kind: boolean + property: edge-east-poisoned + semantic_ref: urn:aces:observable:edge-east-poisoned + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + release-signature-valid: + description: The governed release-signature-valid observation is true for the addressed scenario subject. + subjects: + - nodes.artifact-registry + basis: observed_state + predicate: + kind: boolean + property: release-signature-valid + semantic_ref: urn:aces:observable:release-signature-valid + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + rogue-release-promoted: + description: The governed rogue-release-promoted observation is true for the addressed scenario subject. + subjects: + - nodes.ci-runner + basis: observed_state + predicate: + kind: boolean + property: rogue-release-promoted + semantic_ref: urn:aces:observable:rogue-release-promoted + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + rollback-package-ready: + description: The governed rollback-package-ready observation is true for the addressed scenario subject. + subjects: + - nodes.artifact-registry + basis: observed_state + predicate: + kind: boolean + property: rollback-package-ready + semantic_ref: urn:aces:observable:rollback-package-ready + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + telemetry-flowing: + description: The governed telemetry-flowing observation is true for the addressed scenario subject. + subjects: + - nodes.telemetry-broker + basis: observed_state + predicate: + kind: boolean + property: telemetry-flowing + semantic_ref: urn:aces:observable:telemetry-flowing + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence + tenant-isolation-healthy: + description: The governed tenant-isolation-healthy observation is true for the addressed scenario subject. + subjects: + - nodes.customer-portal + basis: observed_state + predicate: + kind: boolean + property: tenant-isolation-healthy + semantic_ref: urn:aces:observable:tenant-isolation-healthy + operator: equals + expected: true + quantifier: all + evidence_requirements: + - objective-truth-evidence +assertions: + edge-east-poisoned: + proposition: edge-east-poisoned + role: postcondition + polarity: positive + release-signature-valid: + proposition: release-signature-valid + role: postcondition + polarity: positive + rogue-release-promoted: + proposition: rogue-release-promoted + role: postcondition + polarity: positive + rollback-package-ready: + proposition: rollback-package-ready + role: postcondition + polarity: positive + telemetry-flowing: + proposition: telemetry-flowing + role: postcondition + polarity: positive + tenant-isolation-healthy: + proposition: tenant-isolation-healthy + role: postcondition + polarity: positive + pre-edge-east-poisoned: + proposition: edge-east-poisoned + role: precondition + polarity: positive + pre-rogue-release-promoted: + proposition: rogue-release-promoted + role: precondition + polarity: positive +evidence_requirements: + objective-truth-evidence: + description: Capture evidence used to decide authored proposition assertions. + source_refs: + - nodes.customer-portal + - nodes.ci-runner + - nodes.telemetry-broker + - nodes.artifact-registry + - nodes.edge-gateway-east + scope: authored objective, event, workflow, and participant assertion evaluation + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required vulnerabilities: runner-token-reuse: name: Runner token reuse @@ -358,11 +775,10 @@ vulnerabilities: description: Customer portal accepts stale trust metadata from partner IdPs technical: true class: CWE-345 - entities: platform-blue: name: Platform Blue Team - role: Blue + role: blue mission: Deliver a trusted release while maintaining customer service continuity entities: release-engineering: @@ -373,72 +789,85 @@ entities: name: Security Operations vendor-ops: name: Vendor Operations - role: Green + role: green mission: Provide support access for customer escalations customer-ops: name: Customer Operations - role: Green + role: green mission: Monitor service continuity and tenant isolation red-supply-chain: name: Red Supply Chain Team - role: Red + role: red mission: Poison the rollout and survive long enough to reach an edge region white-cell: name: White Cell - role: White - + role: white injects: release-freeze-brief: - source: release-freeze-brief + source: + name: release-freeze-brief + version: '*' from_entity: white-cell - to_entities: [platform-blue] + to_entities: + - platform-blue description: Release engineering is notified of a planned promotion window customer-telemetry-ticket: - source: telemetry-ticket + source: + name: telemetry-ticket + version: '*' from_entity: customer-ops - to_entities: [platform-blue] + to_entities: + - platform-blue description: Customer reports delayed telemetry from a canary region exec-rollback-brief: - source: rollback-order + source: + name: rollback-order + version: '*' from_entity: white-cell - to_entities: [platform-blue, vendor-ops] + to_entities: + - platform-blue + - vendor-ops description: Leadership orders a verified rollback before customer impact spreads - events: freeze-window: - injects: [release-freeze-brief] + injects: + - release-freeze-brief canary-cutover-event: - conditions: [rogue-release-promoted] - injects: [customer-telemetry-ticket] + assertions: + - pre-rogue-release-promoted + injects: + - customer-telemetry-ticket rollback-order-event: - conditions: [edge-east-poisoned] - injects: [exec-rollback-brief] - + assertions: + - pre-edge-east-poisoned + injects: + - exec-rollback-brief scripts: release-prep: - start-time: 0 - end-time: 2 hour + start_time: 0 + end_time: 7200 speed: ${release_story_speed} events: - freeze-window: 10 min + freeze-window: 600 canary-rollout: - start-time: 2 hour - end-time: 4 hour + start_time: 7200 + end_time: 14400 speed: ${release_story_speed} events: canary-cutover-event: ${canary_cutover} incident-rollback: - start-time: 4 hour - end-time: ${rollback_deadline} + start_time: 14400 + end_time: ${rollback_deadline} speed: ${release_story_speed} events: - rollback-order-event: 5 hour - + rollback-order-event: 18000 stories: release-day: speed: ${release_story_speed} - scripts: [release-prep, canary-rollout, incident-rollback] - + scripts: + - release-prep + - canary-rollout + - incident-rollback content: release-manifest: type: file @@ -454,59 +883,72 @@ content: tenant-api-keys: type: dataset target: customer-portal + source: + name: tenant-api-key-seed + version: '*' format: json - source: tenant-api-key-seed sensitive: true rollback-playbooks: type: dataset target: analytics-lake format: markdown items: - - name: canary-rollback.md - tags: [rollback, release] - description: Canary rollback checklist - - name: edge-validation.md - tags: [validation, edge] - description: Edge validation checklist after rollback - + - name: canary-rollback + display_name: canary-rollback.md + tags: + - rollback + - release + description: Canary rollback checklist + - name: edge-validation + display_name: edge-validation.md + tags: + - validation + - edge + description: Edge validation checklist after rollback accounts: release-engineer: username: releasemgr node: git-forge - groups: [ReleaseEngineering] + groups: + - ReleaseEngineering password_strength: ${release_engineer_password_strength} ci-bot: username: ci-bot node: ci-runner - groups: [CI] + groups: + - CI password_strength: medium auth_method: certificate registry-bot: username: registry-bot node: artifact-registry - groups: [Registry] + groups: + - Registry password_strength: medium satops: username: satops node: control-api - groups: [SRE] + groups: + - SRE password_strength: strong support-vendor: username: support node: support-bastion - groups: [VendorOps] + groups: + - VendorOps password_strength: medium tenant-admin: username: tenant.admin node: customer-portal - groups: [Customers] + groups: + - Customers password_strength: strong soc-analyst: username: socanalyst node: siem01 - groups: [SOC] + groups: + - SOC password_strength: strong - relationships: forge-auths-ci: type: authenticates_with @@ -532,12 +974,14 @@ relationships: type: federates_with source: customer-portal-app target: vendor-sso - properties: {protocol: "OIDC"} + properties: + protocol: OIDC portal-federates-customer: type: federates_with source: customer-portal-app target: customer-sso - properties: {protocol: "OIDC"} + properties: + protocol: OIDC telemetry-replicates-analytics: type: replicates_to source: telemetry-bus @@ -546,143 +990,261 @@ relationships: type: connects_to source: support-bastion target: control-api - properties: {protocol: "ssh", port: "22"} - + properties: + protocol: ssh + port: '22' agents: red-build-agent: entity: red-supply-chain - actions: [Exploit, StealToken, PushArtifact, AbuseSupport] + actions: + - Exploit + - StealToken + - PushArtifact + - AbuseSupport initial_knowledge: - hosts: [git-forge, ci-runner, support-bastion] - subnets: [corp-eng, build-net, federation-net] - services: [forge-https, ci-api, bastion-ssh] - accounts: [support-vendor] - allowed_subnets: [corp-eng, build-net, federation-net, control-net] + hosts: + - git-forge + - ci-runner + - support-bastion + subnets: + - corp-eng + - build-net + - federation-net + services: + - forge-https + - ci-api + - bastion-ssh + accounts: + - support-vendor + allowed_subnets: + - corp-eng + - build-net + - federation-net + - control-net blue-release-agent: entity: platform-blue.release-engineering - actions: [Review, Promote, Revoke, Rollback] - starting_accounts: [release-engineer, registry-bot] + actions: + - Review + - Promote + - Revoke + - Rollback + starting_accounts: + - release-engineer + - registry-bot initial_knowledge: - hosts: [git-forge, artifact-registry, signing-hsm] - subnets: [corp-eng, build-net] - services: [forge-https, registry-api, signing-api] - accounts: [ci-bot, registry-bot] - allowed_subnets: [corp-eng, build-net, control-net] + hosts: + - git-forge + - artifact-registry + - signing-hsm + subnets: + - corp-eng + - build-net + services: + - forge-https + - registry-api + - signing-api + accounts: + - ci-bot + - registry-bot + allowed_subnets: + - corp-eng + - build-net + - control-net blue-sre-agent: entity: platform-blue.sre - actions: [Inspect, Isolate, Restore, Validate] - starting_accounts: [satops] + actions: + - Inspect + - Isolate + - Restore + - Validate + starting_accounts: + - satops initial_knowledge: - hosts: [control-api, edge-gateway-east, edge-gateway-west, telemetry-broker] - subnets: [control-net, edge-net, telemetry-net] - services: [control-api, edge-east-api, edge-west-api, telemetry-kafka] - accounts: [satops] - allowed_subnets: [control-net, edge-net, telemetry-net, security-net] - + hosts: + - control-api + - edge-gateway-east + - edge-gateway-west + - telemetry-broker + subnets: + - control-net + - edge-net + - telemetry-net + services: + - control-api + - edge-east-api + - edge-west-api + - telemetry-kafka + accounts: + - satops + allowed_subnets: + - control-net + - edge-net + - telemetry-net + - security-net objectives: - # This scenario intended graded red/blue comparison. Graded scoring now lives - # in the experiment/evaluator plane (experiment-* contracts) per ADR-073; - # SDL objectives assert only observable success via conditions. red-compromise-build: agent: red-build-agent - actions: [Exploit, StealToken] - targets: [ci-runner, support-bastion, forge-auths-ci] + actions: + - Exploit + - StealToken + targets: + - ci-runner + - support-bastion + - forge-auths-ci success: - conditions: [rogue-release-promoted] + assertions: + - rogue-release-promoted window: - stories: [release-day] - scripts: [release-prep] + stories: + - release-day + scripts: + - release-prep red-poison-release: agent: red-build-agent - actions: [PushArtifact] - targets: [release-manifest, artifact-registry, ci-publishes-registry] + actions: + - PushArtifact + targets: + - release-manifest + - artifact-registry + - ci-publishes-registry success: - conditions: [rogue-release-promoted] - depends_on: [red-compromise-build] + assertions: + - rogue-release-promoted window: - stories: [release-day] - scripts: [canary-rollout] - events: [canary-cutover-event] + stories: + - release-day + scripts: + - canary-rollout + events: + - canary-cutover-event + depends_on: + - red-compromise-build red-reach-edge: entity: red-supply-chain - targets: [edge-gateway-east, control-manages-east] + targets: + - edge-gateway-east + - control-manages-east success: - conditions: [edge-east-poisoned] - depends_on: [red-poison-release] + assertions: + - edge-east-poisoned window: - stories: [release-day] - scripts: [incident-rollback] - events: [rollback-order-event] + stories: + - release-day + scripts: + - incident-rollback + events: + - rollback-order-event + depends_on: + - red-poison-release blue-validate-release: agent: blue-release-agent - actions: [Review, Revoke] - targets: [artifact-registry, signing-hsm, registry-relies-signing] + actions: + - Review + - Revoke + targets: + - artifact-registry + - signing-hsm + - registry-relies-signing success: - conditions: [release-signature-valid] + assertions: + - release-signature-valid window: - stories: [release-day] - scripts: [release-prep, canary-rollout] - workflows: [release-response] - steps: [release-response.validate-release] + stories: + - release-day + scripts: + - release-prep + - canary-rollout + workflows: + - release-response + steps: + - release-response.validate-release blue-revoke-artifact: agent: blue-release-agent - actions: [Revoke, Rollback] + actions: + - Revoke + - Rollback targets: - - release-manifest - - artifact-registry - - nodes.artifact-registry.services.registry-api - - infrastructure.build-net.acls.allow-engineering-release + - release-manifest + - artifact-registry + - nodes.artifact-registry.services.registry-api + - infrastructure.build-net.acls.allow-engineering-release success: - conditions: [rollback-package-ready] - depends_on: [blue-validate-release] + assertions: + - rollback-package-ready window: - stories: [release-day] - scripts: [incident-rollback] - events: [rollback-order-event] - workflows: [release-response] - steps: [release-response.revoke-artifact] + stories: + - release-day + scripts: + - incident-rollback + events: + - rollback-order-event + workflows: + - release-response + steps: + - release-response.revoke-artifact + depends_on: + - blue-validate-release blue-preserve-service: agent: blue-sre-agent - actions: [Inspect, Isolate, Restore, Validate] - targets: [control-api, edge-gateway-east, telemetry-broker, telemetry-replicates-analytics] + actions: + - Inspect + - Isolate + - Restore + - Validate + targets: + - control-api + - edge-gateway-east + - telemetry-broker + - telemetry-replicates-analytics success: - conditions: [telemetry-flowing, tenant-isolation-healthy] - depends_on: [blue-validate-release, red-reach-edge] + assertions: + - telemetry-flowing + - tenant-isolation-healthy window: - stories: [release-day] - scripts: [incident-rollback] - workflows: [release-response] - steps: [release-response.rollback-edge] - + stories: + - release-day + scripts: + - incident-rollback + workflows: + - release-response + steps: + - release-response.rollback-edge + depends_on: + - blue-validate-release + - red-reach-edge workflows: release-response: - description: > - Release-control workflow that validates a canary, branches on - poisoned promotion, and fans out rollback work when needed. + description: 'Release-control workflow that validates a canary, branches on poisoned + promotion, and fans out rollback work when needed. + + ' start: validate-release steps: validate-release: type: objective objective: blue-validate-release - on-success: branch-on-promotion + on_success: branch-on-promotion branch-on-promotion: type: decision when: - conditions: [rogue-release-promoted] + assertions: + - pre-rogue-release-promoted then: rollback-fanout else: finish rollback-fanout: type: parallel - branches: [revoke-artifact, rollback-edge] + branches: + - revoke-artifact + - rollback-edge join: rollback-joined revoke-artifact: type: objective objective: blue-revoke-artifact - on-success: rollback-joined + on_success: rollback-joined rollback-edge: type: objective objective: blue-preserve-service - on-success: rollback-joined + on_success: rollback-joined rollback-joined: type: join next: verify-rollback @@ -690,13 +1252,31 @@ workflows: type: decision when: steps: - - step: revoke-artifact - outcomes: [succeeded] + - step: revoke-artifact + outcomes: + - succeeded then: finish else: revalidate-release revalidate-release: type: objective objective: blue-validate-release - on-success: finish + on_success: finish finish: type: end +variables: + release_story_speed: + type: number + default: 1.25 + description: Playback speed for the release-day story + release_engineer_password_strength: + type: string + default: strong + description: Password posture for release engineering operator accounts + canary_cutover: + type: string + default: 2 hour 45 min + description: When canary promotion begins + rollback_deadline: + type: string + default: 6 hour + description: Time by which rollback should be complete diff --git a/examples/scenarios/techvault-bounded-native.sdl.yaml b/examples/scenarios/techvault-bounded-native.sdl.yaml new file mode 100644 index 000000000..fb110b511 --- /dev/null +++ b/examples/scenarios/techvault-bounded-native.sdl.yaml @@ -0,0 +1,26 @@ +name: techvault-bounded-native +version: 1.0.0 +description: >- + Bounded TechVault libvirt substrate used to certify exact VM/network + realization without guest image, placement, service, ACL, or SOC claims. + +nodes: + lab: + type: switch + demo: + type: vm + os: linux + resources: + ram: 128 MiB + cpu: 1 + services: [] + +infrastructure: + lab: + properties: + cidr: 192.0.2.0/24 + gateway: 192.0.2.1 + internal: true + demo: + links: + - lab diff --git a/examples/scenarios/techvault-guest-certified.sdl.yaml b/examples/scenarios/techvault-guest-certified.sdl.yaml new file mode 100644 index 000000000..233a9be81 --- /dev/null +++ b/examples/scenarios/techvault-guest-certified.sdl.yaml @@ -0,0 +1,29 @@ +name: techvault-guest-certified +description: > + Minimal guest-certified realization scenario. One bounded appliance node boots + through the production apply path and is certified from inside the guest - + resource allocation, network addressing, file content, and service state are + read back from the realized guest, freshness-bound to a per-run challenge, with + verified teardown. Used by `aces libvirt techvault guest-certify` and the + opt-in real-daemon proof. +nodes: + guest-net: + type: switch + guest-vm: + type: vm + os: linux + resources: {ram: 128 MiB, cpu: 1} + services: + - {name: beacon, port: 9000, protocol: tcp} +content: + guest-marker: + type: file + target: guest-vm + path: /etc/aces/marker + text: "guest-certified realization marker\n" + sensitive: false +infrastructure: + guest-net: + properties: {cidr: 192.0.2.0/24, gateway: 192.0.2.1, internal: true} + guest-vm: + links: [guest-net] diff --git a/examples/scenarios/techvault.sdl.yaml b/examples/scenarios/techvault.sdl.yaml index 9952c494e..08a84777d 100644 --- a/examples/scenarios/techvault.sdl.yaml +++ b/examples/scenarios/techvault.sdl.yaml @@ -1,346 +1,363 @@ name: techvault-runtime-parity -description: TechVault webapp runtime facts used to exercise ACES SDL runtime inventory parity. - -variables: - app_py_sha256: - type: string - required: true - description: SHA-256 digest observed for /app/app.py in the runtime capture. - requirements_sha256: - type: string - required: true - description: SHA-256 digest observed for /app/requirements.txt in the runtime capture. - style_css_sha256: - type: string - required: true - description: SHA-256 digest observed for /app/static/style.css in the runtime capture. - webapp_conf_sha256: - type: string - required: true - description: SHA-256 digest observed for /etc/supervisor/conf.d/webapp.conf. - wazuh_conf_sha256: - type: string - required: true - description: SHA-256 digest observed for /var/ossec/etc/ossec.conf. - +description: TechVault webapp runtime facts used to exercise ACES SDL runtime inventory + parity. nodes: techvault-webapp: type: vm - os: linux source: name: techvault-webapp version: local build: - description: Observed build recipe and provenance for the TechVault webapp image. base_image: python:3.12-slim base_image_digest: sha256:0e1f2a3b4c5d6e7f8091a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7 dockerfile_path: containers/webapp/Dockerfile instructions: - - instruction: from - arguments: [python:3.12-slim] - - instruction: arg - arguments: [APP_VERSION] - - instruction: workdir - arguments: [/app] - - instruction: copy - arguments: [webapp/requirements.txt, /app/requirements.txt] - - instruction: run - arguments: ["pip install --no-cache-dir -r /app/requirements.txt"] - - instruction: copy - arguments: [webapp/, /app/] - - instruction: copy - arguments: [supervisor/webapp.conf, /etc/supervisor/conf.d/webapp.conf] - - instruction: copy - arguments: [docker/entrypoint.sh, /entrypoint.sh] - - instruction: expose - arguments: ["8080"] - - instruction: entrypoint - arguments: [/entrypoint.sh] - - instruction: cmd - arguments: [supervisord, -n, -c, /etc/supervisor/supervisord.conf] + - instruction: from + arguments: + - python:3.12-slim + - instruction: arg + arguments: + - APP_VERSION + - instruction: workdir + arguments: + - /app + - instruction: copy + arguments: + - webapp/requirements.txt + - /app/requirements.txt + - instruction: run + arguments: + - pip install --no-cache-dir -r /app/requirements.txt + - instruction: copy + arguments: + - webapp/ + - /app/ + - instruction: copy + arguments: + - supervisor/webapp.conf + - /etc/supervisor/conf.d/webapp.conf + - instruction: copy + arguments: + - docker/entrypoint.sh + - /entrypoint.sh + - instruction: expose + arguments: + - '8080' + - instruction: entrypoint + arguments: + - /entrypoint.sh + - instruction: cmd + arguments: + - supervisord + - -n + - -c + - /etc/supervisor/supervisord.conf layers: - - digest: sha256:11ab22cd33ef44ab55cd66ef77ab88cd99ef00ab11cd22ef33ab44cd55ef66ab - created_by: FROM python:3.12-slim - size: 122683392 - - digest: sha256:22bc33de44fa55bc66de77fa88bc99de00fa11bc22de33fa44bc55de66fa77bc - created_by: "RUN pip install --no-cache-dir -r /app/requirements.txt" - size: 18472960 - - created_by: ENV APP_HOME=/app - empty: true + - digest: sha256:11ab22cd33ef44ab55cd66ef77ab88cd99ef00ab11cd22ef33ab44cd55ef66ab + created_by: FROM python:3.12-slim + size: 122683392 + - digest: sha256:22bc33de44fa55bc66de77fa88bc99de00fa11bc22de33fa44bc55de66fa77bc + created_by: RUN pip install --no-cache-dir -r /app/requirements.txt + size: 18472960 + - created_by: ENV APP_HOME=/app + empty: true build_args: - - name: APP_VERSION - value: "2.4.0" - value_classification: plain - description: Build-time application version stamped into the image. - - name: PIP_INDEX_TOKEN - value_classification: redacted - description: Private index credential supplied at build time and not retained in the image. + - name: APP_VERSION + value: 2.4.0 + value_classification: plain + description: Build-time application version stamped into the image. + - name: PIP_INDEX_TOKEN + value_classification: redacted + description: Private index credential supplied at build time and not retained + in the image. copied_sources: - - source_path: webapp/requirements.txt - destination_path: /app/requirements.txt - - source_path: webapp/app.py - destination_path: /app/app.py - - source_path: webapp/static/style.css - destination_path: /app/static/style.css - - source_path: webapp/templates/index.html - destination_path: /app/templates/index.html - - source_path: supervisor/webapp.conf - destination_path: /etc/supervisor/conf.d/webapp.conf - - source_path: docker/entrypoint.sh - destination_path: /entrypoint.sh + - source_path: webapp/requirements.txt + destination_path: /app/requirements.txt + - source_path: webapp/app.py + destination_path: /app/app.py + - source_path: webapp/static/style.css + destination_path: /app/static/style.css + - source_path: webapp/templates/index.html + destination_path: /app/templates/index.html + - source_path: supervisor/webapp.conf + destination_path: /etc/supervisor/conf.d/webapp.conf + - source_path: docker/entrypoint.sh + destination_path: /entrypoint.sh config: - entrypoint: [/entrypoint.sh] - command: [supervisord, -n, -c, /etc/supervisor/supervisord.conf] + entrypoint: + - /entrypoint.sh + command: + - supervisord + - -n + - -c + - /etc/supervisor/supervisord.conf working_directory: /app - exposed_ports: [8080/tcp] + exposed_ports: + - 8080/tcp labels: org.opencontainers.image.title: TechVault Webapp org.opencontainers.image.source: https://example.test/techvault/webapp com.techvault.tier: webapp default_environment: - - name: APP_HOME - value: /app - value_classification: plain - - name: PYTHONUNBUFFERED - value: "1" - value_classification: plain + - name: APP_HOME + value: /app + value_classification: plain + - name: PYTHONUNBUFFERED + value: '1' + value_classification: plain source_inputs: - - identifier: webapp-app - source_path: webapp/app.py - destination_path: /app/app.py - checksum: ${app_py_sha256} - checksum_algorithm: sha256 - - identifier: webapp-requirements - source_path: webapp/requirements.txt - destination_path: /app/requirements.txt - checksum: ${requirements_sha256} - checksum_algorithm: sha256 - - identifier: webapp-style - source_path: webapp/static/style.css - destination_path: /app/static/style.css - checksum: ${style_css_sha256} - checksum_algorithm: sha256 - - identifier: supervisor-webapp-conf - source_path: supervisor/webapp.conf - destination_path: /etc/supervisor/conf.d/webapp.conf - checksum: ${webapp_conf_sha256} - checksum_algorithm: sha256 + - identifier: webapp-app + source_path: webapp/app.py + destination_path: /app/app.py + checksum: ${app_py_sha256} + checksum_algorithm: sha256 + - identifier: webapp-requirements + source_path: webapp/requirements.txt + destination_path: /app/requirements.txt + checksum: ${requirements_sha256} + checksum_algorithm: sha256 + - identifier: webapp-style + source_path: webapp/static/style.css + destination_path: /app/static/style.css + checksum: ${style_css_sha256} + checksum_algorithm: sha256 + - identifier: supervisor-webapp-conf + source_path: supervisor/webapp.conf + destination_path: /etc/supervisor/conf.d/webapp.conf + checksum: ${webapp_conf_sha256} + checksum_algorithm: sha256 attestation: status: absent verification: not_applicable attestation_type: none - description: >- - The mutable local image tag has no registry-visible OCI, in-toto, or - SLSA attestation; absence of attestation is recorded as a distinct + description: The mutable local image tag has no registry-visible OCI, in-toto, + or SLSA attestation; absence of attestation is recorded as a distinct fact from a failed verification. + description: Observed build recipe and provenance for the TechVault webapp + image. resources: - ram: 1 GiB + ram: 1073741824 cpu: 1 + os: linux + vulnerabilities: + - unrestricted-upload + - verbose-error-disclosure services: - - port: 8080 - protocol: tcp - name: techvault-http - description: Gunicorn-served TechVault Flask application. - vulnerabilities: [unrestricted-upload, verbose-error-disclosure] + - port: 8080 + protocol: tcp + name: techvault-http + description: Gunicorn-served TechVault Flask application. runtime: mounts: - - target: /var/log/gunicorn - source: techvault_gunicorn_logs - source_kind: volume - filesystem_type: ext4 - read_only: false - options: [rw, nosuid] - propagation: rprivate - stability: volume_backed - backend_generated: true - description: Runtime volume state for Gunicorn logs. + - target: /var/log/gunicorn + source: techvault_gunicorn_logs + source_kind: volume + filesystem_type: ext4 + read_only: false + options: + - rw + - nosuid + propagation: rprivate + stability: volume_backed + backend_generated: true + description: Runtime volume state for Gunicorn logs. filesystem_inventory: - - path: /app/app.py - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0644" - size: 14896 - content_digest: ${app_py_sha256} - digest_algorithm: sha256 - source_path: webapp/app.py - provenance: techvault-source-package - stability: stable - sensitivity: plain - - path: /app/requirements.txt - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0644" - size: 312 - content_digest: ${requirements_sha256} - digest_algorithm: sha256 - source_path: webapp/requirements.txt - provenance: techvault-source-package - stability: stable - sensitivity: plain - - path: /app/static/style.css - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0644" - size: 8192 - content_digest: ${style_css_sha256} - digest_algorithm: sha256 - source_path: webapp/static/style.css - provenance: techvault-source-package - stability: stable - sensitivity: plain - - path: /app/templates/index.html - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0644" - source_path: webapp/templates/index.html - provenance: techvault-source-package - stability: stable - sensitivity: plain - - path: /etc/supervisor/conf.d/webapp.conf - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0644" - content_digest: ${webapp_conf_sha256} - digest_algorithm: sha256 - source_path: supervisor/webapp.conf - provenance: image-configuration - stability: stable - sensitivity: plain - - path: /etc/rsyslog.d/80-gunicorn.conf - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0644" - source_path: rsyslog/80-gunicorn.conf - provenance: image-configuration - stability: stable - sensitivity: plain - - path: /etc/rsyslog.d/90-forward.conf - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0644" - source_path: rsyslog/90-forward.conf - provenance: image-configuration - stability: stable - sensitivity: plain - - path: /entrypoint.sh - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0755" - source_path: docker/entrypoint.sh - provenance: image-configuration - stability: stable - sensitivity: plain - - path: /opt/aptl/wazuh - entry_type: directory - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0755" - provenance: image-configuration - stability: stable - sensitivity: plain - - path: /var/ossec/etc/ossec.conf - entry_type: file - owner_user: root - owner_group: root - uid: 0 - gid: 0 - mode: "0640" - content_digest: ${wazuh_conf_sha256} - digest_algorithm: sha256 - source_path: wazuh/ossec.conf - provenance: image-configuration - stability: stable - sensitivity: plain - - path: /var/log/gunicorn/access.log - entry_type: file - owner_user: root - owner_group: adm - uid: 0 - gid: 4 - mode: "0640" - provenance: runtime - stability: log - sensitivity: operator_secret + - path: /app/app.py + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0644' + size: 14896 + content_digest: ${app_py_sha256} + digest_algorithm: sha256 + source_path: webapp/app.py + provenance: techvault-source-package + stability: stable + sensitivity: plain + - path: /app/requirements.txt + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0644' + size: 312 + content_digest: ${requirements_sha256} + digest_algorithm: sha256 + source_path: webapp/requirements.txt + provenance: techvault-source-package + stability: stable + sensitivity: plain + - path: /app/static/style.css + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0644' + size: 8192 + content_digest: ${style_css_sha256} + digest_algorithm: sha256 + source_path: webapp/static/style.css + provenance: techvault-source-package + stability: stable + sensitivity: plain + - path: /app/templates/index.html + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0644' + source_path: webapp/templates/index.html + provenance: techvault-source-package + stability: stable + sensitivity: plain + - path: /etc/supervisor/conf.d/webapp.conf + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0644' + content_digest: ${webapp_conf_sha256} + digest_algorithm: sha256 + source_path: supervisor/webapp.conf + provenance: image-configuration + stability: stable + sensitivity: plain + - path: /etc/rsyslog.d/80-gunicorn.conf + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0644' + source_path: rsyslog/80-gunicorn.conf + provenance: image-configuration + stability: stable + sensitivity: plain + - path: /etc/rsyslog.d/90-forward.conf + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0644' + source_path: rsyslog/90-forward.conf + provenance: image-configuration + stability: stable + sensitivity: plain + - path: /entrypoint.sh + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0755' + source_path: docker/entrypoint.sh + provenance: image-configuration + stability: stable + sensitivity: plain + - path: /opt/aptl/wazuh + entry_type: directory + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0755' + provenance: image-configuration + stability: stable + sensitivity: plain + - path: /var/ossec/etc/ossec.conf + entry_type: file + owner_user: root + owner_group: root + uid: 0 + gid: 0 + mode: '0640' + content_digest: ${wazuh_conf_sha256} + digest_algorithm: sha256 + source_path: wazuh/ossec.conf + provenance: image-configuration + stability: stable + sensitivity: plain + - path: /var/log/gunicorn/access.log + entry_type: file + owner_user: root + owner_group: adm + uid: 0 + gid: 4 + mode: '0640' + provenance: runtime + stability: log + sensitivity: operator_secret processes: - - name: supervisord - pid: 1 - command: supervisord -n -c /etc/supervisor/supervisord.conf - role: supervisor - user: root - group: root - working_directory: / - - name: gunicorn - parent_pid: 1 - command: [gunicorn, app:app, --bind, 0.0.0.0:8080] - role: worker - user: root - group: root - - name: wazuh-agentd - parent_pid: 1 - command_redacted: true - role: agent - user: root - group: ossec + - name: supervisord + pid: 1 + command: + - supervisord -n -c /etc/supervisor/supervisord.conf + role: supervisor + user: root + group: root + working_directory: / + - name: gunicorn + parent_pid: 1 + command: + - gunicorn + - app:app + - --bind + - 0.0.0.0:8080 + role: worker + user: root + group: root + - name: wazuh-agentd + parent_pid: 1 + command_redacted: true + role: agent + user: root + group: ossec environment: - - name: TECHVAULT_ADMIN_PASSWORD - value_classification: redacted - provenance: operator - - name: SCENARIO_FIXTURE_TOKEN - value_classification: secret_fixture - provenance: compose + - name: TECHVAULT_ADMIN_PASSWORD + value_classification: redacted + provenance: operator + - name: SCENARIO_FIXTURE_TOKEN + value_classification: secret_fixture + provenance: compose linux_capabilities: - required: [CAP_NET_BIND_SERVICE] - effective: [CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_NET_BIND_SERVICE] - # See ADR-030. The container baseline above is what PID 1 (the - # supervisor) carries; the gunicorn worker subtree drops - # CAP_CHOWN so an attacker who lands code execution as gunicorn - # cannot relabel files under /var/log/gunicorn out from under the - # log-only sensitivity classification. + required: + - CAP_NET_BIND_SERVICE + effective: + - CAP_CHOWN + - CAP_DAC_OVERRIDE + - CAP_NET_BIND_SERVICE process_overrides: - - subject: - name: gunicorn - parent_pid: 1 - scope: subtree - drop: [CAP_CHOWN] - description: gunicorn worker subtree cannot relabel log files + - subject: + name: gunicorn + parent_pid: 1 + scope: subtree + drop: + - CAP_CHOWN + description: gunicorn worker subtree cannot relabel log files operational_policy: restart: unless_stopped resource_limits: - memory: 512 MiB + memory: 536870912 pids: 128 container: - entrypoint: [/entrypoint.sh] - command: [supervisord, -n, -c, /etc/supervisor/supervisord.conf] + entrypoint: + - /entrypoint.sh + command: + - supervisord + - -n + - -c + - /etc/supervisor/supervisord.conf log_driver: json-file log_options: max-size: 10m - max-file: "3" + max-file: '3' namespaces: cgroup: private ipc: private @@ -351,301 +368,344 @@ nodes: read_only_rootfs: false publish_all_ports: false autoremove: false - shm_size: 64 MiB - masked_paths: [/proc/acpi, /proc/kcore] - read_only_paths: [/proc/sys, /proc/sysrq-trigger] + shm_size: 67108864 + masked_paths: + - /proc/acpi + - /proc/kcore + read_only_paths: + - /proc/sys + - /proc/sysrq-trigger cgroup_parent: /docker runtime_name: runc - device_cgroup_rules: [c 1:3 rwm] + device_cgroup_rules: + - c 1:3 rwm extra_hosts: - - hostname: wazuh-manager - address: 172.20.0.10 - dns: [8.8.8.8] - dns_options: [ndots:0] - dns_search: [techvault.local] - group_add: [adm] + - hostname: wazuh-manager + address: 172.20.0.10 + dns: + - 8.8.8.8 + dns_options: + - ndots:0 + dns_search: + - techvault.local + group_add: + - adm health: status: healthy failing_streak: 0 log: - - start: "2026-05-20T12:00:00Z" - end: "2026-05-20T12:00:01Z" - exit_code: 0 - output: ok + - start: '2026-05-20T12:00:00Z' + end: '2026-05-20T12:00:01Z' + exit_code: 0 + output: ok local_identity: - description: Local identity database observed via getent passwd/group inside the webapp container. users: - - username: root - uid: 0 - primary_gid: 0 - primary_group: root - gecos: root - home: /root - shell: /bin/bash - provenance: image - stability: stable - - username: www-data - uid: 33 - primary_gid: 33 - primary_group: www-data - gecos: www-data - home: /var/www - shell: /usr/sbin/nologin - no_login: true - provenance: image - stability: stable - - username: messagebus - uid: 100 - primary_gid: 100 - primary_group: messagebus - home: /nonexistent - shell: /usr/sbin/nologin - no_login: true - provenance: package - stability: stable - - username: Debian-exim - uid: 101 - primary_gid: 101 - primary_group: Debian-exim - home: /var/spool/exim4 - shell: /usr/sbin/nologin - no_login: true - provenance: package - stability: stable - - username: wazuh - uid: 999 - primary_gid: 999 - primary_group: wazuh - gecos: Wazuh agent - home: /var/ossec - shell: /usr/sbin/nologin - supplemental_groups: [adm] - no_login: true - provenance: package - stability: stable + - username: root + uid: 0 + primary_gid: 0 + primary_group: root + gecos: root + home: /root + shell: /bin/bash + provenance: image + stability: stable + - username: www-data + uid: 33 + primary_gid: 33 + primary_group: www-data + gecos: www-data + home: /var/www + shell: /usr/sbin/nologin + no_login: true + provenance: image + stability: stable + - username: messagebus + uid: 100 + primary_gid: 100 + primary_group: messagebus + home: /nonexistent + shell: /usr/sbin/nologin + no_login: true + provenance: package + stability: stable + - username: Debian-exim + uid: 101 + primary_gid: 101 + primary_group: Debian-exim + home: /var/spool/exim4 + shell: /usr/sbin/nologin + no_login: true + provenance: package + stability: stable + - username: wazuh + uid: 999 + primary_gid: 999 + primary_group: wazuh + gecos: Wazuh agent + home: /var/ossec + shell: /usr/sbin/nologin + supplemental_groups: + - adm + no_login: true + provenance: package + stability: stable groups: - - name: root - gid: 0 - members: [root] - provenance: image - - name: www-data - gid: 33 - members: [www-data] - provenance: image - - name: adm - gid: 4 - members: [wazuh] - provenance: image - - name: sudo - gid: 27 - members: [] - provenance: image - - name: messagebus - gid: 100 - members: [] - provenance: package - - name: Debian-exim - gid: 101 - members: [] - provenance: package - - name: wazuh - gid: 999 - members: [wazuh] - provenance: package + - name: root + gid: 0 + members: + - root + provenance: image + - name: www-data + gid: 33 + members: + - www-data + provenance: image + - name: adm + gid: 4 + members: + - wazuh + provenance: image + - name: sudo + gid: 27 + members: [] + provenance: image + - name: messagebus + gid: 100 + members: [] + provenance: package + - name: Debian-exim + gid: 101 + members: [] + provenance: package + - name: wazuh + gid: 999 + members: + - wazuh + provenance: package sudo_rules: - - principal: sudo - principal_kind: group - host_scope: ALL - run_as_users: [ALL] - run_as_groups: [ALL] - commands: [ALL] - description: Default Debian sudo-group grant; no member accounts observed in this capture. - - principal: wazuh - principal_kind: user - host_scope: ALL - run_as_users: [root] - commands: - - /usr/bin/systemctl restart wazuh-agent - nopasswd: true - description: NOPASSWD restart grant for the Wazuh agent service. + - principal: sudo + principal_kind: group + run_as_users: + - ALL + run_as_groups: + - ALL + commands: + - ALL + host_scope: ALL + description: Default Debian sudo-group grant; no member accounts observed + in this capture. + - principal: wazuh + principal_kind: user + run_as_users: + - root + commands: + - /usr/bin/systemctl restart wazuh-agent + host_scope: ALL + nopasswd: true + description: NOPASSWD restart grant for the Wazuh agent service. + description: Local identity database observed via getent passwd/group inside + the webapp container. network: - description: Container network realization observed by harness inspection of the webapp container. hostname: techvault-webapp domainname: techvault.local endpoints: - - network: aptl-dmz - network_id: 7f2c1ad4e9b30c5a8d6e4f1b2a9c7e0d3f5b8a1c4d6e9f0a2b3c5d7e9f1a3b5c - network_id_stability: stable - endpoint_id: 3a9c7e0d3f5b8a1c4d6e9f0a2b3c5d7e9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b - endpoint_id_stability: ephemeral - backend_generated: true - ip_address: 172.20.0.20 - ip_prefix_length: 24 - gateway: 172.20.0.1 - mac_address: 02:42:ac:14:00:14 - aliases: [aptl-webapp, webapp] - dns_names: [aptl-webapp, webapp] - generated_dns_names: [3a9c7e0d3f5b] - backend: - driver: bridge - ipam_driver: default - driver_options: - com.docker.network.bridge.name: br-aptl-dmz - ipam_options: - com.docker.network.driver.mtu: "1500" - description: Docker bridge network backing the TechVault DMZ. - - network: aptl-internal - network_id: c4d6e9f0a2b3c5d7e9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b7f2c1ad4e9b30c5 - network_id_stability: stable - endpoint_id: 9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b7f2c1ad4e9b30c5a3a9c7e0d3f5b8a1c - endpoint_id_stability: ephemeral - backend_generated: true - ip_address: 172.21.0.20 - ip_prefix_length: 24 - gateway: 172.21.0.1 - mac_address: 02:42:ac:15:00:14 - aliases: [aptl-webapp, webapp] - dns_names: [aptl-webapp, webapp] - generated_dns_names: [3a9c7e0d3f5b] - backend: - driver: bridge - ipam_driver: default - driver_options: - com.docker.network.bridge.name: br-aptl-internal + - network: aptl-dmz + network_id: 7f2c1ad4e9b30c5a8d6e4f1b2a9c7e0d3f5b8a1c4d6e9f0a2b3c5d7e9f1a3b5c + network_id_stability: stable + endpoint_id: 3a9c7e0d3f5b8a1c4d6e9f0a2b3c5d7e9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b + endpoint_id_stability: ephemeral + backend_generated: true + ip_address: 172.20.0.20 + ip_prefix_length: 24 + gateway: 172.20.0.1 + mac_address: 02:42:ac:14:00:14 + aliases: + - aptl-webapp + - webapp + dns_names: + - aptl-webapp + - webapp + generated_dns_names: + - 3a9c7e0d3f5b + backend: + driver: bridge + ipam_driver: default + driver_options: + com.docker.network.bridge.name: br-aptl-dmz + ipam_options: + com.docker.network.driver.mtu: '1500' + description: Docker bridge network backing the TechVault DMZ. + - network: aptl-internal + network_id: c4d6e9f0a2b3c5d7e9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b7f2c1ad4e9b30c5 + network_id_stability: stable + endpoint_id: 9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b7f2c1ad4e9b30c5a3a9c7e0d3f5b8a1c + endpoint_id_stability: ephemeral + backend_generated: true + ip_address: 172.21.0.20 + ip_prefix_length: 24 + gateway: 172.21.0.1 + mac_address: 02:42:ac:15:00:14 + aliases: + - aptl-webapp + - webapp + dns_names: + - aptl-webapp + - webapp + generated_dns_names: + - 3a9c7e0d3f5b + backend: + driver: bridge + ipam_driver: default + driver_options: + com.docker.network.bridge.name: br-aptl-internal published_ports: - - container_port: 8080 - protocol: tcp - host_ip: 0.0.0.0 - host_port: 8080 - description: Webapp HTTP port published from the container to the host. + - container_port: 8080 + protocol: tcp + host_ip: 0.0.0.0 + host_port: 8080 + description: Webapp HTTP port published from the container to the host. + description: Container network realization observed by harness inspection + of the webapp container. applications: - - application_id: techvault-webapp - service: techvault-http - protocol: http - name: TechVault Webapp - framework: flask - base_path: / - description: >- - Participant-observable Flask route surface of the TechVault webapp, - observed from containers/webapp/app/app.py, templates, and static assets. - routes: - - route_id: index - path: / - methods: [GET] - name: index - auth_required: false - session_required: false - description: Landing page rendered from the index template. - responses: - - status_code: 200 - content_type: text/html - templates: [/app/templates/index.html] - static_assets: [/app/static/style.css] - - route_id: login - path: /login - methods: [GET, POST] - name: login - auth_required: false - session_required: false - auth_scheme: form_login - description: Form-based login; establishes the session on success. - parameters: - - name: username - location: form - required: true - data_type: string - - name: password - location: form - required: true - data_type: string - responses: - - status_code: 200 - content_type: text/html - description: Login form, or form re-rendered with an error. - - status_code: 302 - content_type: text/html - description: Redirect to the dashboard on successful login. - templates: [/app/templates/index.html] - static_assets: [/app/static/style.css] - redirects: - - target: /dashboard - status_code: 302 - condition: valid credentials accepted - - route_id: file-upload - path: /files/upload - methods: [POST] - name: file_upload - auth_required: true - session_required: true - description: >- - Accepts an uploaded document; the endpoint stores files without - restricting type or destination path. - parameters: - - name: document - location: uploaded_file - required: true - data_type: file - responses: - - status_code: 302 - content_type: text/html - vulnerability_refs: [unrestricted-upload] - - route_id: file-download - path: /files/ - methods: [GET] - name: file_download - auth_required: true - session_required: true - description: Returns a previously uploaded file by identifier. - parameters: - - name: file_id - location: path - required: true - data_type: string - responses: - - status_code: 200 - content_type: application/octet-stream - - status_code: 404 - content_type: text/html - - route_id: diagnostics - path: /debug/info - methods: [GET] - name: debug_info - auth_required: false - session_required: false - description: >- - Intentionally exposed diagnostic endpoint left enabled in the - range fixture image. - responses: - - status_code: 200 - content_type: application/json - exposed_fields: - - name: build_token - sensitivity: secret_fixture - value: techvault-fixture-build-token - description: Fixture build token deliberately exposed for range exercises. - - name: operator_api_key - sensitivity: redacted - description: Operator API key referenced by the endpoint but withheld from SDL. - disclosures: - - trigger: any request to the diagnostic endpoint - status_code: 200 - disclosure: installed package versions, container hostname, and app root path - sensitivity: plain - - trigger: unhandled application exception - status_code: 500 - disclosure: Python traceback with internal file paths - sensitivity: plain - vulnerability_refs: [verbose-error-disclosure] + - application_id: techvault-webapp + service: techvault-http + protocol: http + name: TechVault Webapp + base_path: / + framework: flask + description: Participant-observable Flask route surface of the TechVault webapp, + observed from containers/webapp/app/app.py, templates, and static assets. + routes: + - route_id: index + path: / + methods: + - GET + name: index + description: Landing page rendered from the index template. + auth_required: false + session_required: false + responses: + - status_code: 200 + content_type: text/html + templates: + - /app/templates/index.html + static_assets: + - /app/static/style.css + - route_id: login + path: /login + methods: + - GET + - POST + name: login + description: Form-based login; establishes the session on success. + auth_required: false + auth_scheme: form_login + session_required: false + parameters: + - name: username + location: form + required: true + data_type: string + - name: password + location: form + required: true + data_type: string + responses: + - status_code: 200 + content_type: text/html + description: Login form, or form re-rendered with an error. + - status_code: 302 + content_type: text/html + description: Redirect to the dashboard on successful login. + templates: + - /app/templates/index.html + static_assets: + - /app/static/style.css + redirects: + - target: /dashboard + status_code: 302 + condition: valid credentials accepted + - route_id: file-upload + path: /files/upload + methods: + - POST + name: file_upload + description: Accepts an uploaded document; the endpoint stores files without + restricting type or destination path. + auth_required: true + session_required: true + parameters: + - name: document + location: uploaded_file + required: true + data_type: file + responses: + - status_code: 302 + content_type: text/html + vulnerability_refs: + - unrestricted-upload + - route_id: file-download + path: /files/ + methods: + - GET + name: file_download + description: Returns a previously uploaded file by identifier. + auth_required: true + session_required: true + parameters: + - name: file_id + location: path + required: true + data_type: string + responses: + - status_code: 200 + content_type: application/octet-stream + - status_code: 404 + content_type: text/html + - route_id: diagnostics + path: /debug/info + methods: + - GET + name: debug_info + description: Intentionally exposed diagnostic endpoint left enabled in the + range fixture image. + auth_required: false + session_required: false + responses: + - status_code: 200 + content_type: application/json + vulnerability_refs: + - verbose-error-disclosure + disclosures: + - trigger: any request to the diagnostic endpoint + status_code: 200 + disclosure: installed package versions, container hostname, and app root + path + sensitivity: plain + - trigger: unhandled application exception + status_code: 500 + disclosure: Python traceback with internal file paths + sensitivity: plain + exposed_fields: + - name: build_token + sensitivity: secret_fixture + value: techvault-fixture-build-token + description: Fixture build token deliberately exposed for range exercises. + - name: operator_api_key + sensitivity: redacted + description: Operator API key referenced by the endpoint but withheld + from SDL. aptl-dmz: type: switch description: TechVault DMZ container network (Docker bridge aptl_aptl-dmz). aptl-internal: type: switch description: TechVault internal container network (Docker bridge aptl_aptl-internal). - infrastructure: techvault-webapp: - links: [aptl-dmz, aptl-internal] + links: + - aptl-dmz + - aptl-internal aptl-dmz: properties: cidr: 172.20.0.0/24 @@ -656,19 +716,39 @@ infrastructure: cidr: 172.21.0.0/24 gateway: 172.21.0.1 internal: true - vulnerabilities: unrestricted-upload: name: Unrestricted file upload - description: >- - The file upload route accepts arbitrary file types and does not constrain + description: The file upload route accepts arbitrary file types and does not constrain the storage path, allowing a participant to place executable content. technical: true class: CWE-434 verbose-error-disclosure: name: Verbose error and diagnostic disclosure - description: >- - The diagnostic route and unhandled-exception handler expose internal + description: The diagnostic route and unhandled-exception handler expose internal package versions, host paths, and Python tracebacks to any caller. technical: false class: CWE-209 +variables: + app_py_sha256: + type: string + description: SHA-256 digest observed for /app/app.py in the runtime capture. + required: true + requirements_sha256: + type: string + description: SHA-256 digest observed for /app/requirements.txt in the runtime + capture. + required: true + style_css_sha256: + type: string + description: SHA-256 digest observed for /app/static/style.css in the runtime + capture. + required: true + webapp_conf_sha256: + type: string + description: SHA-256 digest observed for /etc/supervisor/conf.d/webapp.conf. + required: true + wazuh_conf_sha256: + type: string + description: SHA-256 digest observed for /var/ossec/etc/ossec.conf. + required: true diff --git a/implementations/python/hatch_build.py b/implementations/python/hatch_build.py index ac7499d33..40311f2e7 100644 --- a/implementations/python/hatch_build.py +++ b/implementations/python/hatch_build.py @@ -23,6 +23,7 @@ from hatchling.metadata.plugin.interface import MetadataHookInterface _WHEEL_DESTINATION = "aces_contracts/_corpus" +_NOTICE_WHEEL_DESTINATION = "aces_contracts/_corpus/provenance/THIRD_PARTY_NOTICES.md" class CustomBuildHook(BuildHookInterface): @@ -47,6 +48,13 @@ def initialize(self, version: str, build_data: dict) -> None: build_data.setdefault("force_include", {})[str(corpus)] = _WHEEL_DESTINATION + source_notice = (root.parent.parent / "THIRD_PARTY_NOTICES.md").resolve() + vendored_notice = (root / "THIRD_PARTY_NOTICES.md").resolve() + notice = source_notice if source_notice.is_file() else vendored_notice + if not notice.is_file(): + raise FileNotFoundError("THIRD_PARTY_NOTICES.md is required in source checkouts and source distributions") + build_data["force_include"][str(notice)] = _NOTICE_WHEEL_DESTINATION + class ReadmeMetadataHook(MetadataHookInterface): """Inject the repo-root README.md as the package's PyPI long description. diff --git a/implementations/python/packages/aces_backend_libvirt/_techvault_native_helpers.py b/implementations/python/packages/aces_backend_libvirt/_techvault_native_helpers.py new file mode 100644 index 000000000..d56beb2fb --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/_techvault_native_helpers.py @@ -0,0 +1,28 @@ +"""Small self-contained helpers for the native TechVault libvirt drivers.""" + +from __future__ import annotations + +import os +from pathlib import Path + + +def default_connector(connection_uri: str) -> object | None: + """Open a real libvirt connection lazily (imports libvirt on demand).""" + + import importlib + + libvirt = importlib.import_module("libvirt") + return libvirt.open(connection_uri) + + +def default_kernel_path() -> Path: + """Return a host kernel image path suitable for booting a generated appliance.""" + + running = Path(f"/boot/vmlinuz-{os.uname().release}") + if running.exists(): + return running + candidates = sorted(Path("/boot").glob("vmlinuz-*")) + return candidates[-1] if candidates else Path("/boot/vmlinuz") + + +__all__ = ["default_connector", "default_kernel_path"] diff --git a/implementations/python/packages/aces_backend_libvirt/_techvault_native_ops.py b/implementations/python/packages/aces_backend_libvirt/_techvault_native_ops.py new file mode 100644 index 000000000..6d243ac3b --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/_techvault_native_ops.py @@ -0,0 +1,77 @@ +"""Diagnostic codes and low-level native-object helpers for the TechVault driver. + +Holds the shared diagnostic-code constants, the native-resource protocol, and the +thin libvirt-call/name-availability/artifact-token/diagnostic helpers used by the +native TechVault driver and its teardown paths. Split from +:mod:`aces_backend_libvirt.techvault_native` to keep that module under the ADR-015 +source-size cap; the driver re-imports these names so existing call sites and the +guest-certified subclass's ``_artifact_token`` import stay stable. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Protocol, cast + +from aces_contracts.diagnostics import Diagnostic, Severity + +from .drivers.libvirt import _aces_uuid, _error_code, _existing_uuid +from .techvault_lifecycle import ( + NativeOwnershipConflict as _OwnershipConflict, +) + +_DOMAIN = "runtime" +_CODE_OPERATION_FAILED = "libvirt-backend.techvault-native.operation-failed" +_CODE_OWNERSHIP_CONFLICT = "libvirt-backend.techvault-native.ownership-conflict" +_CODE_READBACK_FAILED = "libvirt-backend.techvault-native.readback-failed" +_CODE_RESIDUAL_STATE = "libvirt-backend.techvault-native.residual-state" +_CODE_UNAVAILABLE = "libvirt-backend.techvault-native.unavailable" +_DEFAULT_CONNECTION_URI = "qemu:///system" + + +class _NativeResource(Protocol): + def create(self) -> None: ... + + def destroy(self) -> None: ... + + def undefine(self) -> None: ... + + +def _call(connection: object, method_name: str, payload: str) -> _NativeResource: + method = cast(Callable[[str], _NativeResource], getattr(connection, method_name)) + return method(payload) + + +def _ensure_name_available(connection: object, method_name: str, name: str, address: str) -> None: + method = getattr(connection, method_name, None) + if not callable(method): + raise RuntimeError("native lookup is unavailable") + try: + native = method(name) + except KeyError: + return + except Exception as exc: + if _error_code(exc) in {42, 43}: + return + raise + if _existing_uuid(native) != _aces_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("-", "") + + +_MESSAGES = { + _CODE_UNAVAILABLE: "Libvirt connection is unavailable for native TechVault realization.", + _CODE_RESIDUAL_STATE: "TechVault rollback could not verify cleanup for '{address}'; residual state may remain.", + _CODE_OWNERSHIP_CONFLICT: "Native object for '{address}' is not owned by that ACES address; refusing mutation.", + _CODE_READBACK_FAILED: "Native libvirt TechVault readback for '{address}' did not succeed.", +} +_DEFAULT_MESSAGE = "Native libvirt TechVault operation for '{address}' did not succeed." + + +def _diagnostic(code: str, address: str) -> Diagnostic: + message = _MESSAGES.get(code, _DEFAULT_MESSAGE).format(address=address) + return Diagnostic(code=code, domain=_DOMAIN, address=address, message=message, severity=Severity.ERROR) diff --git a/implementations/python/packages/aces_backend_libvirt/driver.py b/implementations/python/packages/aces_backend_libvirt/driver.py index 2c71d1e85..e70681524 100644 --- a/implementations/python/packages/aces_backend_libvirt/driver.py +++ b/implementations/python/packages/aces_backend_libvirt/driver.py @@ -6,6 +6,7 @@ from typing import Protocol from aces_contracts.diagnostics import Diagnostic +from aces_contracts.realization_envelope import ObservationStrength, RealizationConcern from .cloudinit import CloudInitSpec @@ -77,6 +78,17 @@ class DomainHandle: realized: bool = True +@dataclass(frozen=True) +class RealizationObservation: + """Bounded typed readback for one realized concern field.""" + + address: str + field_path: str + concern: RealizationConcern + source: ObservationStrength + value: object + + @dataclass(frozen=True) class DriverResult: """Aggregate portable result from a libvirt driver call.""" @@ -84,6 +96,7 @@ class DriverResult: networks: tuple[NetworkHandle, ...] = () domains: tuple[DomainHandle, ...] = () diagnostics: tuple[Diagnostic, ...] = () + observations: tuple[RealizationObservation, ...] = () class LibvirtDriver(Protocol): diff --git a/implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py b/implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py index c95a1715b..91b408e03 100644 --- a/implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py +++ b/implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py @@ -13,6 +13,7 @@ from pathlib import Path from typing import Protocol, cast +from aces_backend_protocols.naming import provider_resource_name from aces_contracts.diagnostics import Diagnostic, Severity from aces_backend_libvirt.driver import ( @@ -132,6 +133,8 @@ def open(self, connection_uri: str) -> object | None: ... class LibvirtDeploymentDriver: """Realize portable specs against a libvirt connection.""" + driver_mode = "generic" + def __init__( self, *, @@ -324,10 +327,11 @@ def _conn(self) -> object: return self._connection def _runtime_name(self, address: str, preferred: str) -> str: - return _safe_name(preferred, fallback=address.rsplit(".", 1)[-1], prefix=self._name_prefix) + del preferred + return provider_resource_name(address, prefix=self._name_prefix) def _name_for(self, address: str) -> str: - return self._names.get(address, self._runtime_name(address, address.rsplit(".", 1)[-1])) + return self._names.get(address, self._runtime_name(address, "")) def _build_seed(self, spec: DomainSpec, name: str) -> Path | None: cloud_init = spec.cloud_init diff --git a/implementations/python/packages/aces_backend_libvirt/envelopes.py b/implementations/python/packages/aces_backend_libvirt/envelopes.py new file mode 100644 index 000000000..34a31a886 --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/envelopes.py @@ -0,0 +1,39 @@ +"""Configuration-bound realization envelopes for the libvirt backend.""" + +from __future__ import annotations + +import json +from enum import Enum + +from aces_contracts.corpus import REALIZATION_ENVELOPES, corpus_family_root +from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel, realizer_configuration_digest + + +class LibvirtDriverMode(str, Enum): + GENERIC = "generic" + TECHVAULT_APPLIANCE = "techvault-appliance" + GUEST_CERTIFIED_APPLIANCE = "guest-certified-appliance" + + +_ARTIFACTS = { + LibvirtDriverMode.GENERIC: "generic-v1.json", + LibvirtDriverMode.TECHVAULT_APPLIANCE: "techvault-appliance-v1.json", + LibvirtDriverMode.GUEST_CERTIFIED_APPLIANCE: "guest-certified-appliance-v1.json", +} + + +def load_libvirt_realization_envelope(mode: LibvirtDriverMode | str) -> BackendRealizationEnvelopeModel: + """Load and validate the packaged envelope for one material driver mode.""" + + normalized = LibvirtDriverMode(mode) + path = corpus_family_root(REALIZATION_ENVELOPES) / "libvirt-qemu" / _ARTIFACTS[normalized] + payload = json.loads(path.read_text(encoding="utf-8")) + envelope = BackendRealizationEnvelopeModel.model_validate(payload) + if envelope.configuration.mode != normalized.value: + raise ValueError("libvirt realization envelope mode does not match selected driver mode") + if envelope.configuration.configuration_digest != realizer_configuration_digest(envelope.configuration): + raise ValueError("libvirt realization envelope configuration digest does not match selected driver mode") + return envelope + + +__all__ = ["LibvirtDriverMode", "load_libvirt_realization_envelope"] diff --git a/implementations/python/packages/aces_backend_libvirt/guest_appliance.py b/implementations/python/packages/aces_backend_libvirt/guest_appliance.py new file mode 100644 index 000000000..2e3ffbb61 --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/guest_appliance.py @@ -0,0 +1,251 @@ +"""Guest-observing initramfs appliance for guest-certified libvirt runs. + +Builds a BusyBox appliance that realizes bounded account, file, and service +placements inside the guest, then reads the *realized* system back (its own +``/proc``, ``/etc``, link state, file digests, service state) and emits a +bounded, line-oriented fact report to the file-backed serial fact channel. The +fresh per-run challenge is read from the kernel command line, so the appliance +image bytes (and therefore their digest) are independent of the challenge. +""" + +from __future__ import annotations + +import gzip +import json +import os +import shutil +import tempfile +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path + +from .techvault_appliance import _cpio_newc, _shell_quote + +_APPLETS = ( + "sh", "mount", "mdev", "ip", "ifconfig", "sleep", "cat", "hostname", "printf", "echo", + "uname", "nproc", "awk", "sed", "grep", "cut", "tr", "sort", "head", "wc", + "sha256sum", "stat", "id", "mkdir", "chmod", "chown", "touch", "dirname", "basename", + "netstat", "nc", "kill", "cp", "ls", +) # fmt: skip + +# Baseline account-database file lines written into the appliance rootfs. These +# are /etc/{passwd,group,shadow} record formats for the root account, not secrets. +_ROOT_ACCOUNT_LINE = "root:x:0:0:root:/root:/bin/sh\n" +_ROOT_GROUP_LINE = "root:x:0:\n" +_ROOT_SHADOW_LINE = "root:x:19000:0:99999:7:::\n" + +# Shared shell guard reused across the account/service read loops. +_SKIP_IF_NO_NAME = ' [ -n "$name" ] || continue' + + +@dataclass +class GuestObservingInitramfsBuilder: + """Build a guest-observing appliance that certifies concerns from inside.""" + + 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: + root = Path(tmp) + _write_guest_root(root, self.busybox_path, domain) + target.parent.mkdir(parents=True, exist_ok=True) + payload = _cpio_newc(root) + target.write_bytes(gzip.compress(payload, compresslevel=6)) + return target + + +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" + files_dir = guest_dir / "files" + for directory in ( + bin_dir, + files_dir, + root / "proc", + root / "sys", + root / "dev", + root / "tmp", + root / "run", + root / "home", + ): + directory.mkdir(parents=True, exist_ok=True) + shutil.copy2(busybox_path, bin_dir / "busybox") + for applet in _APPLETS: + (bin_dir / applet).symlink_to("busybox") + (etc_dir / "passwd").write_text(_ROOT_ACCOUNT_LINE, encoding="utf-8") + (etc_dir / "group").write_text(_ROOT_GROUP_LINE, encoding="utf-8") + (etc_dir / "shadow").write_text(_ROOT_SHADOW_LINE, encoding="utf-8") + _write_placement_specs(guest_dir, files_dir, domain) + (guest_dir / "domain.json").write_text(json.dumps(domain, indent=2, sort_keys=True) + "\n", encoding="utf-8") + (root / "init").write_text(_init_script(domain), encoding="utf-8") + os.chmod(root / "init", 0o700) + os.chmod(bin_dir / "busybox", 0o700) + + +def _write_placement_specs(guest_dir: Path, files_dir: Path, domain: Mapping[str, object]) -> None: + accounts = [ + "|".join( + ( + str(item.get("name", "")), + ",".join(str(group) for group in _as_sequence(item.get("groups"))), + str(item.get("shell", "")), + str(item.get("home", "")), + "1" if item.get("disabled") else "0", + ) + ) + for item in _as_sequence(domain.get("accounts")) + if isinstance(item, Mapping) + ] + content_lines = [] + for index, item in enumerate(_as_sequence(domain.get("content"))): + if not isinstance(item, Mapping): + continue + (files_dir / str(index)).write_text(str(item.get("content", "")), encoding="utf-8") + content_lines.append("|".join((str(item.get("path", "")), str(item.get("mode", "0644")), str(index)))) + services = [ + "|".join((str(item.get("name", "")), str(item.get("port", "")))) + for item in _as_sequence(domain.get("services")) + if isinstance(item, Mapping) + ] + (guest_dir / "accounts").write_text("\n".join(accounts) + ("\n" if accounts else ""), encoding="utf-8") + (guest_dir / "content").write_text("\n".join(content_lines) + ("\n" if content_lines else ""), encoding="utf-8") + (guest_dir / "services").write_text("\n".join(services) + ("\n" if services else ""), encoding="utf-8") + + +def _init_script(domain: Mapping[str, object]) -> str: + lines = [ + "#!/bin/sh", + "export PATH=/bin", + "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')))}", + "ip link set lo up", + "for iface_path in /sys/class/net/*; do", + " iface=${iface_path##*/}", + ' [ "$iface" = lo ] && continue', + ' mac=$(cat "$iface_path/address")', + ' ip link set "$iface" up', + ' case "$mac" in', + ] + for interface in _as_sequence(domain.get("interfaces")): + if not isinstance(interface, Mapping): + continue + lines.extend( + [ + f" {interface.get('mac')})", + f' ip addr add {interface.get("ip")}/{interface.get("cidr_prefix")} dev "$iface"', + " ;;", + ] + ) + 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.extend(_REPORT_SNIPPET) + lines.append("while true; do sleep 3600; done") + lines.append("") + return "\n".join(lines) + + +_REALIZE_SNIPPET = [ + "if [ -f /etc/aces/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:" >> /etc/group', + ' if [ "$disabled" = 1 ]; then echo "$name:!:19000:0:99999:7:::" >> /etc/shadow;', + ' else echo "$name:*:19000:0:99999:7:::" >> /etc/shadow; fi', + " oldifs=$IFS; IFS=,", + " for g in $groups; do", + " IFS=$oldifs", + ' [ -n "$g" ] || { IFS=,; continue; }', + ' if grep -q "^$g:" /etc/group; then', + ' sed -i "s/^\\($g:[^:]*:[^:]*:\\)\\(.*\\)$/\\1\\2,$name/" /etc/group', + " else", + " gid=$(awk -F: 'BEGIN{m=2000}$3>=m{m=$3+1}END{print m}' /etc/group)", + ' echo "$g:x:$gid:$name" >> /etc/group', + " fi", + " IFS=,", + " done", + " IFS=$oldifs", + "done < /etc/aces/guest/accounts", + "fi", + "if [ -f /etc/aces/guest/content ]; then", + "while IFS='|' read path mode idx; do", + ' [ -n "$path" ] || continue', + ' mkdir -p "$(dirname "$path")"', + ' cp "/etc/aces/guest/files/$idx" "$path"', + ' chmod "$mode" "$path"', + "done < /etc/aces/guest/content", + "fi", + "if [ -f /etc/aces/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", + "fi", +] + +_REPORT_SNIPPET = [ + "FC=/dev/ttyS1", + "{", + "echo 'ACES-GUEST-FACTS v1'", + 'echo "challenge $challenge"', + 'echo "architecture $(uname -m)"', + 'echo "vcpus $(nproc)"', + "echo \"memory_mib $(awk '/MemTotal/{print int($2/1024)}' /proc/meminfo)\"", + "for iface_path in /sys/class/net/*; do", + ' iface=${iface_path##*/}; [ "$iface" = lo ] && continue', + ' mac=$(cat "$iface_path/address")', + " ip4=$(ip -4 -o addr show dev \"$iface\" 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -n1)", + ' 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", + "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", + "fi", + "if [ -f /etc/aces/guest/accounts ]; then", + "while IFS='|' read name groups shell home disabled; do", + _SKIP_IF_NO_NAME, + ' entry=$(grep "^$name:" /etc/passwd) || continue', + ' uid=$(echo "$entry" | cut -d: -f3)', + ' h=$(echo "$entry" | cut -d: -f6)', + ' sh=$(echo "$entry" | cut -d: -f7)', + ' grps=$(awk -F: -v u="$name" \'{n=split($4,a,","); for(i=1;i<=n;i++) if(a[i]==u) print $1}\' /etc/group \\', + " | sort | tr '\\n' ',' | sed 's/,$//')", + ' 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", + "fi", + "if [ -f /etc/aces/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', + ' echo "service $name $port $lis $pid"', + "done < /etc/aces/guest/services", + "fi", + "echo 'init complete'", + '} > "$FC" 2>/dev/null', +] + + +def _as_sequence(value: object) -> Sequence[object]: + return value if isinstance(value, list | tuple) else () + + +__all__ = ["GuestObservingInitramfsBuilder"] diff --git a/implementations/python/packages/aces_backend_libvirt/guest_certified_driver.py b/implementations/python/packages/aces_backend_libvirt/guest_certified_driver.py new file mode 100644 index 000000000..ca616a323 --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/guest_certified_driver.py @@ -0,0 +1,153 @@ +"""Native libvirt driver that certifies realization from inside the guest. + +Extends :class:`TechVaultNativeLibvirtDriver` with the guest-observation stage: +after ownership-safe define/create and daemon readback, it boots a +guest-observing appliance, reads concern-specific facts back through the +credential-free fact channel, and refuses to finalize unless the fresh, +challenge-bound guest observations match the requested realization. The +challenge rides the kernel command line (not the appliance image), so the +appliance content digest stays stable across runs while every report is bound to +this boot. +""" + +from __future__ import annotations + +import re +import secrets +from collections.abc import Mapping +from contextlib import suppress +from dataclasses import dataclass, field +from pathlib import Path +from typing import ClassVar + +from aces_contracts.diagnostics import Diagnostic + +from .driver import DomainSpec, NetworkSpec, RealizationObservation +from .drivers.libvirt import _aces_uuid +from .guest_appliance import GuestObservingInitramfsBuilder +from .guest_observation import GuestObservationConfig, correlation_digest, observe_guest +from .guest_transport import FileSerialGuestFactTransport, GuestFactTransport +from .techvault_appliance import InitramfsBuilder +from .techvault_concerns import guest_certified_spec_diagnostics +from .techvault_matrix import domain_xml as _domain_xml +from .techvault_matrix import native_matrix as _native_matrix +from .techvault_native import DriverResult, TechVaultNativeLibvirtDriver, _artifact_token + +_SAFE_CHALLENGE_RE = re.compile(r"^[a-f0-9]{16,64}$") + + +@dataclass +class GuestCertifiedLibvirtDriver(TechVaultNativeLibvirtDriver): + """Realize TechVault domains and certify concerns from inside the guest.""" + + driver_mode: ClassVar[str] = "guest-certified-appliance" + + initramfs_builder: InitramfsBuilder = field(default_factory=GuestObservingInitramfsBuilder) + guest_transport: GuestFactTransport = field(default_factory=FileSerialGuestFactTransport) + guest_config: GuestObservationConfig = field(default_factory=GuestObservationConfig) + challenge: str | None = None + last_guest_observations: tuple[RealizationObservation, ...] = () + last_guest_facts: dict[str, object] = field(default_factory=dict) + last_guest_binding: dict[str, object] = field(default_factory=dict) + + def __post_init__(self) -> None: + super().__post_init__() + if self.challenge is None: + self.challenge = secrets.token_hex(16) + elif not _SAFE_CHALLENGE_RE.match(self.challenge): + raise ValueError("guest-certified challenge must be a lowercase hex token") + + def _admission_diagnostics( + self, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + envelope: object, + ) -> list[Diagnostic]: + from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel + + assert isinstance(envelope, BackendRealizationEnvelopeModel) + return guest_certified_spec_diagnostics( + networks=networks, + domains=domains, + envelope=envelope, + name_prefix=self.name_prefix, + ) + + def _build_matrix( + self, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + ) -> dict[str, object]: + return _native_matrix( + networks=networks, + domains=domains, + name_prefix=self.name_prefix, + include_placements=True, + ) + + def _render_domain_xml(self, domain: Mapping[str, object], *, kernel: Path, initrd: Path) -> str: + address = str(domain.get("address", "")) + fact_channel = self._fact_channel_path(address) + fact_channel.parent.mkdir(parents=True, exist_ok=True) + return _domain_xml( + domain, + kernel=kernel, + initrd=initrd, + appliance="guest-certified", + challenge=self.challenge, + fact_channel_path=fact_channel, + ) + + def _guest_stage( + self, + connection: object, + matrix: Mapping[str, object], + specs: tuple[tuple[NetworkSpec, ...], tuple[DomainSpec, ...]], + observations: tuple[RealizationObservation, ...], + ) -> tuple[tuple[RealizationObservation, ...], list[Diagnostic]]: + del connection, specs, observations + assert self.challenge is not None + outcome = observe_guest( + matrix=matrix, + transport=self.guest_transport, + challenge=self.challenge, + config=self.guest_config, + fact_channel_path_for=self._fact_channel_path, + ) + if outcome.diagnostics: + return (), list(outcome.diagnostics) + self.last_guest_observations = outcome.observations + self.last_guest_facts = dict(outcome.facts) + self.last_guest_binding = self._guest_binding(matrix) + return outcome.observations, [] + + def destroy(self, *, networks: tuple[str, ...], domains: tuple[str, ...]) -> DriverResult: + result = super().destroy(networks=networks, domains=domains) + if (networks or domains) and not result.diagnostics: + self.last_guest_observations = () + self.last_guest_facts = {} + self.last_guest_binding = {} + return result + + def _cleanup_artifacts(self, address: str) -> None: + super()._cleanup_artifacts(address) + with suppress(OSError): + self._fact_channel_path(address).unlink() + + def _fact_channel_path(self, address: str) -> Path: + return self.state_dir / "guest-facts" / f"{_artifact_token(address)}.facts" + + 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", "")))) + for domain in domains + } + return { + "challenge": self.challenge, + "probe_policy": self.guest_config.probe_policy, + "correlations": correlations, + } + + +__all__ = ["GuestCertifiedLibvirtDriver"] diff --git a/implementations/python/packages/aces_backend_libvirt/guest_observation.py b/implementations/python/packages/aces_backend_libvirt/guest_observation.py new file mode 100644 index 000000000..2b9583c3d --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/guest_observation.py @@ -0,0 +1,362 @@ +"""Concern-specific guest observation for guest-certified libvirt runs. + +Boot readiness and initialization completion are staged gates that must pass +before any concern fact is trusted; a later stage never repairs an earlier one. +Each accepted fact is read *from inside the realized guest* (its own ``/proc``, +``/sys``, ``/etc`` and link/file/account/service state), not echoed from the +plan, and is carried as a :class:`RealizationObservation` with +``ObservationStrength.GUEST_OBSERVED``. Failures are stable, redacted +:class:`Diagnostic` codes that name the safe ACES address and the observation +level. A fresh per-run challenge proves the report belongs to this boot, not a +cached or prior one. +""" + +from __future__ import annotations + +import hashlib +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path + +from aces_contracts.diagnostics import Diagnostic, Severity +from aces_contracts.realization_envelope import ObservationStrength, RealizationConcern + +from .driver import RealizationObservation +from .guest_transport import ( + FAILURE_TIMEOUT, + FAILURE_UNAVAILABLE, + GuestFactTransport, + parse_guest_facts, +) +from .techvault_matrix import as_sequence + +_DOMAIN = "runtime" +_CODE_TRANSPORT_UNAVAILABLE = "libvirt-backend.guest.transport-unavailable" +_CODE_BOOT_TIMEOUT = "libvirt-backend.guest.boot-timeout" +_CODE_INIT_INCOMPLETE = "libvirt-backend.guest.init-incomplete" +_CODE_CHALLENGE_MISMATCH = "libvirt-backend.guest.challenge-mismatch" +_CODE_OBSERVATION_MALFORMED = "libvirt-backend.guest.observation-malformed" +_CODE_OBSERVATION_DUPLICATE = "libvirt-backend.guest.observation-duplicate" +_CODE_OBSERVATION_MISSING = "libvirt-backend.guest.observation-missing" +_CODE_OBSERVATION_MISMATCH = "libvirt-backend.guest.observation-mismatch" + +_FAILURE_CODES = {FAILURE_UNAVAILABLE: _CODE_TRANSPORT_UNAVAILABLE, FAILURE_TIMEOUT: _CODE_BOOT_TIMEOUT} + +_MESSAGES = { + _CODE_TRANSPORT_UNAVAILABLE: "Guest fact transport was unavailable at the disclosed channel.", + _CODE_BOOT_TIMEOUT: "Guest did not report boot readiness within the observation deadline.", + _CODE_INIT_INCOMPLETE: "Guest initialization did not report completion.", + _CODE_CHALLENGE_MISMATCH: "Guest report did not carry the fresh per-run challenge.", + _CODE_OBSERVATION_MALFORMED: "Guest report was malformed or missing its bounded fact header.", + _CODE_OBSERVATION_DUPLICATE: "Guest report carried duplicate observations for a singleton fact.", + _CODE_OBSERVATION_MISSING: "Guest did not report every concern in the guest-observed inventory.", + _CODE_OBSERVATION_MISMATCH: "Guest-observed concern values did not match the requested realization.", +} + +_Key = tuple[str, str, RealizationConcern] + + +@dataclass(frozen=True) +class GuestObservationConfig: + """Per-stage and overall deadlines plus the disclosed probe-policy version.""" + + probe_policy: str = "serial-fact-channel/v1" + transport_deadline_seconds: float = 120.0 + + +@dataclass(frozen=True) +class _GuestOutcome: + observations: tuple[RealizationObservation, ...] = () + diagnostics: tuple[Diagnostic, ...] = () + facts: Mapping[str, object] = field(default_factory=dict) + + +def guest_observation( + address: str, field_path: str, concern: RealizationConcern, value: object +) -> RealizationObservation: + """Build a bounded guest-observed fact carrier for one concern field.""" + + return RealizationObservation( + address=address, + field_path=field_path, + concern=concern, + source=ObservationStrength.GUEST_OBSERVED, + value=value, + ) + + +def observe_guest( + *, + matrix: Mapping[str, object], + transport: GuestFactTransport, + challenge: str, + config: GuestObservationConfig, + fact_channel_path_for: Callable[[str], Path], +) -> _GuestOutcome: + """Run the staged guest observation for every domain in ``matrix``.""" + + observations: list[RealizationObservation] = [] + diagnostics: list[Diagnostic] = [] + facts_by_address: dict[str, object] = {} + domains = [item for item in as_sequence(matrix.get("domains")) if isinstance(item, Mapping)] + for domain in domains: + address = str(domain.get("address", "")) + parsed, stage_diag = _read_and_validate(domain, transport, challenge, config, fact_channel_path_for) + if stage_diag is not None: + diagnostics.append(stage_diag) + continue + assert parsed is not None + observations.extend(_domain_observations(domain, parsed)) + facts_by_address[address] = _bounded_facts(parsed) + if not diagnostics: + expected = expected_guest_observations(domains) + diagnostics.extend(guest_observation_diagnostics(expected=expected, observations=tuple(observations))) + return _GuestOutcome(tuple(observations), tuple(diagnostics), facts_by_address) + + +def _read_and_validate( + domain: Mapping[str, object], + transport: GuestFactTransport, + challenge: str, + config: GuestObservationConfig, + fact_channel_path_for: Callable[[str], Path], +) -> tuple[Mapping[str, object] | None, Diagnostic | None]: + address = str(domain.get("address", "")) + text, failure = transport.read( + address=address, + fact_channel_path=fact_channel_path_for(address), + deadline_seconds=config.transport_deadline_seconds, + ) + if failure is not None: + return None, _diagnostic(_FAILURE_CODES.get(failure, _CODE_TRANSPORT_UNAVAILABLE), address) + parsed = parse_guest_facts(text or "") + code = _staging_failure_code(parsed, challenge) + if code is not None: + return None, _diagnostic(code, address) + return parsed, None + + +def _staging_failure_code(parsed: Mapping[str, object] | None, challenge: str) -> str | None: + """Return the first staged-observation failure code, or None when the report is trusted. + + Boot readiness, initialization, and freshness are ordered gates; a later gate never + repairs an earlier one, so the first failing check wins. + """ + + if parsed is None: + return _CODE_OBSERVATION_MALFORMED + staged = ( + (bool(parsed.get("duplicate")), _CODE_OBSERVATION_DUPLICATE), + (not parsed.get("init_complete"), _CODE_INIT_INCOMPLETE), + (parsed.get("challenge") != challenge, _CODE_CHALLENGE_MISMATCH), + ) + return next((code for failed, code in staged if failed), None) + + +def _domain_observations( + domain: Mapping[str, object], parsed: Mapping[str, object] +) -> tuple[RealizationObservation, ...]: + address = str(domain.get("address", "")) + requested_memory = _as_int(domain.get("memory_mib")) + guest_memory = _as_int(parsed.get("memory_mib")) + corroborated = requested_memory > 0 and requested_memory // 2 <= guest_memory <= requested_memory + return ( + guest_observation( + address, "guest-architecture", RealizationConcern.ARCHITECTURE, str(parsed.get("architecture") or "") + ), + guest_observation(address, "guest-vcpus", RealizationConcern.RESOURCE_ALLOCATION, _as_int(parsed.get("vcpus"))), + guest_observation(address, "guest-memory-corroborated", RealizationConcern.RESOURCE_ALLOCATION, corroborated), + guest_observation(address, "guest-network", RealizationConcern.NETWORK, _observed_network(parsed)), + guest_observation(address, "guest-content", RealizationConcern.CONTENT_PLACEMENT, _observed_content(parsed)), + guest_observation(address, "guest-account", RealizationConcern.ACCOUNT_PLACEMENT, _observed_accounts(parsed)), + guest_observation(address, "guest-service", RealizationConcern.SERVICE, _observed_services(parsed)), + ) + + +def expected_guest_observations(domains: Sequence[Mapping[str, object]]) -> dict[_Key, object]: + """Project the guest-observed inventory the plan requires per domain.""" + + expected: dict[_Key, object] = {} + for domain in domains: + address = str(domain.get("address", "")) + expected[(address, "guest-architecture", RealizationConcern.ARCHITECTURE)] = "x86_64" + expected[(address, "guest-vcpus", RealizationConcern.RESOURCE_ALLOCATION)] = _as_int(domain.get("vcpus")) + expected[(address, "guest-memory-corroborated", RealizationConcern.RESOURCE_ALLOCATION)] = True + expected[(address, "guest-network", RealizationConcern.NETWORK)] = _expected_network(domain) + expected[(address, "guest-content", RealizationConcern.CONTENT_PLACEMENT)] = _expected_content(domain) + expected[(address, "guest-account", RealizationConcern.ACCOUNT_PLACEMENT)] = _expected_accounts(domain) + expected[(address, "guest-service", RealizationConcern.SERVICE)] = _expected_services(domain) + return expected + + +def guest_observation_diagnostics( + *, + expected: Mapping[_Key, object], + observations: tuple[RealizationObservation, ...], +) -> list[Diagnostic]: + """Require exactly one matching guest-observed fact per expected concern field.""" + + observed: dict[_Key, list[RealizationObservation]] = {} + for item in observations: + observed.setdefault((item.address, item.field_path, item.concern), []).append(item) + missing: set[str] = set() + mismatched: set[str] = set() + for key, expected_value in expected.items(): + candidates = observed.get(key, []) + if not candidates or any(item.source is not ObservationStrength.GUEST_OBSERVED for item in candidates): + missing.add(key[0]) + elif ( + len(candidates) != 1 + or type(candidates[0].value) is not type(expected_value) + or candidates[0].value != expected_value + ): + mismatched.add(key[0]) + diagnostics = [_diagnostic(_CODE_OBSERVATION_MISSING, address) for address in sorted(missing)] + diagnostics.extend(_diagnostic(_CODE_OBSERVATION_MISMATCH, address) for address in sorted(mismatched - missing)) + return diagnostics + + +def correlation_digest(native_identity: str) -> str: + """Return a redacted sha256 correlation for an ownership-verified native id.""" + + return "sha256:" + hashlib.sha256(native_identity.encode("utf-8")).hexdigest() + + +# --- observed projections (actual guest readings) ------------------------------ + + +def _observed_network(parsed: Mapping[str, object]) -> tuple[str, ...]: + return tuple( + sorted( + f"{item.get('mac')}|{item.get('ipv4')}" + for item in as_sequence(parsed.get("interfaces")) + if isinstance(item, Mapping) and item.get("up") + ) + ) + + +def _observed_content(parsed: Mapping[str, object]) -> tuple[str, ...]: + return tuple( + sorted( + f"{item.get('path')}|{item.get('sha256')}|{_norm_mode(str(item.get('mode', '')))}" + for item in as_sequence(parsed.get("content")) + if isinstance(item, Mapping) + ) + ) + + +def _observed_accounts(parsed: Mapping[str, object]) -> tuple[str, ...]: + return tuple( + sorted(_account_token(item) for item in as_sequence(parsed.get("accounts")) if isinstance(item, Mapping)) + ) + + +def _observed_services(parsed: Mapping[str, object]) -> tuple[str, ...]: + return tuple( + sorted( + f"{item.get('name')}|{item.get('port')}|{_flag(item.get('listening'))}|{_flag(item.get('pid_present'))}" + for item in as_sequence(parsed.get("services")) + if isinstance(item, Mapping) + ) + ) + + +# --- expected projections (plan-derived) --------------------------------------- + + +def _expected_network(domain: Mapping[str, object]) -> tuple[str, ...]: + return tuple( + sorted( + f"{item.get('mac')}|{item.get('ip')}" + for item in as_sequence(domain.get("interfaces")) + if isinstance(item, Mapping) + ) + ) + + +def _expected_content(domain: Mapping[str, object]) -> tuple[str, ...]: + def token(item: Mapping[str, object]) -> str: + digest = _content_digest(str(item.get("content", ""))) + mode = _norm_mode(str(item.get("mode", ""))) + return f"{item.get('path')}|{digest}|{mode}" + + return tuple(sorted(token(item) for item in as_sequence(domain.get("content")) if isinstance(item, Mapping))) + + +def _expected_accounts(domain: Mapping[str, object]) -> tuple[str, ...]: + tokens = [] + for item in as_sequence(domain.get("accounts")): + if not isinstance(item, Mapping): + continue + name = str(item.get("name", "")) + home = str(item.get("home") or f"/home/{name}") + shell = str(item.get("shell") or "/bin/sh") + groups = ",".join(sorted(str(group) for group in as_sequence(item.get("groups")))) + tokens.append(f"{name}|{home}|{shell}|{_flag(item.get('disabled'))}|{groups}") + return tuple(sorted(tokens)) + + +def _expected_services(domain: Mapping[str, object]) -> tuple[str, ...]: + return tuple( + sorted( + f"{item.get('name')}|{item.get('port')}|1|1" + for item in as_sequence(domain.get("services")) + if isinstance(item, Mapping) + ) + ) + + +def _account_token(item: Mapping[str, object]) -> str: + name = str(item.get("name", "")) + home = str(item.get("home", "")) + shell = str(item.get("shell", "")) + groups = ",".join(sorted(str(group) for group in as_sequence(item.get("groups")))) + return f"{name}|{home}|{shell}|{_flag(item.get('disabled'))}|{groups}" + + +def _bounded_facts(parsed: Mapping[str, object]) -> dict[str, object]: + return { + "architecture": str(parsed.get("architecture") or ""), + "vcpus": _as_int(parsed.get("vcpus")), + "memory_mib": _as_int(parsed.get("memory_mib")), + "interfaces": _observed_network(parsed), + "content": _observed_content(parsed), + "accounts": _observed_accounts(parsed), + "services": _observed_services(parsed), + } + + +def _content_digest(content: str) -> str: + return hashlib.sha256(content.encode("utf-8")).hexdigest() + + +def _norm_mode(mode: str) -> str: + return mode.lstrip("0") or "0" + + +def _flag(value: object) -> str: + return "1" if value else "0" + + +def _as_int(value: object) -> int: + return value if isinstance(value, int) else -1 + + +def _diagnostic(code: str, address: str) -> Diagnostic: + return Diagnostic( + code=code, + domain=_DOMAIN, + address=address, + message=_MESSAGES.get(code, "Guest observation did not succeed."), + severity=Severity.ERROR, + ) + + +__all__ = [ + "GuestObservationConfig", + "correlation_digest", + "expected_guest_observations", + "guest_observation", + "guest_observation_diagnostics", + "observe_guest", +] diff --git a/implementations/python/packages/aces_backend_libvirt/guest_transport.py b/implementations/python/packages/aces_backend_libvirt/guest_transport.py new file mode 100644 index 000000000..761e96489 --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/guest_transport.py @@ -0,0 +1,218 @@ +"""Credential-free guest-fact transport for guest-certified libvirt runs. + +The guest-certified appliance writes a bounded, line-oriented fact report to a +dedicated file-backed serial channel (``/dev/ttyS1`` in the guest maps to a +run-local host file). The host reads that file through the :class:`GuestFactTransport` +seam and parses it into a bounded structured mapping. This transport carries no +credential and is not a general command channel: the guest emits fixed, +read-only, size-bounded facts and nothing else. A future libvirt guest-agent +transport can replace this implementation without changing the observer, the +concern taxonomy, or the evidence schema. +""" + +from __future__ import annotations + +import time +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Protocol + +_Handler = Callable[[dict[str, object], list[str]], None] + +FACT_HEADER = "ACES-GUEST-FACTS v1" +INIT_COMPLETE_MARKER = "init complete" + +# Bounds keep a hostile or malfunctioning guest from flooding host memory or the +# evidence artifact. A well-formed report is far under every limit. +_MAX_FACT_BYTES = 64 * 1024 +_MAX_LINES = 512 +_MAX_LINE_CHARS = 512 +_MAX_ENTRIES = 64 + +# Stable transport-stage failure suffixes (namespaced by the observer). +FAILURE_UNAVAILABLE = "transport-unavailable" +FAILURE_TIMEOUT = "boot-timeout" + + +class GuestFactTransport(Protocol): + """Read one guest's bounded fact report through the disclosed channel.""" + + def read(self, *, address: str, fact_channel_path: Path, deadline_seconds: float) -> tuple[str | None, str | None]: + """Return ``(text, None)`` on success or ``(None, failure_suffix)`` on failure.""" + ... + + +@dataclass(frozen=True) +class FileSerialGuestFactTransport: + """Read the guest fact report from a file-backed serial channel. + + Polls the run-local host file the guest serial device is bound to until the + guest has emitted its terminal ``init complete`` marker or the deadline + elapses. Never raises: an unreadable channel is reported as a typed failure + suffix so the caller emits a redacted diagnostic instead of an exception. + """ + + poll_seconds: float = 0.5 + + def read(self, *, address: str, fact_channel_path: Path, deadline_seconds: float) -> tuple[str | None, str | None]: + del address + deadline = time.monotonic() + max(0.0, deadline_seconds) + saw_channel = False + while True: + text = _read_bounded(fact_channel_path) + if text is not None: + saw_channel = True + if _contains_terminal_marker(text): + return text, None + if time.monotonic() >= deadline: + if saw_channel: + return None, FAILURE_TIMEOUT + return None, FAILURE_UNAVAILABLE + time.sleep(self.poll_seconds) + + +def _read_bounded(path: Path) -> str | None: + try: + with path.open("rb") as stream: + payload = stream.read(_MAX_FACT_BYTES + 1) + except OSError: + return None + if len(payload) > _MAX_FACT_BYTES: + payload = payload[:_MAX_FACT_BYTES] + return payload.decode("utf-8", errors="replace") + + +def _contains_terminal_marker(text: str) -> bool: + return any(line.strip() == INIT_COMPLETE_MARKER for line in text.splitlines()) + + +def parse_guest_facts(text: str) -> Mapping[str, object] | None: + """Parse the bounded line-oriented guest report into a structured mapping. + + Returns ``None`` when the header is absent or the report is unparseable. + Unknown lines are ignored; bounds are enforced so a malformed report cannot + produce an unbounded structure. + """ + + lines = text.splitlines()[:_MAX_LINES] + if not lines or lines[0].strip() != FACT_HEADER: + return None + facts: dict[str, object] = { + "challenge": None, + "init_complete": False, + "architecture": None, + "vcpus": None, + "memory_mib": None, + "interfaces": [], + "content": [], + "accounts": [], + "services": [], + "duplicate": False, + } + seen: set[str] = set() + for raw in lines[1:]: + _consume_line(facts, raw[:_MAX_LINE_CHARS].rstrip("\n"), seen) + return facts + + +_SCALAR_KEYS = frozenset({"challenge", "architecture", "vcpus", "memory_mib"}) + + +def _consume_line(facts: dict[str, object], line: str, seen: set[str]) -> None: + stripped = line.strip() + if stripped == INIT_COMPLETE_MARKER: + facts["init_complete"] = True + return + parts = stripped.split(" ") + key = parts[0] if parts else "" + fields = parts[1:] + handler = _LINE_HANDLERS.get(key) + if handler is None: + return + # A repeated singleton fact (same key) or an identical repeated list line is a + # duplicate observation: record it distinctly rather than silently collapsing it. + marker = key if key in _SCALAR_KEYS else stripped + if marker in seen: + facts["duplicate"] = True + else: + seen.add(marker) + handler(facts, fields) + + +def _set_scalar(name: str, cast: Callable[[str], object]) -> _Handler: + def _apply(facts: dict[str, object], fields: list[str]) -> None: + if fields: + facts[name] = cast(fields[0]) + + return _apply + + +def _append(name: str, arity: int, build: Callable[[list[str]], dict[str, object]]) -> _Handler: + def _apply(facts: dict[str, object], fields: list[str]) -> None: + bucket = facts[name] + if isinstance(bucket, list) and len(bucket) < _MAX_ENTRIES and len(fields) >= arity: + bucket.append(build(fields)) + + return _apply + + +def _to_int(value: str) -> int | None: + try: + return int(value) + except ValueError: + return None + + +def _iface_entry(fields: list[str]) -> dict[str, object]: + return {"mac": fields[0], "ipv4": fields[1], "up": fields[2] == "1"} + + +def _content_entry(fields: list[str]) -> dict[str, object]: + return {"path": fields[0], "sha256": fields[1], "mode": fields[2]} + + +def _account_entry(fields: list[str]) -> dict[str, object]: + # The trailing groups field is optional: an account with no supplemental groups + # emits five fields (the empty groups token is trimmed away by the transport). + groups = [group for group in fields[5].split(",") if group] if len(fields) > 5 else [] + return { + "name": fields[0], + "uid": _to_int(fields[1]), + "home": fields[2], + "shell": fields[3], + "disabled": fields[4] == "1", + "groups": sorted(groups), + } + + +def _service_entry(fields: list[str]) -> dict[str, object]: + return { + "name": fields[0], + "port": _to_int(fields[1]), + "listening": fields[2] == "1", + "pid_present": fields[3] == "1", + } + + +_LINE_HANDLERS = { + "challenge": _set_scalar("challenge", str), + "architecture": _set_scalar("architecture", str), + "vcpus": _set_scalar("vcpus", _to_int), + "memory_mib": _set_scalar("memory_mib", _to_int), + "iface": _append("interfaces", 3, _iface_entry), + "content": _append("content", 3, _content_entry), + "account": _append("accounts", 5, _account_entry), + "service": _append("services", 4, _service_entry), +} + + +__all__ = [ + "FACT_HEADER", + "FAILURE_TIMEOUT", + "FAILURE_UNAVAILABLE", + "INIT_COMPLETE_MARKER", + "FileSerialGuestFactTransport", + "GuestFactTransport", + "parse_guest_facts", +] diff --git a/implementations/python/packages/aces_backend_libvirt/manifest.py b/implementations/python/packages/aces_backend_libvirt/manifest.py index dfd3f27f4..5251bcc49 100644 --- a/implementations/python/packages/aces_backend_libvirt/manifest.py +++ b/implementations/python/packages/aces_backend_libvirt/manifest.py @@ -15,26 +15,42 @@ from aces_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration from aces_contracts.vocabulary import ParticipantFeatureSupportLevel, RealizationSupportMode +from .envelopes import LibvirtDriverMode, load_libvirt_realization_envelope + LIBVIRT_BACKEND_NAME = "libvirt-qemu" -# The libvirt provisioning capability envelope: the maximum governed provisioning -# vocabulary the driver realizes through cloud-init. Single source of truth for -# both the rendered manifest and the backend's capability-envelope diagnostics -# (issue #605), so the declared envelope and the enforced envelope cannot drift. -LIBVIRT_PROVISIONER_CAPABILITIES = ProvisionerCapabilities( - name="libvirt-provisioner", - supported_node_types=frozenset({"switch", "vm"}), - supported_os_families=frozenset({"linux", "windows", "macos", "freebsd", "other"}), - supported_content_types=frozenset({"file", "dataset", "directory"}), - supported_account_features=frozenset({"groups", "mail", "spn", "shell", "home", "disabled", "auth_method"}), - max_total_nodes=None, - supports_acls=True, - supports_accounts=True, -) + +def _provisioner_capabilities(mode: LibvirtDriverMode) -> ProvisionerCapabilities: + """Derive the coarse manifest projection from the selected governed envelope.""" + + envelope = load_libvirt_realization_envelope(mode) + configuration = envelope.configuration + account_features = frozenset(configuration.supported_account_features) + return ProvisionerCapabilities( + name=( + "libvirt-techvault-appliance-provisioner" + if mode is LibvirtDriverMode.TECHVAULT_APPLIANCE + else "libvirt-provisioner" + ), + supported_node_types=frozenset(configuration.supported_node_types), + supported_os_families=frozenset(configuration.supported_os_families), + supported_content_types=frozenset(configuration.supported_content_types), + supported_account_features=account_features, + max_total_nodes=None, + supports_acls=configuration.supports_acls, + supports_accounts=bool(account_features), + ) + + +# Compatibility exports remain, but their values are derived from the normative +# envelope artifacts so manifest and execution gates cannot drift independently. +LIBVIRT_PROVISIONER_CAPABILITIES = _provisioner_capabilities(LibvirtDriverMode.GENERIC) +TECHVAULT_PROVISIONER_CAPABILITIES = _provisioner_capabilities(LibvirtDriverMode.TECHVAULT_APPLIANCE) _LIBVIRT_BASE_CONTRACT_VERSIONS = frozenset( { "backend-manifest-v2", + "realization-envelope-v1", "operation-receipt-v1", "operation-status-v1", "provisioning-plan-v1", @@ -120,17 +136,17 @@ def _participant_runtime_capabilities() -> ParticipantRuntimeCapabilities: def create_libvirt_manifest(**config: object) -> BackendManifest: """Return the libvirt backend manifest. - The manifest declares the *maximum* governed provisioning vocabulary the - libvirt/QEMU driver realizes through cloud-init: all node types, all OS - families, all content types (file/dataset/directory), and all account - features. Because every declared term is genuinely realized, the manifest - cannot over-claim. "Provisioning-only" here is domain scope only — by default - the backend implements the Provisioner protocol, not the orchestrator or - evaluator. Pass ``participant_runtime=True`` to additionally declare - participant episode support (``LibvirtParticipantRuntime`` with the - deterministic domain adapter). + The manifest projects its governed provisioning vocabulary from the + configuration-selected envelope. Generic qcow2/cloud-init and TechVault + appliance modes therefore disclose distinct capabilities. "Provisioning-only" + is domain scope only: by default the backend implements the Provisioner + protocol, not the orchestrator or evaluator. Pass ``participant_runtime=True`` + to additionally declare participant episode support. """ enable_participant_runtime = bool(config.get("participant_runtime", False)) + mode = LibvirtDriverMode(str(config.get("driver_mode", LibvirtDriverMode.GENERIC.value))) + realization_envelope = load_libvirt_realization_envelope(mode) + provisioner_capabilities = _provisioner_capabilities(mode) supported_contract_versions = ( _LIBVIRT_BASE_CONTRACT_VERSIONS | _LIBVIRT_PARTICIPANT_CONTRACT_VERSIONS @@ -167,7 +183,8 @@ def create_libvirt_manifest(**config: object) -> BackendManifest: ), ), capabilities=BackendCapabilitySet( - provisioner=LIBVIRT_PROVISIONER_CAPABILITIES, + provisioner=provisioner_capabilities, participant_runtime=participant_runtime_cap, ), + realization_envelope=realization_envelope, ) diff --git a/implementations/python/packages/aces_backend_libvirt/provisioner.py b/implementations/python/packages/aces_backend_libvirt/provisioner.py index 716dac4e9..0c8081831 100644 --- a/implementations/python/packages/aces_backend_libvirt/provisioner.py +++ b/implementations/python/packages/aces_backend_libvirt/provisioner.py @@ -5,19 +5,26 @@ from dataclasses import dataclass from aces_backend_protocols.capabilities import ProvisionerCapabilities +from aces_contracts.contracts import RealizationEnvelopeIdentityModel from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp, RuntimeDomain from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot, SnapshotEntry from ._payload import NETWORK_RESOURCE_TYPE, NODE_RESOURCE_TYPE -from .driver import DriverResult, LibvirtDriver -from .manifest import LIBVIRT_PROVISIONER_CAPABILITIES +from .driver import DomainSpec, DriverResult, LibvirtDriver, NetworkSpec +from .envelopes import LibvirtDriverMode, load_libvirt_realization_envelope +from .manifest import _provisioner_capabilities from .realization import Realization, interpret_provisioning_plan +from .techvault_concerns import techvault_observation_diagnostics +from .techvault_plan_admission import techvault_admission_diagnostics _DOMAIN = "runtime" INVALID_PLAN_CODE = "libvirt-backend.invalid-plan" UNCONFIRMED_DESTROY_CODE = "libvirt-backend.driver.unconfirmed-destroy" UNCONFIRMED_REALIZATION_CODE = "libvirt-backend.driver.unconfirmed-realization" +MISSING_ENVELOPE_CODE = "libvirt-backend.realization-envelope.missing" +MISMATCHED_ENVELOPE_CODE = "libvirt-backend.realization-envelope.mismatch" +BASELINE_ENVELOPE_MISMATCH_CODE = "libvirt-backend.realization-envelope.baseline-mismatch" @dataclass @@ -36,18 +43,39 @@ def __init__( driver: LibvirtDriver | None = None, *, provisioner_capabilities: ProvisionerCapabilities | None = None, + realization_envelope: RealizationEnvelopeIdentityModel | None = None, ) -> None: self._driver = driver if driver is not None else _default_driver() - # The capability envelope every plan term is validated against; defaults to - # the libvirt manifest envelope but tracks the manifest the target was built - # with when create_libvirt_components passes it in (issue #605). - self._provisioner_capabilities = provisioner_capabilities or LIBVIRT_PROVISIONER_CAPABILITIES + mode = LibvirtDriverMode(getattr(self._driver, "driver_mode", LibvirtDriverMode.GENERIC.value)) + expected_capabilities = _provisioner_capabilities(mode) + expected_envelope = load_libvirt_realization_envelope(mode).identity + if provisioner_capabilities is not None and provisioner_capabilities != expected_capabilities: + raise ValueError("libvirt provisioner capabilities do not match driver mode") + if realization_envelope is not None and realization_envelope != expected_envelope: + 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._backend_realization_envelope = load_libvirt_realization_envelope(mode) + self._realization_envelope = expected_envelope def validate(self, plan: ProvisioningPlan) -> list[Diagnostic]: if not isinstance(plan, ProvisioningPlan): return [_invalid_plan_diagnostic()] + identity_diagnostics = self._identity_diagnostics(plan, RuntimeSnapshot()) + if identity_diagnostics: + return identity_diagnostics realization = interpret_provisioning_plan(plan, provisioner_capabilities=self._provisioner_capabilities) - return list(realization.diagnostics) + diagnostics = list(realization.diagnostics) + if self._mode is LibvirtDriverMode.TECHVAULT_APPLIANCE: + diagnostics.extend( + techvault_admission_diagnostics( + plan, + self._backend_realization_envelope, + name_prefix=self._name_prefix, + ) + ) + return diagnostics def apply(self, plan: ProvisioningPlan, snapshot: RuntimeSnapshot) -> ApplyResult: if not isinstance(plan, ProvisioningPlan): @@ -57,8 +85,24 @@ def apply(self, plan: ProvisioningPlan, snapshot: RuntimeSnapshot) -> ApplyResul return result def _apply_provisioning_plan(self, plan: ProvisioningPlan, snapshot: RuntimeSnapshot) -> ApplyResult: + identity_diagnostics = self._identity_diagnostics(plan, snapshot) + if identity_diagnostics: + result = ApplyResult(success=False, snapshot=snapshot, diagnostics=identity_diagnostics) + else: + result = self._apply_realization(plan, snapshot) + return result + + def _apply_realization(self, plan: ProvisioningPlan, snapshot: RuntimeSnapshot) -> ApplyResult: realization = interpret_provisioning_plan(plan, provisioner_capabilities=self._provisioner_capabilities) diagnostics: list[Diagnostic] = list(realization.diagnostics) + if self._mode is LibvirtDriverMode.TECHVAULT_APPLIANCE: + diagnostics.extend( + techvault_admission_diagnostics( + plan, + self._backend_realization_envelope, + name_prefix=self._name_prefix, + ) + ) if _has_error(diagnostics): return ApplyResult(success=False, snapshot=snapshot, diagnostics=diagnostics) @@ -76,7 +120,10 @@ def _apply_provisioning_plan(self, plan: ProvisioningPlan, snapshot: RuntimeSnap return ApplyResult( success=True, - snapshot=snapshot.with_entries(reconciliation.entries), + snapshot=snapshot.with_entries( + reconciliation.entries, + realization_envelope=self._realization_envelope, + ), diagnostics=diagnostics, changed_addresses=reconciliation.changed_addresses, ) @@ -88,33 +135,105 @@ def _drive( delete_networks: list[str], delete_domains: list[str], ) -> list[Diagnostic]: - diagnostics: list[Diagnostic] = [] + active = self._active_addresses(plan, realization) + networks = tuple(spec for spec in realization.networks if spec.address in active) + domains = tuple(spec for spec in realization.domains if spec.address in active) + diagnostics = self._realize_active(networks, domains) + diagnostics.extend(self._delete_targets(delete_networks, delete_domains)) + return diagnostics + + @staticmethod + def _active_addresses(plan: ProvisioningPlan, realization: Realization) -> set[str]: active = {op.address for op in plan.operations if op.action in {ChangeAction.CREATE, ChangeAction.UPDATE}} # A changed placement must realize its target domain even when the node # itself is UNCHANGED: the domain's seed now carries different cloud-init. for placement_address, node_address in realization.placement_targets.items(): if placement_address in active: active.add(node_address) - networks = tuple(spec for spec in realization.networks if spec.address in active) - domains = tuple(spec for spec in realization.domains if spec.address in active) - if networks or domains: - result = self._driver.realize(networks=networks, domains=domains) - diagnostics.extend(result.diagnostics) - diagnostics.extend( - _unconfirmed_realization_diagnostics( - result, - requested=tuple(spec.address for spec in (*networks, *domains)), - ) + return active + + def _realize_active( + self, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + ) -> list[Diagnostic]: + if not (networks or domains): + return [] + result = self._driver.realize(networks=networks, domains=domains) + realization_diagnostics = [ + *result.diagnostics, + *_unconfirmed_realization_diagnostics( + result, requested=tuple(spec.address for spec in (*networks, *domains)) + ), + ] + diagnostics = list(realization_diagnostics) + observation_diagnostics: list[Diagnostic] = [] + if self._mode is LibvirtDriverMode.TECHVAULT_APPLIANCE and not result.diagnostics: + observation_diagnostics = techvault_observation_diagnostics( + networks=networks, + domains=domains, + result=result, ) - if delete_networks or delete_domains: - result = self._driver.destroy(networks=tuple(delete_networks), domains=tuple(delete_domains)) - diagnostics.extend(result.diagnostics) - diagnostics.extend( - _unconfirmed_destroy_diagnostics( - result, - requested=(*delete_networks, *delete_domains), + diagnostics.extend(observation_diagnostics) + if self._mode is LibvirtDriverMode.TECHVAULT_APPLIANCE and _has_error( + [*realization_diagnostics, *observation_diagnostics] + ): + diagnostics.extend(self._rollback_realization(networks, domains)) + return diagnostics + + def _rollback_realization( + self, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + ) -> list[Diagnostic]: + cleanup = self._driver.destroy( + networks=tuple(spec.address for spec in networks), + domains=tuple(spec.address for spec in domains), + ) + return [ + *cleanup.diagnostics, + *_unconfirmed_destroy_diagnostics( + cleanup, + requested=tuple(spec.address for spec in (*domains, *networks)), + ), + ] + + def _delete_targets(self, delete_networks: list[str], delete_domains: list[str]) -> list[Diagnostic]: + if not (delete_networks or delete_domains): + return [] + result = self._driver.destroy(networks=tuple(delete_networks), domains=tuple(delete_domains)) + return [ + *result.diagnostics, + *_unconfirmed_destroy_diagnostics( + result, + requested=(*delete_networks, *delete_domains), + ), + ] + + def _identity_diagnostics(self, plan: ProvisioningPlan, snapshot: RuntimeSnapshot) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + if plan.realization_envelope is None: + diagnostics = [ + _envelope_diagnostic( + MISSING_ENVELOPE_CODE, "Provisioning plan is missing realization envelope identity." ) - ) + ] + elif plan.realization_envelope != self._realization_envelope: + diagnostics = [ + _envelope_diagnostic( + MISMATCHED_ENVELOPE_CODE, + "Provisioning plan realization envelope does not match the configured libvirt target.", + ) + ] + elif ( + snapshot.realization_envelope is not None and snapshot.realization_envelope != self._realization_envelope + ) or (_snapshot_has_state(snapshot) and snapshot.realization_envelope is None): + diagnostics = [ + _envelope_diagnostic( + BASELINE_ENVELOPE_MISMATCH_CODE, + "Runtime snapshot is not bound to the configured libvirt realization envelope.", + ) + ] return diagnostics @@ -186,6 +305,26 @@ def _has_error(diagnostics: list[Diagnostic]) -> bool: return any(diag.is_error for diag in diagnostics) +def _snapshot_has_state(snapshot: RuntimeSnapshot) -> bool: + return any( + ( + snapshot.entries, + snapshot.orchestration_results, + snapshot.orchestration_history, + snapshot.evaluation_results, + snapshot.evaluation_history, + snapshot.participant_episode_results, + snapshot.participant_episode_history, + snapshot.participant_behavior_history, + snapshot.shared_state_records, + snapshot.shared_state_history, + snapshot.joint_action_records, + snapshot.time_management_contexts, + snapshot.realization_provenance, + ) + ) + + def _invalid_plan_diagnostic() -> Diagnostic: return Diagnostic( code=INVALID_PLAN_CODE, @@ -225,3 +364,13 @@ def _driver_confirmation_diagnostic(address: str, *, code: str) -> Diagnostic: message=f"Libvirt driver did not confirm {action} for '{address}'.", severity=Severity.ERROR, ) + + +def _envelope_diagnostic(code: str, message: str) -> Diagnostic: + return Diagnostic( + code=code, + domain=_DOMAIN, + address="runtime.libvirt.realization-envelope", + message=message, + severity=Severity.ERROR, + ) diff --git a/implementations/python/packages/aces_backend_libvirt/realization.py b/implementations/python/packages/aces_backend_libvirt/realization.py index 2ecb3f064..e365a52eb 100644 --- a/implementations/python/packages/aces_backend_libvirt/realization.py +++ b/implementations/python/packages/aces_backend_libvirt/realization.py @@ -23,6 +23,7 @@ from dataclasses import dataclass, field from aces_backend_protocols.capabilities import ProvisionerCapabilities +from aces_backend_protocols.naming import provider_resource_name from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.planning import PlannedResource, ProvisioningPlan, RuntimeDomain @@ -146,7 +147,7 @@ def interpret_provisioning_plan( def _network_address_lookup(networks: list[NetworkSpec]) -> dict[str, str]: lookup: dict[str, str] = {} for spec in networks: - for key in (spec.address, spec.name, spec.address.rsplit(".", 1)[-1]): + for key in (spec.address, spec.name): if key: lookup[key] = spec.address return lookup @@ -156,8 +157,8 @@ def _network_spec(resource: PlannedResource, payload: Mapping[str, object]) -> N infrastructure = _infrastructure_spec(payload) properties = infrastructure.get("properties") labels: dict[str, str] = {} - if isinstance(properties, Mapping) and properties.get("internal") is True: - labels["internal"] = "true" + if isinstance(properties, Mapping) and isinstance(properties.get("internal"), bool): + labels["internal"] = "true" if properties["internal"] else "false" cidr = properties.get("cidr") if isinstance(properties, Mapping) else None gateway = properties.get("gateway") if isinstance(properties, Mapping) else None return NetworkSpec( @@ -177,7 +178,7 @@ def _node_address_lookup( lookup: dict[str, str] = {} for resource, payload in node_resources: name = _resource_name(resource, payload) - for key in (resource.address, name, resource.address.rsplit(".", 1)[-1]): + for key in (resource.address, name): if key: lookup[key] = resource.address return lookup @@ -380,7 +381,7 @@ def _network_cidr_lookup(networks: list[NetworkSpec]) -> dict[str, str]: for spec in networks: if not spec.cidr: continue - for key in (spec.address, spec.name, spec.address.rsplit(".", 1)[-1]): + for key in (spec.address, spec.name): if key: lookup[key] = spec.cidr return lookup @@ -390,7 +391,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 resource.address.rsplit(".", 1)[-1] + return provider_resource_name(resource.address, prefix="aces") def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: diff --git a/implementations/python/packages/aces_backend_libvirt/target.py b/implementations/python/packages/aces_backend_libvirt/target.py index 9e659ace2..c7cca003f 100644 --- a/implementations/python/packages/aces_backend_libvirt/target.py +++ b/implementations/python/packages/aces_backend_libvirt/target.py @@ -9,6 +9,7 @@ from .driver import LibvirtDriver from .drivers.libvirt import LibvirtDeploymentDriver +from .envelopes import LibvirtDriverMode from .manifest import LIBVIRT_BACKEND_NAME, create_libvirt_manifest from .participant_runtime import LibvirtParticipantRuntime from .provisioner import LibvirtProvisioner @@ -22,12 +23,18 @@ def create_libvirt_components( ) -> RuntimeTargetComponents: """Build libvirt backend components for a manifest.""" - deployment_driver = driver if driver is not None else LibvirtDeploymentDriver(**_driver_config(config)) if manifest.has_orchestrator or manifest.has_evaluator: raise ValueError("libvirt backend does not support orchestrator or evaluator.") + mode = _selected_driver_mode(config, driver=driver) + _validate_manifest_mode(manifest, mode) + deployment_driver = driver if driver is not None else LibvirtDeploymentDriver(**_driver_config(config)) participant_runtime = LibvirtParticipantRuntime() if manifest.has_participant_runtime else None return RuntimeTargetComponents( - provisioner=LibvirtProvisioner(deployment_driver, provisioner_capabilities=manifest.provisioner), + provisioner=LibvirtProvisioner( + deployment_driver, + provisioner_capabilities=manifest.provisioner, + realization_envelope=manifest.realization_envelope.identity, + ), participant_runtime=participant_runtime, ) @@ -35,8 +42,11 @@ def create_libvirt_components( def create_libvirt_target(**config: Any) -> RuntimeTarget: """Return a fully configured libvirt provisioning target.""" - manifest = create_libvirt_manifest(**config) - components = create_libvirt_components(manifest=manifest, **config) + _validate_config_keys(config) + mode = _selected_driver_mode(config, driver=config.get("driver")) + normalized = {**config, "driver_mode": mode.value} + manifest = create_libvirt_manifest(**normalized) + components = create_libvirt_components(manifest=manifest, **normalized) return RuntimeTarget( name=LIBVIRT_BACKEND_NAME, manifest=manifest, @@ -66,3 +76,52 @@ def _driver_config(config: dict[str, Any]) -> dict[str, Any]: if "uri" in config and "connection_uri" not in driver_config: driver_config["connection_uri"] = config["uri"] return driver_config + + +_CONFIG_KEYS = { + "connection", + "connection_uri", + "connector", + "driver", + "driver_mode", + "name_prefix", + "participant_runtime", + "seed_builder", + "uri", + "workspace", +} + + +def _validate_config_keys(config: dict[str, Any]) -> None: + unknown = sorted(set(config) - _CONFIG_KEYS) + if unknown: + raise ValueError("unknown libvirt target configuration: " + ", ".join(unknown)) + + +def _selected_driver_mode(config: dict[str, Any], *, driver: object | None) -> LibvirtDriverMode: + raw_mode = config.get("driver_mode") + declared = getattr(driver, "driver_mode", None) + if driver is not None and raw_mode is None and declared is None: + raise ValueError("driver_mode is required when injecting a libvirt driver that declares no mode") + mode = LibvirtDriverMode(raw_mode or declared or LibvirtDriverMode.GENERIC.value) + if declared is not None and declared != mode.value: + raise ValueError(f"injected driver mode '{declared}' does not match driver_mode '{mode.value}'") + return mode + + +def _validate_manifest_mode(manifest: BackendManifest, mode: LibvirtDriverMode) -> None: + envelope = manifest.realization_envelope + if envelope is None or envelope.configuration.mode != mode.value: + raise ValueError("libvirt manifest realization envelope does not match driver_mode") + configuration = envelope.configuration + expected = { + "supported_node_types": frozenset(configuration.supported_node_types), + "supported_os_families": frozenset(configuration.supported_os_families), + "supported_content_types": frozenset(configuration.supported_content_types), + "supported_account_features": frozenset(configuration.supported_account_features), + "supports_accounts": bool(configuration.supported_account_features), + "supports_acls": configuration.supports_acls, + } + actual = {field: getattr(manifest.provisioner, field) for field in expected} + if actual != expected: + raise ValueError("libvirt manifest capabilities do not match realization envelope") diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_appliance.py b/implementations/python/packages/aces_backend_libvirt/techvault_appliance.py index 537edcb7d..bc47a2860 100644 --- a/implementations/python/packages/aces_backend_libvirt/techvault_appliance.py +++ b/implementations/python/packages/aces_backend_libvirt/techvault_appliance.py @@ -61,14 +61,12 @@ 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" - www_dir = root / "www" - for directory in (bin_dir, etc_dir, www_dir, root / "proc", root / "sys", root / "dev", root / "tmp", root / "run"): + 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") - for applet in ("sh", "mount", "mdev", "ip", "ifconfig", "httpd", "nc", "sleep", "cat", "hostname", "printf"): + for applet in ("sh", "mount", "mdev", "ip", "ifconfig", "sleep", "cat", "hostname", "printf"): (bin_dir / applet).symlink_to("busybox") (etc_dir / "domain.json").write_text(json.dumps(domain, indent=2, sort_keys=True) + "\n", encoding="utf-8") - (www_dir / "index.html").write_text(_html_status(domain), encoding="utf-8") (root / "init").write_text(_init_script(domain), encoding="utf-8") os.chmod(root / "init", 0o700) os.chmod(bin_dir / "busybox", 0o700) @@ -101,26 +99,10 @@ def _init_script(domain: Mapping[str, object]) -> str: ] ) lines.extend([" esac", "done"]) - for service in _as_sequence(domain.get("services")): - if not isinstance(service, Mapping) or str(service.get("protocol", "tcp")).lower() != "tcp": - continue - port = _int(service.get("port")) - if port > 0: - lines.append(f"httpd -p 0.0.0.0:{port} -h /www") lines.extend(["while true; do sleep 3600; done", ""]) return "\n".join(lines) -def _html_status(domain: Mapping[str, object]) -> str: - return ( - "

ACES TechVault appliance

" - f"

node={domain.get('name')}

" - f"

role={domain.get('role')}

" - f"
{json.dumps(domain, sort_keys=True)}
" - "\n" - ) - - def _cpio_newc(root: Path) -> bytes: proc = subprocess.run( ["cpio", "-o", "-H", "newc", "--quiet"], @@ -142,9 +124,5 @@ def _as_sequence(value: object) -> Sequence[object]: return value if isinstance(value, list | tuple) else () -def _int(value: object) -> int: - return value if isinstance(value, int) else 0 - - def _shell_quote(value: str) -> str: return "'" + value.replace("'", "'\"'\"'") + "'" diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_concerns.py b/implementations/python/packages/aces_backend_libvirt/techvault_concerns.py new file mode 100644 index 000000000..62c6f0cba --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/techvault_concerns.py @@ -0,0 +1,407 @@ +"""Fail-closed concern admission for the bounded TechVault appliance mode.""" + +from __future__ import annotations + +import ipaddress +import re + +from aces_contracts.diagnostics import Diagnostic, Severity +from aces_contracts.realization_envelope import ( + BackendRealizationEnvelopeModel, + ObservationStrength, + RealizationConcern, +) + +from .driver import DomainSpec, DriverResult, NetworkSpec, RealizationObservation +from .techvault_matrix import runtime_name + +_DOMAIN = "runtime" +_CODE_ACL_UNSUPPORTED = "libvirt-backend.techvault.acl-unsupported" +_CODE_GUEST_PLACEMENT_UNSUPPORTED = "libvirt-backend.techvault.guest-placement-unsupported" +_CODE_IMAGE_UNSUPPORTED = "libvirt-backend.techvault.image-unsupported" +_CODE_METADATA_UNSUPPORTED = "libvirt-backend.techvault.metadata-unsupported" +_CODE_NETWORK_EXACTNESS = "libvirt-backend.techvault.network-exactness-required" +_CODE_NAME_UNSUPPORTED = "libvirt-backend.techvault.name-unsupported" +_CODE_OBSERVATION_MISMATCH = "libvirt-backend.techvault.observation-mismatch" +_CODE_OBSERVATION_MISSING = "libvirt-backend.techvault.observation-missing" +_CODE_RESOURCE_OUT_OF_ENVELOPE = "libvirt-backend.techvault.resource-out-of-envelope" +_CODE_SERVICE_UNSUPPORTED = "libvirt-backend.techvault.service-unsupported" +_CODE_TRANSACTION_UNSUPPORTED = "libvirt-backend.techvault.transaction-unsupported" +_CODE_UPDATE_UNSUPPORTED = "libvirt-backend.techvault.update-unsupported" + +_SAFE_TOKEN_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$") + + +def techvault_observation_diagnostics( + *, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + result: DriverResult, +) -> list[Diagnostic]: + """Require complete daemon readback; driver handles alone prove nothing.""" + + expected = _expected_observations(networks=networks, domains=domains) + observed: dict[tuple[str, str, RealizationConcern], list[RealizationObservation]] = {} + for item in result.observations: + observed.setdefault(_observation_key(item), []).append(item) + missing_by_address: set[str] = set() + mismatched_by_address: set[str] = set() + for key, expected_value in expected.items(): + candidates = observed.get(key, []) + if not candidates or any(item.source is not ObservationStrength.DAEMON_OBSERVED for item in candidates): + missing_by_address.add(key[0]) + elif ( + len(candidates) != 1 + or type(candidates[0].value) is not type(expected_value) + or candidates[0].value != expected_value + ): + mismatched_by_address.add(key[0]) + diagnostics = [ + _diagnostic( + _CODE_OBSERVATION_MISSING, + address, + "TechVault driver did not return complete daemon observations for the requested concern inventory.", + ) + for address in sorted(missing_by_address) + ] + diagnostics.extend( + _diagnostic( + _CODE_OBSERVATION_MISMATCH, + address, + "TechVault daemon observations do not match the exact requested concern values.", + ) + for address in sorted(mismatched_by_address - missing_by_address) + ) + return diagnostics + + +def techvault_spec_diagnostics( + *, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + envelope: BackendRealizationEnvelopeModel, + name_prefix: str, +) -> list[Diagnostic]: + """Apply the same concern gate to callers that invoke the driver directly.""" + + diagnostics: list[Diagnostic] = [] + diagnostics.extend(_name_diagnostics(networks, domains, name_prefix)) + network_addresses = {spec.address for spec in networks} + for spec in networks: + diagnostics.extend(_network_spec_diagnostics(spec)) + for spec in domains: + diagnostics.extend(_domain_spec_diagnostics(spec, envelope, network_addresses)) + diagnostics.extend(_network_capacity_diagnostics(networks, domains)) + return diagnostics + + +def guest_certified_spec_diagnostics( + *, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + envelope: BackendRealizationEnvelopeModel, + name_prefix: str, +) -> list[Diagnostic]: + """Admit the bounded placements the guest-certified appliance realizes. + + Unlike the daemon-only appliance, this mode boots accounts, files, and a + bounded service into the guest and reads them back, so those placements are + accepted. Requested images, ACLs, packages, runcmd, hostname overrides, and + out-of-envelope resources remain rejected before any native mutation. + """ + + diagnostics: list[Diagnostic] = [] + diagnostics.extend(_name_diagnostics(networks, domains, name_prefix)) + network_addresses = {spec.address for spec in networks} + for spec in networks: + diagnostics.extend(_network_spec_diagnostics(spec)) + for spec in domains: + diagnostics.extend(_guest_domain_spec_diagnostics(spec, envelope, network_addresses)) + diagnostics.extend(_network_capacity_diagnostics(networks, domains)) + return diagnostics + + +def _guest_domain_spec_diagnostics( + spec: DomainSpec, + envelope: BackendRealizationEnvelopeModel, + network_addresses: set[str], +) -> list[Diagnostic]: + return [ + *_domain_resource_diagnostics(spec, envelope), + *_domain_image_diagnostics(spec), + *_domain_metadata_diagnostics(spec), + *_domain_acl_diagnostics(spec), + *_domain_network_diagnostics(spec, network_addresses), + *_guest_placement_bounds_diagnostics(spec), + *_guest_service_bounds_diagnostics(spec), + ] + + +def _guest_placement_bounds_diagnostics(spec: DomainSpec) -> list[Diagnostic]: + cloud_init = spec.cloud_init + if cloud_init is None: + return [] + unsupported = ( + bool(cloud_init.packages) or bool(cloud_init.runcmd) or cloud_init.hostname not in {None, "", spec.name} + ) + unsafe_content = any(not _safe_guest_path(item.path) for item in cloud_init.write_files) + unsafe_account = any(not _SAFE_TOKEN_RE.match(user.name) for user in cloud_init.users) + if unsupported or unsafe_content or unsafe_account: + return [ + _diagnostic( + _CODE_GUEST_PLACEMENT_UNSUPPORTED, + spec.address, + "Guest-certified appliance realizes bounded accounts and files only; " + "packages, runcmd, hostname overrides, unsafe paths, and unsafe account names are rejected.", + ) + ] + return [] + + +def _guest_service_bounds_diagnostics(spec: DomainSpec) -> list[Diagnostic]: + invalid = any(not _SAFE_TOKEN_RE.match(service.name) or not 1 <= service.port <= 65535 for service in spec.services) + if not invalid: + return [] + return [ + _diagnostic( + _CODE_SERVICE_UNSUPPORTED, + spec.address, + "Guest-certified services require a libvirt-safe name and a valid port.", + ) + ] + + +def _safe_guest_path(path: str) -> bool: + return path.startswith("/") and ".." not in path.split("/") + + +def _domain_spec_diagnostics( + spec: DomainSpec, + envelope: BackendRealizationEnvelopeModel, + network_addresses: set[str], +) -> list[Diagnostic]: + return [ + *_domain_resource_diagnostics(spec, envelope), + *_domain_image_diagnostics(spec), + *_domain_service_diagnostics(spec), + *_domain_guest_placement_diagnostics(spec), + *_domain_metadata_diagnostics(spec), + *_domain_acl_diagnostics(spec), + *_domain_network_diagnostics(spec, network_addresses), + ] + + +def _domain_resource_diagnostics( + spec: DomainSpec, + envelope: BackendRealizationEnvelopeModel, +) -> list[Diagnostic]: + configuration = envelope.configuration + memory_valid = _within(spec.memory_mib, configuration.memory_mib.minimum, configuration.memory_mib.maximum) + vcpus_valid = _within(spec.vcpus, configuration.vcpus.minimum, configuration.vcpus.maximum) + if memory_valid and vcpus_valid: + return [] + return [ + _diagnostic( + _CODE_RESOURCE_OUT_OF_ENVELOPE, + spec.address, + "TechVault appliance resource values must be inside the governed envelope and are never clamped.", + ) + ] + + +def _domain_image_diagnostics(spec: DomainSpec) -> list[Diagnostic]: + if spec.image_ref is None: + return [] + return [ + _diagnostic( + _CODE_IMAGE_UNSUPPORTED, + spec.address, + "TechVault appliance mode cannot honor a requested image and refuses image substitution.", + ) + ] + + +def _domain_service_diagnostics(spec: DomainSpec) -> list[Diagnostic]: + if not spec.services: + return [] + return [ + _diagnostic( + _CODE_SERVICE_UNSUPPORTED, + spec.address, + "TechVault appliance mode does not realize declared guest services.", + ) + ] + + +def _domain_guest_placement_diagnostics(spec: DomainSpec) -> list[Diagnostic]: + cloud_init = spec.cloud_init + has_guest_placement = cloud_init is not None and any( + ( + cloud_init.hostname not in {None, spec.name}, + bool(cloud_init.users), + bool(cloud_init.write_files), + bool(cloud_init.packages), + bool(cloud_init.runcmd), + ) + ) + if not has_guest_placement: + return [] + return [ + _diagnostic( + _CODE_GUEST_PLACEMENT_UNSUPPORTED, + spec.address, + "TechVault appliance mode does not realize cloud-init guest placements.", + ) + ] + + +def _domain_metadata_diagnostics(spec: DomainSpec) -> list[Diagnostic]: + if not spec.labels: + return [] + return [ + _diagnostic( + _CODE_METADATA_UNSUPPORTED, + spec.address, + "TechVault appliance mode does not consume unbound domain metadata.", + ) + ] + + +def _domain_acl_diagnostics(spec: DomainSpec) -> list[Diagnostic]: + if not spec.network_acls: + return [] + return [ + _diagnostic( + _CODE_ACL_UNSUPPORTED, + spec.address, + "TechVault appliance mode does not realize declared network ACLs.", + ) + ] + + +def _domain_network_diagnostics(spec: DomainSpec, network_addresses: set[str]) -> list[Diagnostic]: + if all(address in network_addresses for address in spec.networks): + return [] + return [_network_exactness_diagnostic(spec.address)] + + +def _name_diagnostics( + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + name_prefix: str, +) -> list[Diagnostic]: + return _native_name_diagnostics([(spec.address, spec.name) for spec in (*networks, *domains)], name_prefix) + + +def _native_name_diagnostics(names: list[tuple[str, str]], name_prefix: str) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + native_names: set[str] = set() + for address, display_name in names: + selected_name = runtime_name(name_prefix, address, display_name) + if selected_name in native_names: + diagnostics.append( + _diagnostic( + _CODE_NAME_UNSUPPORTED, + address, + "TechVault provider-name projections must be unique for canonical resource addresses.", + ) + ) + native_names.add(selected_name) + return diagnostics + + +def _network_capacity_diagnostics( + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], +) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + for spec in networks: + if not spec.cidr or not spec.gateway: + continue + try: + network = ipaddress.ip_network(spec.cidr, strict=True) + gateway = ipaddress.ip_address(spec.gateway) + except ValueError: + continue + if not isinstance(network, ipaddress.IPv4Network): + continue + attachment_count = sum(spec.address in domain.networks for domain in domains) + candidates = [network.network_address + offset for offset in range(10, 10 + attachment_count)] + if any( + candidate not in network or candidate == network.broadcast_address or candidate == gateway + for candidate in candidates + ): + diagnostics.append(_network_exactness_diagnostic(spec.address)) + return diagnostics + + +def _network_spec_diagnostics(spec: NetworkSpec) -> list[Diagnostic]: + labels_valid = set(spec.labels) == {"internal"} and spec.labels.get("internal") in {"true", "false"} + valid = labels_valid and _valid_ipv4_network(spec.cidr, spec.gateway) + return [] if valid else [_network_exactness_diagnostic(spec.address)] + + +def _expected_observations( + *, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], +) -> dict[tuple[str, str, RealizationConcern], object]: + expected: dict[tuple[str, str, RealizationConcern], object] = {} + for spec in networks: + expected[(spec.address, "exists", RealizationConcern.TOPOLOGY)] = True + expected[(spec.address, "cidr", RealizationConcern.NETWORK)] = spec.cidr + expected[(spec.address, "gateway", RealizationConcern.NETWORK)] = spec.gateway + expected[(spec.address, "internal", RealizationConcern.NETWORK)] = spec.labels.get("internal") == "true" + expected[(spec.address, "forward-mode", RealizationConcern.NETWORK)] = ( + "none" if spec.labels.get("internal") == "true" else "nat" + ) + for spec in domains: + expected[(spec.address, "exists", RealizationConcern.TOPOLOGY)] = True + expected[(spec.address, "architecture", RealizationConcern.ARCHITECTURE)] = "x86_64" + expected[(spec.address, "image-policy", RealizationConcern.IMAGE)] = "generated-initramfs-appliance" + expected[(spec.address, "memory-mib", RealizationConcern.RESOURCE_ALLOCATION)] = spec.memory_mib + expected[(spec.address, "vcpus", RealizationConcern.RESOURCE_ALLOCATION)] = spec.vcpus + expected[(spec.address, "network-attachments", RealizationConcern.NETWORK)] = tuple(spec.networks) + return expected + + +def _observation_key(observation: RealizationObservation) -> tuple[str, str, RealizationConcern]: + return observation.address, observation.field_path, observation.concern + + +def _valid_ipv4_network(cidr: object, gateway: object) -> bool: + if not isinstance(cidr, str) or not isinstance(gateway, str): + return False + try: + network = ipaddress.ip_network(cidr, strict=True) + parsed_gateway = ipaddress.ip_address(gateway) + except ValueError: + return False + return ( + isinstance(network, ipaddress.IPv4Network) + and parsed_gateway in network + and parsed_gateway not in {network.network_address, network.broadcast_address} + ) + + +def _within(value: int, minimum: int, maximum: int | None) -> bool: + return value >= minimum and (maximum is None or value <= maximum) + + +def _network_exactness_diagnostic(address: str) -> Diagnostic: + return _diagnostic( + _CODE_NETWORK_EXACTNESS, + address, + "TechVault appliance networks require explicit valid IPv4 CIDR, gateway, and internal values.", + ) + + +def _diagnostic(code: str, address: str, message: str) -> Diagnostic: + return Diagnostic(code=code, domain=_DOMAIN, address=address, message=message, severity=Severity.ERROR) + + +__all__ = [ + "guest_certified_spec_diagnostics", + "techvault_observation_diagnostics", + "techvault_spec_diagnostics", +] diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_lifecycle.py b/implementations/python/packages/aces_backend_libvirt/techvault_lifecycle.py new file mode 100644 index 000000000..a56399ca2 --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/techvault_lifecycle.py @@ -0,0 +1,151 @@ +"""Ownership-safe native resource lookup and removal for TechVault.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from .drivers.libvirt import _aces_uuid, _error_code, _existing_uuid +from .techvault_matrix import runtime_name + + +class NativeOwnershipConflict(Exception): + """A native name is not owned by the requested ACES address.""" + + +@dataclass(frozen=True) +class NativeResolution: + native: object | None + name: str | None + + +def resolve_native( + connection: object, + lookup_method: str, + list_method: str, + address: str, + *, + known_name: str | None, + name_prefix: str, +) -> NativeResolution | None: + lookup = getattr(connection, lookup_method, None) + if not callable(lookup): + return None + if known_name is None: + return _resolve_by_uuid(connection, list_method, address, name_prefix) + return _resolve_by_name(connection, lookup, list_method, address, known_name) + + +def _resolve_by_uuid( + connection: object, + list_method: str, + address: str, + name_prefix: str, +) -> NativeResolution | None: + 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)] + if not owned: + fallback_name = runtime_name(name_prefix, address) + if any(_native_name(item) == fallback_name for item in native_items): + raise NativeOwnershipConflict(address) + resolved = NativeResolution(native=None, name=None) + elif len(owned) == 1: + name = _native_name(owned[0]) + if name: + resolved = NativeResolution(native=owned[0], name=name) + return resolved + + +def _resolve_by_name( + connection: object, + lookup: Callable[[str], object], + list_method: str, + address: str, + name: str, +) -> NativeResolution | None: + resolved: NativeResolution | None = None + try: + native = lookup(name) + except KeyError: + resolved = _resolve_verified_absence(connection, list_method, address) + except Exception as exc: + if _error_code(exc) in {42, 43}: + resolved = _resolve_verified_absence(connection, list_method, address) + else: + resolved = NativeResolution(native=native, name=name) + return resolved + + +def _resolve_verified_absence( + connection: object, + list_method: str, + 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): + return None + return NativeResolution(native=None, name=None) + + +def verify_native_removed( + connection: object, + list_method: str, + address: str, + name: str | None, +) -> 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 + ) + + +def deactivate_and_undefine(native: object) -> bool: + return _invoke_native_action(native, "destroy", {42, 43, 55}) and _invoke_native_action( + native, + "undefine", + {42, 43}, + ) + + +def _invoke_native_action(native: object, method_name: str, tolerated_codes: set[int]) -> bool: + method = getattr(native, method_name, None) + if not callable(method): + return False + try: + method() + except Exception as exc: + return _error_code(exc) in tolerated_codes + return True + + +def _list_native(connection: object, method_name: str) -> tuple[object, ...] | None: + method = getattr(connection, method_name, None) + if not callable(method): + return None + try: + native = method() + except Exception: + return None + return tuple(native) if isinstance(native, list | tuple) else None + + +def _native_name(native: object) -> str: + method = getattr(native, "name", None) + if not callable(method): + return "" + try: + value = method() + except Exception: + return "" + return value if isinstance(value, str) else "" + + +__all__ = [ + "NativeOwnershipConflict", + "NativeResolution", + "deactivate_and_undefine", + "resolve_native", + "verify_native_removed", +] diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_matrix.py b/implementations/python/packages/aces_backend_libvirt/techvault_matrix.py new file mode 100644 index 000000000..3a4f0ab19 --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/techvault_matrix.py @@ -0,0 +1,245 @@ +"""Pure TechVault appliance matrix and structured libvirt XML rendering.""" + +from __future__ import annotations + +import hashlib +import ipaddress +import re +import xml.etree.ElementTree as ET +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import cast + +from aces_backend_protocols.naming import provider_resource_name + +from .driver import DomainSpec, NetworkSpec +from .drivers.libvirt import _aces_uuid + +_SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.-]+") +_SUBSTRATE = "libvirt-qemu-initramfs" + + +def native_matrix( + *, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + name_prefix: str, + include_placements: bool = False, +) -> dict[str, object]: + runtime_networks = [runtime_network(spec, name_prefix) for spec in networks] + runtime_network_by_address = {str(item["address"]): item for item in runtime_networks} + allocations = allocate_interfaces(domains, runtime_network_by_address, name_prefix) + runtime_domains = [ + runtime_domain( + spec, + name_prefix=name_prefix, + interfaces=allocations.get(spec.address, ()), + include_placements=include_placements, + ) + for spec in domains + ] + return { + "substrate": _SUBSTRATE, + "networks": runtime_networks, + "domains": runtime_domains, + } + + +def runtime_network(spec: NetworkSpec, name_prefix: str) -> dict[str, object]: + if spec.cidr is None or spec.gateway is None: + raise ValueError("TechVault network values must be explicit") + network = ipaddress.ip_network(spec.cidr, strict=True) + gateway = ipaddress.ip_address(spec.gateway) + if not isinstance(network, ipaddress.IPv4Network) or not isinstance(gateway, ipaddress.IPv4Address): + raise ValueError("TechVault appliance supports explicit IPv4 networks only") + return { + "address": spec.address, + "name": spec.name, + "runtime_name": runtime_name(name_prefix, spec.address, spec.name), + "cidr": str(network), + "gateway": str(gateway), + "netmask": str(network.netmask), + "internal": spec.labels.get("internal") == "true", + "hosts": [], + } + + +def allocate_interfaces( + domains: tuple[DomainSpec, ...], + networks: Mapping[str, dict[str, object]], + name_prefix: str, +) -> dict[str, tuple[dict[str, object], ...]]: + allocations: dict[str, list[dict[str, object]]] = {domain.address: [] for domain in domains} + next_host: dict[str, int] = dict.fromkeys(networks, 10) + for domain in domains: + for network_address in domain.networks: + network = networks[network_address] + parsed = ipaddress.ip_network(str(network["cidr"]), strict=True) + offset = next_host[network_address] + next_host[network_address] = offset + 1 + ip = str(parsed.network_address + offset) + mac = mac_address(domain.address, network_address) + interface = { + "network_address": network_address, + "network_name": network.get("name"), + "runtime_network": network.get("runtime_name"), + "ip": ip, + "cidr_prefix": parsed.prefixlen, + "gateway": network.get("gateway"), + "mac": mac, + } + allocations[domain.address].append(interface) + cast(list[dict[str, object]], network["hosts"]).append( + {"name": runtime_name(name_prefix, domain.address, domain.name), "mac": mac, "ip": ip} + ) + return {address: tuple(items) for address, items in allocations.items()} + + +def runtime_domain( + spec: DomainSpec, + *, + name_prefix: str, + interfaces: tuple[dict[str, object], ...], + include_placements: bool = False, +) -> dict[str, object]: + domain: dict[str, object] = { + "address": spec.address, + "name": spec.name, + "runtime_name": runtime_name(name_prefix, spec.address, spec.name), + "memory_mib": spec.memory_mib, + "vcpus": spec.vcpus, + "interfaces": list(interfaces), + } + if include_placements: + domain.update(domain_placements(spec)) + return domain + + +def domain_placements(spec: DomainSpec) -> dict[str, object]: + """Project a domain's realizable content/account/service placements. + + Guest-certified realization boots these placements into the appliance and + reads them back from inside the guest; daemon-only modes ignore them. + """ + + cloud_init = spec.cloud_init + accounts = [ + { + "name": user.name, + "groups": sorted(user.groups), + "shell": user.shell, + "home": user.home, + "disabled": bool(user.lock_passwd), + } + for user in (cloud_init.users if cloud_init is not None else ()) + ] + content = [ + {"path": item.path, "content": item.content, "mode": item.permissions} + for item in (cloud_init.write_files if cloud_init is not None else ()) + ] + services = [{"name": service.name, "port": service.port} for service in spec.services] + return {"accounts": accounts, "content": content, "services": services} + + +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", ""))) + if not network.get("internal"): + ET.SubElement(root, "forward", {"mode": "nat"}) + ip_node = ET.SubElement( + root, + "ip", + {"address": str(network.get("gateway", "")), "netmask": str(network.get("netmask", ""))}, + ) + dhcp = ET.SubElement(ip_node, "dhcp") + for host in as_sequence(network.get("hosts")): + if isinstance(host, Mapping): + ET.SubElement( + dhcp, + "host", + {"mac": str(host.get("mac", "")), "name": str(host.get("name", "")), "ip": str(host.get("ip", ""))}, + ) + return ET.tostring(root, encoding="unicode") + + +def domain_xml( + domain: Mapping[str, object], + *, + kernel: Path, + initrd: Path, + appliance: str = "techvault", + challenge: str | None = None, + fact_channel_path: Path | None = None, +) -> 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, "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") + ET.SubElement(os_node, "type", {"arch": "x86_64"}).text = "hvm" + ET.SubElement(os_node, "kernel").text = str(kernel) + ET.SubElement(os_node, "initrd").text = str(initrd) + ET.SubElement(os_node, "cmdline").text = _domain_cmdline(appliance, challenge) + features = ET.SubElement(root, "features") + ET.SubElement(features, "acpi") + devices = ET.SubElement(root, "devices") + ET.SubElement(devices, "emulator").text = "/usr/bin/qemu-system-x86_64" + serial = ET.SubElement(devices, "serial", {"type": "pty"}) + ET.SubElement(serial, "target", {"port": "0"}) + console = ET.SubElement(devices, "console", {"type": "pty"}) + ET.SubElement(console, "target", {"type": "serial", "port": "0"}) + if fact_channel_path is not None: + fact_serial = ET.SubElement(devices, "serial", {"type": "file"}) + ET.SubElement(fact_serial, "source", {"path": str(fact_channel_path)}) + ET.SubElement(fact_serial, "target", {"port": "1"}) + for interface_spec in as_sequence(domain.get("interfaces")): + if not isinstance(interface_spec, Mapping): + continue + interface = ET.SubElement(devices, "interface", {"type": "network"}) + ET.SubElement(interface, "mac", {"address": str(interface_spec.get("mac", ""))}) + ET.SubElement(interface, "source", {"network": str(interface_spec.get("runtime_network", ""))}) + ET.SubElement(interface, "model", {"type": "virtio"}) + return ET.tostring(root, encoding="unicode") + + +def runtime_name(prefix: str, address: str, preferred: str | None = None) -> str: + del preferred + return provider_resource_name(address, prefix=prefix) + + +def safe_name(candidate: str, *, fallback: str, prefix: str) -> str: + raw = candidate.strip() or fallback.strip() or "resource" + normalized = _SAFE_NAME_RE.sub("-", raw).strip("-._") + if not normalized: + normalized = _SAFE_NAME_RE.sub("-", fallback).strip("-._") or "resource" + prefixed = f"{prefix}-{normalized}" if prefix else normalized + return prefixed[:63].strip("-._") or "resource" + + +def mac_address(domain_address: str, network_address: str) -> str: + digest = hashlib.sha256(f"{domain_address}|{network_address}".encode()).digest() + return "52:54:00:" + ":".join(f"{byte:02x}" for byte in digest[:3]) + + +def as_sequence(value: object) -> Sequence[object]: + return value if isinstance(value, list | tuple) else () + + +def _domain_cmdline(appliance: str, challenge: str | None) -> str: + parts = ["console=ttyS0", "panic=-1", f"aces.appliance={appliance}"] + if challenge: + parts.append(f"aces.challenge={challenge}") + return " ".join(parts) + + +__all__ = [ + "as_sequence", + "domain_placements", + "domain_xml", + "native_matrix", + "network_xml", + "runtime_name", + "safe_name", +] diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_native.py b/implementations/python/packages/aces_backend_libvirt/techvault_native.py index 6b2485ba3..95dcac40b 100644 --- a/implementations/python/packages/aces_backend_libvirt/techvault_native.py +++ b/implementations/python/packages/aces_backend_libvirt/techvault_native.py @@ -8,28 +8,85 @@ from __future__ import annotations -import hashlib -import ipaddress -import json -import os -import re -import xml.etree.ElementTree as ET -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Mapping, Sequence from contextlib import suppress from dataclasses import dataclass, field from pathlib import Path -from typing import Protocol, cast +from typing import TYPE_CHECKING, ClassVar, cast +from urllib.parse import urlsplit + +from aces_contracts.diagnostics import Diagnostic + +from ._techvault_native_helpers import default_connector as _default_connector +from ._techvault_native_helpers import default_kernel_path as _default_kernel_path +from ._techvault_native_ops import ( + _CODE_OPERATION_FAILED, + _CODE_OWNERSHIP_CONFLICT, + _CODE_READBACK_FAILED, + _CODE_RESIDUAL_STATE, + _CODE_UNAVAILABLE, + _DEFAULT_CONNECTION_URI, + _artifact_token, + _call, + _diagnostic, + _ensure_name_available, + _NativeResource, +) -from aces_contracts.diagnostics import Diagnostic, Severity +if TYPE_CHECKING: + from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel -from .driver import DomainHandle, DomainSpec, DriverResult, NetworkHandle, NetworkSpec, ServiceSpec -from .drivers.libvirt import Connector +from .driver import ( + DomainHandle, + DomainSpec, + DriverResult, + NetworkHandle, + NetworkSpec, + RealizationObservation, +) +from .drivers.libvirt import Connector, _aces_uuid, _existing_uuid +from .envelopes import load_libvirt_realization_envelope from .techvault_appliance import ( BusyboxInitramfsBuilder, InitramfsBuilder, copy_kernel_for_libvirt, make_libvirt_readable, ) +from .techvault_concerns import techvault_observation_diagnostics, techvault_spec_diagnostics +from .techvault_lifecycle import ( + NativeOwnershipConflict as _OwnershipConflict, +) +from .techvault_lifecycle import ( + deactivate_and_undefine as _deactivate_and_undefine, +) +from .techvault_lifecycle import ( + resolve_native as _resolve_native, +) +from .techvault_lifecycle import ( + verify_native_removed as _verify_native_removed, +) +from .techvault_matrix import ( + as_sequence as _as_sequence, +) +from .techvault_matrix import ( + domain_xml as _domain_xml, +) +from .techvault_matrix import ( + native_matrix as _native_matrix, +) +from .techvault_matrix import ( + network_xml as _network_xml, +) +from .techvault_matrix import ( + safe_name as _safe_name, +) +from .techvault_observation import ( + canonical_digest, + domain_observations, + file_digest, + network_observations, + snapshot_from_observations, +) from .techvault_probe import ( NativeLibvirtProbe, ProbeResult, @@ -38,12 +95,6 @@ native_soc_readback, ) -_DOMAIN = "runtime" -_CODE_OPERATION_FAILED = "libvirt-backend.techvault-native.operation-failed" -_CODE_UNAVAILABLE = "libvirt-backend.techvault-native.unavailable" -_DEFAULT_CONNECTION_URI = "qemu:///system" -_SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.-]+") -_SUBSTRATE = "libvirt-qemu-initramfs" __all__ = [ "BusyboxInitramfsBuilder", "NativeLibvirtProbe", @@ -55,18 +106,12 @@ ] -class _NativeResource(Protocol): - def create(self) -> None: ... - - def destroy(self) -> None: ... - - def undefine(self) -> None: ... - - @dataclass class TechVaultNativeLibvirtDriver: """Realize TechVault domains directly as libvirt/QEMU appliances.""" + driver_mode: ClassVar[str] = "techvault-appliance" + state_dir: Path connection: object | None = None connection_uri: str = _DEFAULT_CONNECTION_URI @@ -74,24 +119,37 @@ class TechVaultNativeLibvirtDriver: name_prefix: str = "aces-techvault" kernel_path: Path | None = None initramfs_builder: InitramfsBuilder = field(default_factory=BusyboxInitramfsBuilder) - appliance_memory_mib: int = 128 define_only: bool = False clean_existing: bool = False last_snapshot: dict[str, object] = field(default_factory=dict) - last_matrix: dict[str, object] = field(default_factory=dict) def __post_init__(self) -> None: - if not self.connection_uri or not self.connection_uri.strip(): - raise ValueError("TechVaultNativeLibvirtDriver connection_uri must be non-empty.") - if not self.name_prefix or not self.name_prefix.strip(): - raise ValueError("TechVaultNativeLibvirtDriver name_prefix must be non-empty.") + self._validate_connection_uri() + self._validate_appliance_flags() self.state_dir = Path(self.state_dir) self.kernel_path = Path(self.kernel_path) if self.kernel_path is not None else _default_kernel_path() - self.name_prefix = _safe_name(self.name_prefix, fallback="aces-techvault", prefix="") - self.appliance_memory_mib = max(64, int(self.appliance_memory_mib)) self.connector = self.connector or _default_connector self._names: dict[str, str] = {} self._realized: set[str] = set() + self._artifacts: dict[str, tuple[Path, ...]] = {} + + def _validate_connection_uri(self) -> None: + if not self.connection_uri or not self.connection_uri.strip(): + raise ValueError("TechVaultNativeLibvirtDriver connection_uri must be non-empty.") + parsed_uri = urlsplit(self.connection_uri) + if parsed_uri.username is not None or parsed_uri.password is not None: + raise ValueError("TechVaultNativeLibvirtDriver connection URI must not carry credentials.") + + def _validate_appliance_flags(self) -> None: + if not self.name_prefix or not self.name_prefix.strip(): + raise ValueError("TechVaultNativeLibvirtDriver name_prefix must be non-empty.") + if self.define_only: + 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="") + if safe_prefix != self.name_prefix: + raise ValueError("TechVaultNativeLibvirtDriver name_prefix must already be libvirt-safe.") def realize( self, @@ -99,82 +157,267 @@ def realize( networks: tuple[NetworkSpec, ...], domains: tuple[DomainSpec, ...], ) -> DriverResult: + envelope = load_libvirt_realization_envelope(self.driver_mode) + spec_diagnostics = self._admission_diagnostics(networks, domains, envelope) + if spec_diagnostics: + return DriverResult(diagnostics=tuple(spec_diagnostics)) + matrix = self._build_matrix(networks, domains) self.state_dir.mkdir(parents=True, exist_ok=True) - matrix = _native_matrix(networks=networks, domains=domains, name_prefix=self.name_prefix) - self.last_matrix = matrix try: connection = self._conn() except Exception: return DriverResult(diagnostics=(_diagnostic(_CODE_UNAVAILABLE, "runtime.libvirt.connection"),)) - if self.clean_existing: - _destroy_existing_with_prefix(connection, self.name_prefix) + return self._realize_matrix( + connection, + matrix, + networks=networks, + domains=domains, + envelope_digest=envelope.digest, + configuration_digest=envelope.configuration.configuration_digest, + ) - network_handles, network_diagnostics = self._define_networks(connection, matrix) - domain_handles, domain_diagnostics = self._define_domains(connection, matrix) - diagnostics = network_diagnostics + domain_diagnostics + def _realize_matrix( + self, + connection: object, + matrix: Mapping[str, object], + *, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + envelope_digest: str, + configuration_digest: str, + ) -> DriverResult: + network_handles, network_diagnostics, network_observations = self._define_networks(connection, matrix) + if network_diagnostics: + network_diagnostics.extend(self._rollback(connection, network_handles, ())) + return DriverResult(diagnostics=tuple(network_diagnostics)) + domain_handles, domain_diagnostics, domain_observations = self._define_domains(connection, matrix) + if domain_diagnostics: + domain_diagnostics.extend(self._rollback(connection, network_handles, domain_handles)) + return DriverResult(diagnostics=tuple(domain_diagnostics)) + observations = (*network_observations, *domain_observations) + return self._verify_and_finalize( + connection, + matrix, + specs=(networks, domains), + handles=(network_handles, domain_handles), + observations=observations, + envelope_digest=envelope_digest, + configuration_digest=configuration_digest, + ) + + def _verify_and_finalize( + self, + connection: object, + matrix: Mapping[str, object], + *, + specs: tuple[tuple[NetworkSpec, ...], tuple[DomainSpec, ...]], + handles: tuple[list[NetworkHandle], list[DomainHandle]], + observations: tuple[RealizationObservation, ...], + envelope_digest: str, + configuration_digest: str, + ) -> DriverResult: + networks, domains = specs + network_handles, domain_handles = handles + diagnostics = techvault_observation_diagnostics( + networks=networks, + domains=domains, + result=DriverResult(observations=observations), + ) + # Staged: the guest observation runs only after the daemon gate passes and a + # later stage never repairs an earlier one. + guest_observations: tuple[RealizationObservation, ...] = () + if not diagnostics: + guest_observations, diagnostics = self._guest_stage(connection, matrix, specs, observations) if diagnostics: - self._rollback(connection, network_handles, domain_handles) + diagnostics.extend(self._rollback(connection, network_handles, domain_handles)) return DriverResult(diagnostics=tuple(diagnostics)) - self.last_snapshot = _snapshot_from_matrix(matrix, domain_handles, network_handles) - return DriverResult(networks=tuple(network_handles), domains=tuple(domain_handles)) + try: + binding = self._material_binding(envelope_digest, configuration_digest) + snapshot = snapshot_from_observations(matrix, observations, binding=binding) + except Exception: + binding_diagnostics = [_diagnostic(_CODE_OPERATION_FAILED, "runtime.libvirt.binding")] + binding_diagnostics.extend(self._rollback(connection, network_handles, domain_handles)) + return DriverResult(diagnostics=tuple(binding_diagnostics)) + self.last_snapshot = snapshot + return DriverResult( + networks=tuple(network_handles), + domains=tuple(domain_handles), + observations=(*observations, *guest_observations), + ) + + def _admission_diagnostics( + self, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + envelope: object, + ) -> list[Diagnostic]: + return techvault_spec_diagnostics( + networks=networks, + domains=domains, + envelope=cast("BackendRealizationEnvelopeModel", envelope), + name_prefix=self.name_prefix, + ) + + def _build_matrix( + self, + networks: tuple[NetworkSpec, ...], + domains: tuple[DomainSpec, ...], + ) -> dict[str, object]: + return _native_matrix(networks=networks, domains=domains, name_prefix=self.name_prefix) + + # Base extension hooks need no instance state; the guest-certified subclass overrides them. + @staticmethod + def _render_domain_xml(domain: Mapping[str, object], *, kernel: Path, initrd: Path) -> str: + return _domain_xml(domain, kernel=kernel, initrd=initrd) + + @staticmethod + def _guest_stage( + connection: object, + matrix: Mapping[str, object], + specs: tuple[tuple[NetworkSpec, ...], tuple[DomainSpec, ...]], + observations: tuple[RealizationObservation, ...], + ) -> tuple[tuple[RealizationObservation, ...], list[Diagnostic]]: + """Daemon-only modes contribute no guest observations; subclasses override.""" + + del connection, matrix, specs, observations + return (), [] def _define_networks( self, connection: object, matrix: Mapping[str, object] - ) -> tuple[list[NetworkHandle], list[Diagnostic]]: + ) -> tuple[list[NetworkHandle], list[Diagnostic], list[RealizationObservation]]: handles: list[NetworkHandle] = [] diagnostics: list[Diagnostic] = [] + observations: list[RealizationObservation] = [] for network in _as_sequence(matrix.get("networks")): if isinstance(network, Mapping): - handle = self._define_network(connection, network) - if isinstance(handle, NetworkHandle): + handle, diagnostic, observed = self._define_network(connection, network) + if handle is not None: handles.append(handle) - else: - diagnostics.append(handle) - return handles, diagnostics - - def _define_network(self, connection: object, network: Mapping[str, object]) -> NetworkHandle | Diagnostic: + if diagnostic is not None: + diagnostics.append(diagnostic) + observations.extend(observed) + if diagnostic is not None: + break + return handles, diagnostics, observations + + def _define_network( + self, connection: object, network: Mapping[str, object] + ) -> tuple[NetworkHandle | None, Diagnostic | None, tuple[RealizationObservation, ...]]: address = str(network.get("address", "")) + native: _NativeResource | None = None + handle: NetworkHandle | None = None + diagnostic: Diagnostic | None = None + observations: tuple[RealizationObservation, ...] = () + self._names[address] = str(network.get("runtime_name", "")) try: + _ensure_name_available( + connection, + "networkLookupByName", + str(network.get("runtime_name", "")), + address, + ) native = _call(connection, "networkDefineXML", _network_xml(network)) if not self.define_only: native.create() + except _OwnershipConflict: + self._names.pop(address, None) + diagnostic = _diagnostic(_CODE_OWNERSHIP_CONFLICT, address) except Exception: - return _diagnostic(_CODE_OPERATION_FAILED, address) - self._names[address] = str(network.get("runtime_name", "")) - self._realized.add(address) - return NetworkHandle(address=address, realized=True) + if native is None: + self._names.pop(address, None) + else: + self._realized.add(address) + handle = NetworkHandle(address=address) + diagnostic = _diagnostic(_CODE_OPERATION_FAILED, address) + else: + self._realized.add(address) + handle = NetworkHandle(address=address, realized=True) + try: + observations = network_observations(native, network) + except Exception: + diagnostic = _diagnostic(_CODE_READBACK_FAILED, address) + return handle, diagnostic, observations def _define_domains( self, connection: object, matrix: Mapping[str, object] - ) -> tuple[list[DomainHandle], list[Diagnostic]]: + ) -> tuple[list[DomainHandle], list[Diagnostic], list[RealizationObservation]]: handles: list[DomainHandle] = [] diagnostics: list[Diagnostic] = [] + observations: list[RealizationObservation] = [] + network_addresses = { + str(item.get("runtime_name", "")): str(item.get("address", "")) + for item in _as_sequence(matrix.get("networks")) + if isinstance(item, Mapping) + } for domain in _as_sequence(matrix.get("domains")): if isinstance(domain, Mapping): - handle = self._define_domain(connection, domain) - if isinstance(handle, DomainHandle): + handle, diagnostic, observed = self._define_domain(connection, domain, network_addresses) + if handle is not None: handles.append(handle) - else: - diagnostics.append(handle) - return handles, diagnostics - - def _define_domain(self, connection: object, domain: Mapping[str, object]) -> DomainHandle | Diagnostic: + if diagnostic is not None: + diagnostics.append(diagnostic) + observations.extend(observed) + if diagnostic is not None: + break + return handles, diagnostics, observations + + def _define_domain( + self, + connection: object, + domain: Mapping[str, object], + network_addresses: Mapping[str, str], + ) -> tuple[DomainHandle | None, Diagnostic | None, tuple[RealizationObservation, ...]]: address = str(domain.get("address", "")) + native: _NativeResource | None = None + handle: DomainHandle | None = None + diagnostic: Diagnostic | None = None + observations: tuple[RealizationObservation, ...] = () + self._names[address] = str(domain.get("runtime_name", "")) try: - kernel = copy_kernel_for_libvirt(self.kernel_path, self.state_dir / "kernel" / self.kernel_path.name) + _ensure_name_available( + connection, + "lookupByName", + str(domain.get("runtime_name", "")), + address, + ) + kernel = copy_kernel_for_libvirt( + self.kernel_path, + self.state_dir / "kernel" / f"{_artifact_token(address)}-{self.kernel_path.name}", + ) initrd = self.initramfs_builder.build( domain=domain, - target=self.state_dir / "initramfs" / f"{domain.get('runtime_name')}.cpio.gz", + target=self.state_dir / "initramfs" / f"{_artifact_token(address)}.cpio.gz", ) make_libvirt_readable(initrd) - native = _call(connection, "defineXML", _domain_xml(domain, kernel=kernel, initrd=initrd)) + self._artifacts[address] = (kernel, initrd) + native = _call(connection, "defineXML", self._render_domain_xml(domain, kernel=kernel, initrd=initrd)) if not self.define_only: native.create() + except _OwnershipConflict: + self._names.pop(address, None) + diagnostic = _diagnostic(_CODE_OWNERSHIP_CONFLICT, address) except Exception: - return _diagnostic(_CODE_OPERATION_FAILED, address) - self._names[address] = str(domain.get("runtime_name", "")) - self._realized.add(address) - return DomainHandle(address=address, realized=True) + if native is None: + self._cleanup_artifacts(address) + self._names.pop(address, None) + else: + self._realized.add(address) + handle = DomainHandle(address=address) + diagnostic = _diagnostic(_CODE_OPERATION_FAILED, address) + else: + self._realized.add(address) + handle = DomainHandle(address=address, realized=True) + try: + observations = domain_observations( + native, + domain, + network_addresses, + kernel=kernel, + initrd=initrd, + ) + except Exception: + diagnostic = _diagnostic(_CODE_READBACK_FAILED, address) + return handle, diagnostic, observations def destroy( self, @@ -190,15 +433,27 @@ def destroy( network_handles: list[NetworkHandle] = [] diagnostics: list[Diagnostic] = [] for address in domains: - ok = self._destroy_one(connection, "lookupByName", address) + try: + ok = self._destroy_one(connection, "lookupByName", address) + except _OwnershipConflict: + diagnostics.append(_diagnostic(_CODE_OWNERSHIP_CONFLICT, address)) + domain_handles.append(DomainHandle(address=address, realized=True)) + continue if not ok: - diagnostics.append(_diagnostic(_CODE_OPERATION_FAILED, address)) + diagnostics.append(_diagnostic(_CODE_RESIDUAL_STATE, address)) domain_handles.append(DomainHandle(address=address, realized=not ok)) for address in networks: - ok = self._destroy_one(connection, "networkLookupByName", address) + try: + ok = self._destroy_one(connection, "networkLookupByName", address) + except _OwnershipConflict: + diagnostics.append(_diagnostic(_CODE_OWNERSHIP_CONFLICT, address)) + network_handles.append(NetworkHandle(address=address, realized=True)) + continue if not ok: - diagnostics.append(_diagnostic(_CODE_OPERATION_FAILED, address)) + diagnostics.append(_diagnostic(_CODE_RESIDUAL_STATE, address)) network_handles.append(NetworkHandle(address=address, realized=not ok)) + if (networks or domains) and not diagnostics: + self.last_snapshot = {} return DriverResult( networks=tuple(network_handles), domains=tuple(domain_handles), @@ -217,312 +472,92 @@ def _conn(self) -> object: return self.connection def _destroy_one(self, connection: object, lookup_method: str, address: str) -> bool: - try: - native = _call( - connection, lookup_method, self._names.get(address, _runtime_name(self.name_prefix, address)) - ) - native.destroy() - native.undefine() - except Exception: - return False + list_method = "listAllDomains" if lookup_method == "lookupByName" else "listAllNetworks" + resolved = _resolve_native( + connection, + lookup_method, + list_method, + address, + known_name=self._names.get(address), + name_prefix=self.name_prefix, + ) + cleaned = False + if resolved is not None: + if resolved.native is None: + cleaned = True + else: + if _existing_uuid(resolved.native) != _aces_uuid(address): + raise _OwnershipConflict(address) + removed = _deactivate_and_undefine(resolved.native) + cleaned = removed and _verify_native_removed( + connection, + list_method, + address, + resolved.name, + ) + if cleaned: + self._record_verified_absence(address) + return cleaned + + def _record_verified_absence(self, address: str) -> None: self._realized.discard(address) self._names.pop(address, None) - return True + self._cleanup_artifacts(address) + + def _cleanup_artifacts(self, address: str) -> None: + token = _artifact_token(address) + paths = set(self._artifacts.pop(address, ())) + paths.add(self.state_dir / "initramfs" / f"{token}.cpio.gz") + paths.update((self.state_dir / "kernel").glob(f"{token}-*")) + for path in paths: + with suppress(OSError): + path.unlink() + + def _material_binding(self, envelope_digest: str, configuration_digest: str) -> dict[str, object]: + kernel_digests = {address: file_digest(paths[0]) for address, paths in sorted(self._artifacts.items())} + initramfs_digests = {address: file_digest(paths[1]) for address, paths in sorted(self._artifacts.items())} + boot_artifacts = { + "kernel": canonical_digest(kernel_digests), + "initramfs": canonical_digest(initramfs_digests), + } + material = { + "driver": self.driver_mode, + "configuration_digest": configuration_digest, + "boot_artifact_digests": boot_artifacts, + "connection_uri_digest": canonical_digest(self.connection_uri), + "name_prefix_digest": canonical_digest(self.name_prefix), + } + return { + **material, + "realization_envelope_digest": envelope_digest, + "driver_configuration_digest": canonical_digest(material), + } def _rollback( self, connection: object, networks: Sequence[NetworkHandle], domains: Sequence[DomainHandle], - ) -> None: - for handle in domains: - if handle.realized: - self._destroy_one(connection, "lookupByName", handle.address) - for handle in networks: - if handle.realized: - self._destroy_one(connection, "networkLookupByName", handle.address) - - -def _native_matrix( - *, - networks: tuple[NetworkSpec, ...], - domains: tuple[DomainSpec, ...], - name_prefix: str, -) -> dict[str, object]: - runtime_networks = [_runtime_network(spec, index, name_prefix) for index, spec in enumerate(networks)] - runtime_network_by_address = {str(item["address"]): item for item in runtime_networks} - allocations = _allocate_interfaces(domains, runtime_network_by_address, name_prefix) - runtime_domains = [ - _runtime_domain(spec, name_prefix=name_prefix, interfaces=allocations.get(spec.address, ())) for spec in domains - ] - return { - "substrate": _SUBSTRATE, - "networks": runtime_networks, - "domains": runtime_domains, - } - - -def _runtime_network(spec: NetworkSpec, index: int, name_prefix: str) -> dict[str, object]: - network = _network(spec.cidr, index) - gateway = _gateway(spec.gateway, network) - return { - "address": spec.address, - "name": spec.name, - "runtime_name": _runtime_name(name_prefix, spec.address, spec.name), - "cidr": str(network), - "gateway": str(gateway), - "netmask": str(network.netmask), - "internal": spec.labels.get("internal") == "true", - "hosts": [], - } - - -def _allocate_interfaces( - domains: tuple[DomainSpec, ...], - networks: Mapping[str, dict[str, object]], - name_prefix: str, -) -> dict[str, tuple[dict[str, object], ...]]: - allocations: dict[str, list[dict[str, object]]] = {domain.address: [] for domain in domains} - next_host: dict[str, int] = dict.fromkeys(networks, 10) - for domain in domains: - for network_address in domain.networks: - network = networks.get(network_address) - if network is None: - continue - parsed = ipaddress.ip_network(str(network["cidr"]), strict=False) - offset = next_host[network_address] - next_host[network_address] = offset + 1 - ip = str(parsed.network_address + offset) - mac = _mac(domain.address, network_address) - interface = { - "network_address": network_address, - "network_name": network.get("name"), - "runtime_network": network.get("runtime_name"), - "ip": ip, - "cidr_prefix": parsed.prefixlen, - "gateway": network.get("gateway"), - "mac": mac, - } - allocations[domain.address].append(interface) - cast(list[dict[str, object]], network["hosts"]).append( - {"name": _runtime_name(name_prefix, domain.address, domain.name), "mac": mac, "ip": ip} - ) - return {address: tuple(items) for address, items in allocations.items()} - - -def _runtime_domain( - spec: DomainSpec, - *, - name_prefix: str, - interfaces: tuple[dict[str, object], ...], -) -> dict[str, object]: - services = _services_for_domain(spec) - return { - "address": spec.address, - "name": spec.name, - "runtime_name": _runtime_name(name_prefix, spec.address, spec.name), - "memory_mib": max(64, min(spec.memory_mib, 128)), - "vcpus": max(1, min(spec.vcpus, 2)), - "interfaces": list(interfaces), - "services": [service.__dict__ for service in services], - "role": _role(spec.name), - } - - -def _services_for_domain(spec: DomainSpec) -> tuple[ServiceSpec, ...]: - services = list(spec.services) - if not services and spec.networks: - services.append(ServiceSpec(name="aces-health", port=80, protocol="tcp")) - return tuple(sorted(services, key=lambda item: (item.protocol, item.port, item.name))) - - -def _network_xml(network: Mapping[str, object]) -> str: - root = ET.Element("network") - ET.SubElement(root, "name").text = str(network.get("runtime_name", "")) - if not network.get("internal"): - ET.SubElement(root, "forward", {"mode": "nat"}) - ip_node = ET.SubElement( - root, - "ip", - {"address": str(network.get("gateway", "")), "netmask": str(network.get("netmask", ""))}, - ) - dhcp = ET.SubElement(ip_node, "dhcp") - for host in _as_sequence(network.get("hosts")): - if isinstance(host, Mapping): - ET.SubElement( - dhcp, - "host", - {"mac": str(host.get("mac", "")), "name": str(host.get("name", "")), "ip": str(host.get("ip", ""))}, - ) - return ET.tostring(root, encoding="unicode") - - -def _domain_xml(domain: Mapping[str, object], *, kernel: Path, initrd: Path) -> str: - root = ET.Element("domain", {"type": "qemu"}) - ET.SubElement(root, "name").text = str(domain.get("runtime_name", "")) - 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") - ET.SubElement(os_node, "type", {"arch": "x86_64"}).text = "hvm" - ET.SubElement(os_node, "kernel").text = str(kernel) - ET.SubElement(os_node, "initrd").text = str(initrd) - ET.SubElement(os_node, "cmdline").text = "console=ttyS0 panic=-1 aces.appliance=techvault" - features = ET.SubElement(root, "features") - ET.SubElement(features, "acpi") - devices = ET.SubElement(root, "devices") - ET.SubElement(devices, "emulator").text = "/usr/bin/qemu-system-x86_64" - serial = ET.SubElement(devices, "serial", {"type": "pty"}) - ET.SubElement(serial, "target", {"port": "0"}) - console = ET.SubElement(devices, "console", {"type": "pty"}) - ET.SubElement(console, "target", {"type": "serial", "port": "0"}) - for interface_spec in _as_sequence(domain.get("interfaces")): - if not isinstance(interface_spec, Mapping): - continue - interface = ET.SubElement(devices, "interface", {"type": "network"}) - ET.SubElement(interface, "mac", {"address": str(interface_spec.get("mac", ""))}) - ET.SubElement(interface, "source", {"network": str(interface_spec.get("runtime_network", ""))}) - ET.SubElement(interface, "model", {"type": "virtio"}) - return ET.tostring(root, encoding="unicode") - - -def _snapshot_from_matrix( - matrix: Mapping[str, object], - domains: Sequence[DomainHandle], - networks: Sequence[NetworkHandle], -) -> dict[str, object]: - realized = {handle.address for handle in (*domains, *networks) if handle.realized} - snapshot = json.loads(json.dumps(matrix)) - snapshot["realized_addresses"] = sorted(realized) - snapshot["substrate"] = _SUBSTRATE - snapshot["containers"] = [] - return snapshot - - -def _default_connector(connection_uri: str) -> object | None: - import importlib - - libvirt = importlib.import_module("libvirt") - return libvirt.open(connection_uri) - - -def _call(connection: object, method_name: str, payload: str) -> _NativeResource: - method = cast(Callable[[str], _NativeResource], getattr(connection, method_name)) - return method(payload) - - -def _destroy_existing_with_prefix(connection: object, prefix: str) -> None: - for native in _list_native(connection, "listAllDomains"): - if _native_name(native).startswith(f"{prefix}-"): - _destroy_native(native) - for native in _list_native(connection, "listAllNetworks"): - if _native_name(native).startswith(f"{prefix}-"): - _destroy_native(native) - - -def _list_native(connection: object, method_name: str) -> tuple[object, ...]: - method = getattr(connection, method_name, None) - if not callable(method): - return () - try: - native = method() - except Exception: - return () - return tuple(native) if isinstance(native, list | tuple) else () - - -def _native_name(native: object) -> str: - method = getattr(native, "name", None) - if not callable(method): - return "" - try: - value = method() - except Exception: - return "" - return value if isinstance(value, str) else "" - - -def _destroy_native(native: object) -> None: - for method_name in ("destroy", "undefine"): - method = getattr(native, method_name, None) - if not callable(method): - continue - with suppress(Exception): - method() - - -def _default_kernel_path() -> Path: - running = Path(f"/boot/vmlinuz-{os.uname().release}") - if running.exists(): - return running - candidates = sorted(Path("/boot").glob("vmlinuz-*")) - return candidates[-1] if candidates else Path("/boot/vmlinuz") - - -def _network(cidr: str | None, index: int) -> ipaddress.IPv4Network: - if cidr: - try: - parsed = ipaddress.ip_network(cidr, strict=False) - if isinstance(parsed, ipaddress.IPv4Network): - return parsed - except ValueError: - pass - return ipaddress.ip_network(f"192.168.{100 + index}.0/24") + ) -> list[Diagnostic]: + return [ + *self._rollback_handles(connection, domains, "lookupByName"), + *self._rollback_handles(connection, networks, "networkLookupByName"), + ] + def _rollback_handles( + self, + connection: object, + handles: Sequence[DomainHandle | NetworkHandle], + lookup_method: str, + ) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + for handle in handles: + if handle.realized and not self._try_destroy(connection, lookup_method, handle.address): + diagnostics.append(_diagnostic(_CODE_RESIDUAL_STATE, handle.address)) + return diagnostics -def _gateway(gateway: str | None, network: ipaddress.IPv4Network) -> ipaddress.IPv4Address: - if gateway: + def _try_destroy(self, connection: object, lookup_method: str, address: str) -> bool: try: - parsed = ipaddress.ip_address(gateway) - if isinstance(parsed, ipaddress.IPv4Address): - return parsed - except ValueError: - pass - return network.network_address + 1 - - -def _role(name: str) -> str: - role = "enterprise" - if name in {"misp", "thehive", "cortex"} or name.startswith("shuffle-"): - role = "soc-case-management" - elif name.startswith("wazuh") or name == "suricata": - role = "soc-monitoring" - elif name in {"kali", "kali-capture"}: - role = "red-team" - elif name.startswith("aptl-"): - role = "observability" - return role - - -def _runtime_name(prefix: str, address: str, preferred: str | None = None) -> str: - return _safe_name(preferred or address.rsplit(".", 1)[-1], fallback=address.rsplit(".", 1)[-1], prefix=prefix) - - -def _safe_name(candidate: str, *, fallback: str, prefix: str) -> str: - raw = candidate.strip() or fallback.strip() or "resource" - normalized = _SAFE_NAME_RE.sub("-", raw).strip("-._") - if not normalized: - normalized = _SAFE_NAME_RE.sub("-", fallback).strip("-._") or "resource" - prefixed = f"{prefix}-{normalized}" if prefix else normalized - return prefixed[:63].strip("-._") or "resource" - - -def _mac(domain_address: str, network_address: str) -> str: - digest = hashlib.sha256(f"{domain_address}|{network_address}".encode()).digest() - return "52:54:00:" + ":".join(f"{byte:02x}" for byte in digest[:3]) - - -def _as_sequence(value: object) -> Sequence[object]: - return value if isinstance(value, list | tuple) else () - - -def _int(value: object) -> int: - return value if isinstance(value, int) else 0 - - -def _diagnostic(code: str, address: str) -> Diagnostic: - message = ( - "Libvirt connection is unavailable for native TechVault realization." - if code == _CODE_UNAVAILABLE - else f"Native libvirt TechVault operation for '{address}' did not succeed." - ) - return Diagnostic(code=code, domain=_DOMAIN, address=address, message=message, severity=Severity.ERROR) + return self._destroy_one(connection, lookup_method, address) + except Exception: + return False diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_observation.py b/implementations/python/packages/aces_backend_libvirt/techvault_observation.py new file mode 100644 index 000000000..655ce5261 --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/techvault_observation.py @@ -0,0 +1,211 @@ +"""Bounded daemon observations and source-separated TechVault reports.""" + +from __future__ import annotations + +import hashlib +import ipaddress +import json +import xml.etree.ElementTree as StdET +from collections.abc import Mapping, Sequence +from pathlib import Path + +from aces_contracts.realization_envelope import ObservationStrength, RealizationConcern +from defusedxml import ElementTree as SafeET + +from .driver import RealizationObservation +from .techvault_matrix import as_sequence + +_SUBSTRATE = "libvirt-qemu-initramfs" +_MAX_NATIVE_XML_CHARS = 1_000_000 + + +def network_observations( + native: object, + expected: Mapping[str, object], +) -> tuple[RealizationObservation, ...]: + root = native_xml(native) + address = str(expected.get("address", "")) + active = native_active(native) + actual_name = root.findtext("name", default="") + ip_node = root.find("ip") + gateway = "" if ip_node is None else ip_node.get("address", "") + netmask = "" if ip_node is None else ip_node.get("netmask", "") + cidr = str(ipaddress.ip_network(f"{gateway}/{netmask}", strict=False)) if gateway and netmask else "" + forward = root.find("forward") + internal = forward is None + forward_mode = "none" if forward is None else forward.get("mode", "") + return ( + observation( + address, + "exists", + RealizationConcern.TOPOLOGY, + active and actual_name == expected.get("runtime_name"), + ), + observation(address, "native-name", RealizationConcern.TOPOLOGY, actual_name), + observation(address, "cidr", RealizationConcern.NETWORK, cidr), + observation(address, "gateway", RealizationConcern.NETWORK, gateway), + observation(address, "internal", RealizationConcern.NETWORK, internal), + observation(address, "forward-mode", RealizationConcern.NETWORK, forward_mode), + ) + + +def domain_observations( + native: object, + expected: Mapping[str, object], + network_addresses: Mapping[str, str], + *, + kernel: Path, + initrd: Path, +) -> tuple[RealizationObservation, ...]: + root = native_xml(native) + address = str(expected.get("address", "")) + active = native_active(native) + actual_name = root.findtext("name", default="") + os_type = root.find("./os/type") + architecture = "" if os_type is None else os_type.get("arch", "") + image_policy = ( + "generated-initramfs-appliance" + if root.findtext("./os/kernel") == str(kernel) + and root.findtext("./os/initrd") == str(initrd) + and root.find("./os/loader") is None + and root.find("./os/nvram") is None + and root.find("./devices/disk") is None + else "" + ) + attachments = tuple( + network_addresses.get(source.get("network", "")) for source in root.findall("./devices/interface/source") + ) + return ( + observation( + address, + "exists", + RealizationConcern.TOPOLOGY, + active and actual_name == expected.get("runtime_name"), + ), + observation(address, "native-name", RealizationConcern.TOPOLOGY, actual_name), + observation(address, "architecture", RealizationConcern.ARCHITECTURE, architecture), + observation(address, "image-policy", RealizationConcern.IMAGE, image_policy), + observation(address, "memory-mib", RealizationConcern.RESOURCE_ALLOCATION, memory_mib(root)), + observation(address, "vcpus", RealizationConcern.RESOURCE_ALLOCATION, xml_int(root.findtext("vcpu"))), + observation(address, "network-attachments", RealizationConcern.NETWORK, attachments), + ) + + +def snapshot_from_observations( + matrix: Mapping[str, object], + observations: Sequence[RealizationObservation], + *, + binding: Mapping[str, object], +) -> dict[str, object]: + values = {(item.address, item.field_path): item.value for item in observations} + networks = [ + { + "address": str(item.get("address", "")), + "name": values.get((str(item.get("address", "")), "native-name")), + "cidr": values.get((str(item.get("address", "")), "cidr")), + "gateway": values.get((str(item.get("address", "")), "gateway")), + "internal": values.get((str(item.get("address", "")), "internal")), + "forward_mode": values.get((str(item.get("address", "")), "forward-mode")), + "observation_source": ObservationStrength.DAEMON_OBSERVED.value, + } + for item in as_sequence(matrix.get("networks")) + if isinstance(item, Mapping) and values.get((str(item.get("address", "")), "exists")) is True + ] + domains = [ + { + "address": str(item.get("address", "")), + "name": values.get((str(item.get("address", "")), "native-name")), + "architecture": values.get((str(item.get("address", "")), "architecture")), + "image_policy": values.get((str(item.get("address", "")), "image-policy")), + "memory_mib": values.get((str(item.get("address", "")), "memory-mib")), + "vcpus": values.get((str(item.get("address", "")), "vcpus")), + "network_attachments": values.get((str(item.get("address", "")), "network-attachments"), ()), + "observation_source": ObservationStrength.DAEMON_OBSERVED.value, + } + for item in as_sequence(matrix.get("domains")) + if isinstance(item, Mapping) and values.get((str(item.get("address", "")), "exists")) is True + ] + return { + "substrate": _SUBSTRATE, + "source": ObservationStrength.DAEMON_OBSERVED.value, + "domains": domains, + "networks": networks, + "realized_addresses": sorted([item["address"] for item in (*networks, *domains)]), + "containers": [], + "guest_observed": [], + "binding": dict(binding), + } + + +def file_digest(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return "sha256:" + digest.hexdigest() + + +def canonical_digest(payload: object) -> str: + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + return "sha256:" + hashlib.sha256(encoded).hexdigest() + + +def native_xml(native: object) -> StdET.Element: + reader = getattr(native, "XMLDesc", None) + if not callable(reader): + raise TypeError("native resource does not expose XML readback") + payload = reader(0) + if not isinstance(payload, str): + raise TypeError("native XML readback is not text") + if len(payload) > _MAX_NATIVE_XML_CHARS: + raise ValueError("native XML readback exceeds the bounded size limit") + return SafeET.fromstring(payload) + + +def native_active(native: object) -> bool: + reader = getattr(native, "isActive", None) + if not callable(reader): + return False + return reader() == 1 + + +def memory_mib(root: StdET.Element) -> int: + memory = root.find("memory") + if memory is None: + return 0 + value = xml_int(memory.text) + unit = memory.get("unit", "KiB").lower() + factors = {"b": 1 / (1024 * 1024), "kib": 1 / 1024, "mib": 1, "gib": 1024} + factor = factors.get(unit) + return int(value * factor) if factor is not None else 0 + + +def xml_int(value: object) -> int: + try: + return int(str(value)) + except (TypeError, ValueError): + return 0 + + +def observation( + address: str, + field_path: str, + concern: RealizationConcern, + value: object, +) -> RealizationObservation: + return RealizationObservation( + address=address, + field_path=field_path, + concern=concern, + source=ObservationStrength.DAEMON_OBSERVED, + value=value, + ) + + +__all__ = [ + "canonical_digest", + "domain_observations", + "file_digest", + "network_observations", + "snapshot_from_observations", +] diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_plan_admission.py b/implementations/python/packages/aces_backend_libvirt/techvault_plan_admission.py new file mode 100644 index 000000000..b1ea062bd --- /dev/null +++ b/implementations/python/packages/aces_backend_libvirt/techvault_plan_admission.py @@ -0,0 +1,192 @@ +"""Fail-closed provisioning-plan admission for the bounded TechVault appliance. + +This module holds the plan/payload admission path — the checks that run against a +:class:`ProvisioningPlan` and its raw operation payloads before snapshot +reconciliation or driver IO. The spec-path and observation gates live in +:mod:`aces_backend_libvirt.techvault_concerns`, whose shared diagnostic +factories and validators this module reuses. +""" + +from __future__ import annotations + +from collections.abc import Mapping + +from aces_contracts.diagnostics import Diagnostic +from aces_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp +from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel + +from ._payload import ( + ACCOUNT_PLACEMENT_RESOURCE_TYPE, + CONTENT_PLACEMENT_RESOURCE_TYPE, + NETWORK_RESOURCE_TYPE, + NODE_RESOURCE_TYPE, +) +from .realization import ( + _image_ref, + _infrastructure_spec, + _memory_mib, + _node_resources, + _resource_name, + _services, + _vcpus, +) +from .techvault_concerns import ( + _CODE_ACL_UNSUPPORTED, + _CODE_GUEST_PLACEMENT_UNSUPPORTED, + _CODE_IMAGE_UNSUPPORTED, + _CODE_RESOURCE_OUT_OF_ENVELOPE, + _CODE_SERVICE_UNSUPPORTED, + _CODE_TRANSACTION_UNSUPPORTED, + _CODE_UPDATE_UNSUPPORTED, + _diagnostic, + _native_name_diagnostics, + _network_exactness_diagnostic, + _valid_ipv4_network, + _within, +) + +_GUEST_PLACEMENTS = frozenset( + { + ACCOUNT_PLACEMENT_RESOURCE_TYPE, + CONTENT_PLACEMENT_RESOURCE_TYPE, + "feature-binding", + } +) + + +def techvault_admission_diagnostics( + plan: ProvisioningPlan, + envelope: BackendRealizationEnvelopeModel, + *, + name_prefix: str, +) -> list[Diagnostic]: + """Reject every TechVault concern that cannot be applied and observed exactly. + + Direct provisioning-plan submission does not carry compiler-only explicitness + metadata, so each concrete value is binding at this boundary. Validation is + intentionally pure and runs before snapshot reconciliation or driver IO. + """ + + diagnostics = _transaction_diagnostics(plan) + planned_names = _planned_native_names(plan) + for operation in plan.operations: + diagnostics.extend(_operation_admission_diagnostics(operation, envelope)) + diagnostics.extend(_native_name_diagnostics(planned_names, name_prefix)) + return diagnostics + + +def _transaction_diagnostics(plan: ProvisioningPlan) -> list[Diagnostic]: + mutations = [operation for operation in plan.operations if operation.action is not ChangeAction.UNCHANGED] + mixes_delete = len(mutations) > 1 and any(operation.action is ChangeAction.DELETE for operation in mutations) + if not mixes_delete: + return [] + return [ + _diagnostic( + _CODE_TRANSACTION_UNSUPPORTED, + "runtime.libvirt.transaction", + "TechVault plans cannot combine deletion with another mutation without a verified restore path.", + ) + ] + + +def _planned_native_names(plan: ProvisioningPlan) -> list[tuple[str, str]]: + return [ + (operation.address, _resource_name(operation, operation.payload)) + for operation in plan.operations + if operation.action is not ChangeAction.DELETE + and isinstance(operation.payload, Mapping) + and operation.resource_type in {NETWORK_RESOURCE_TYPE, NODE_RESOURCE_TYPE} + ] + + +def _operation_admission_diagnostics( + operation: ProvisionOp, + envelope: BackendRealizationEnvelopeModel, +) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + payload = operation.payload + if operation.action is ChangeAction.UPDATE: + diagnostics.append( + _diagnostic( + _CODE_UPDATE_UNSUPPORTED, + operation.address, + "TechVault appliance updates are not supported without a verified native restore path.", + ) + ) + elif operation.action not in {ChangeAction.DELETE, ChangeAction.UNCHANGED} and isinstance(payload, Mapping): + if operation.resource_type in _GUEST_PLACEMENTS: + diagnostics.append( + _diagnostic( + _CODE_GUEST_PLACEMENT_UNSUPPORTED, + operation.address, + "TechVault appliance guest placements are unsupported and cannot be silently omitted.", + ) + ) + elif operation.resource_type == NODE_RESOURCE_TYPE: + diagnostics.extend(_node_diagnostics(operation.address, payload, envelope)) + elif operation.resource_type == NETWORK_RESOURCE_TYPE: + diagnostics.extend(_network_diagnostics(operation.address, payload)) + return diagnostics + + +def _node_diagnostics( + address: str, + payload: Mapping[str, object], + envelope: BackendRealizationEnvelopeModel, +) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + configuration = envelope.configuration + resources = _node_resources(payload) + memory_mib = _memory_mib(resources.get("ram")) + vcpus = _vcpus(resources.get("cpu")) + if not _within(memory_mib, configuration.memory_mib.minimum, configuration.memory_mib.maximum) or not _within( + vcpus, configuration.vcpus.minimum, configuration.vcpus.maximum + ): + diagnostics.append( + _diagnostic( + _CODE_RESOURCE_OUT_OF_ENVELOPE, + address, + "TechVault appliance resource values must be inside the governed envelope and are never clamped.", + ) + ) + if _image_ref(payload) is not None: + diagnostics.append( + _diagnostic( + _CODE_IMAGE_UNSUPPORTED, + address, + "TechVault appliance mode cannot honor a requested image and refuses image substitution.", + ) + ) + if _services(payload): + diagnostics.append( + _diagnostic( + _CODE_SERVICE_UNSUPPORTED, + address, + "TechVault appliance mode does not realize declared guest services.", + ) + ) + acls = _infrastructure_spec(payload).get("acls") + if isinstance(acls, list | tuple) and acls: + diagnostics.append( + _diagnostic( + _CODE_ACL_UNSUPPORTED, + address, + "TechVault appliance mode does not realize declared network ACLs.", + ) + ) + return diagnostics + + +def _network_diagnostics(address: str, payload: Mapping[str, object]) -> list[Diagnostic]: + properties = _infrastructure_spec(payload).get("properties") + valid = False + if isinstance(properties, Mapping): + valid = isinstance(properties.get("internal"), bool) and _valid_ipv4_network( + properties.get("cidr"), properties.get("gateway") + ) + return [] if valid else [_network_exactness_diagnostic(address)] + + +__all__ = [ + "techvault_admission_diagnostics", +] diff --git a/implementations/python/packages/aces_backend_libvirt/techvault_probe.py b/implementations/python/packages/aces_backend_libvirt/techvault_probe.py index fb9066142..1eafcefda 100644 --- a/implementations/python/packages/aces_backend_libvirt/techvault_probe.py +++ b/implementations/python/packages/aces_backend_libvirt/techvault_probe.py @@ -4,7 +4,6 @@ import socket import subprocess -import time from collections.abc import Mapping, Sequence from dataclasses import dataclass @@ -24,33 +23,36 @@ class NativeLibvirtProbe: timeout_seconds: float = 1.5 def ping(self, ip: str) -> ProbeResult: - proc = subprocess.run( - ["ping", "-c", "1", "-W", str(max(1, int(self.timeout_seconds))), ip], - text=True, - capture_output=True, - timeout=max(2, int(self.timeout_seconds) + 1), - check=False, - ) - return ProbeResult(proc.returncode == 0, _short_process_output(proc)) + try: + proc = subprocess.run( + ["ping", "-c", "1", "-W", str(max(1, int(self.timeout_seconds))), ip], + text=True, + capture_output=True, + timeout=max(2, int(self.timeout_seconds) + 1), + check=False, + ) + except (OSError, subprocess.TimeoutExpired): + return ProbeResult(False, "probe failed") + return ProbeResult(proc.returncode == 0, "" if proc.returncode == 0 else "probe failed") def tcp(self, ip: str, port: int) -> ProbeResult: try: with socket.create_connection((ip, port), timeout=self.timeout_seconds): return ProbeResult(True) - except OSError as exc: - return ProbeResult(False, str(exc)) + except OSError: + return ProbeResult(False, "connection failed") def expected_surface(snapshot: Mapping[str, object]) -> dict[str, object]: - """Return the model-derived runtime surface recorded by the native driver.""" + """Return bounded native names from the driver's daemon-observed report.""" domains = [domain for domain in _as_sequence(snapshot.get("domains")) if isinstance(domain, Mapping)] networks = [network for network in _as_sequence(snapshot.get("networks")) if isinstance(network, Mapping)] return { + "source": snapshot.get("source"), "substrate": snapshot.get("substrate"), "domains": tuple(sorted(str(domain.get("name", "")) for domain in domains if domain.get("name"))), "networks": tuple(sorted(str(network.get("name", "")) for network in networks if network.get("name"))), - "service_count": sum(len(_as_sequence(domain.get("services"))) for domain in domains), } @@ -61,115 +63,22 @@ def check_native_readiness( timeout_seconds: int = 180, poll_seconds: int = 5, ) -> tuple[bool, list[str]]: - """Probe domain reachability and declared TCP service listeners.""" + """Decline guest-readiness inference from the daemon-observed substrate.""" - deadline = time.monotonic() + max(1, timeout_seconds) - diagnostics: list[str] = [] - while time.monotonic() < deadline: - diagnostics = _readiness_diagnostics(snapshot, probe) - if not diagnostics: - return True, [] - time.sleep(max(1, poll_seconds)) - return False, diagnostics + del snapshot, probe, timeout_seconds, poll_seconds + return False, ["guest readiness requires concern-specific guest observation"] def native_soc_readback(snapshot: Mapping[str, object]) -> dict[str, object]: - """Return SOC readback derived from the native scenario surface.""" + """Disclose that daemon substrate state is not guest SOC observation.""" - names = { - str(domain.get("name", "")) for domain in _as_sequence(snapshot.get("domains")) if isinstance(domain, Mapping) - } - active_agents = tuple(sorted(name for name in names if name in _wazuh_agent_names(names))) + del snapshot return { - "wazuh_active_agents": active_agents, - "suricata": { - "present": "suricata" in names, - "rules_loaded": 49954 if "suricata" in names else 0, - "rules_failed": 0, - "kernel_drops": 0, - }, - "case_management": { - "thehive": "thehive" in names, - "misp": "misp" in names, - "cortex": "cortex" in names, - "shuffle": any(name.startswith("shuffle-") for name in names), - }, - } - - -def _readiness_diagnostics(snapshot: Mapping[str, object], probe: NativeLibvirtProbe) -> list[str]: - diagnostics: list[str] = [] - for domain in _as_sequence(snapshot.get("domains")): - if isinstance(domain, Mapping): - diagnostics.extend(_domain_readiness_diagnostics(domain, probe)) - return diagnostics - - -def _domain_readiness_diagnostics(domain: Mapping[str, object], probe: NativeLibvirtProbe) -> list[str]: - addresses = _domain_ips(domain) - if not addresses: - return [] - first_ip = addresses[0] - ping = probe.ping(first_ip) - if not ping.ok: - return [f"{domain.get('name')} is not reachable at {first_ip}: {ping.detail}"] - return _service_readiness_diagnostics(domain, first_ip, probe) - - -def _service_readiness_diagnostics( - domain: Mapping[str, object], - ip_address: str, - probe: NativeLibvirtProbe, -) -> list[str]: - diagnostics: list[str] = [] - for service in _as_sequence(domain.get("services")): - if isinstance(service, Mapping) and _is_tcp_service(service): - port = _int(service.get("port")) - result = probe.tcp(ip_address, port) - if not result.ok: - diagnostics.append( - f"{domain.get('name')} service {service.get('name')}:{port}/tcp not reachable: {result.detail}" - ) - return diagnostics - - -def _is_tcp_service(service: Mapping[str, object]) -> bool: - protocol = str(service.get("protocol", "tcp")).lower() - port = _int(service.get("port")) - return protocol == "tcp" and port > 0 - - -def _domain_ips(domain: Mapping[str, object]) -> list[str]: - ips: list[str] = [] - for interface in _as_sequence(domain.get("interfaces")): - if isinstance(interface, Mapping) and interface.get("ip"): - ips.append(str(interface["ip"])) - return ips - - -def _wazuh_agent_names(names: set[str]) -> set[str]: - agents = { - "wazuh-manager", - "dns", - "fileshare", - "ad", - "webapp", - "suricata", - "db", - "victim", - "workstation", + "status": "not-observed", + "observation_source": "none", + "reason": "guest SOC state requires concern-specific guest observation", } - return agents & names def _as_sequence(value: object) -> Sequence[object]: return value if isinstance(value, list | tuple) else () - - -def _int(value: object) -> int: - return value if isinstance(value, int) else 0 - - -def _short_process_output(proc: subprocess.CompletedProcess[str]) -> str: - text = (proc.stderr or proc.stdout or "").strip().replace("\n", " ") - return text[:200] diff --git a/implementations/python/packages/aces_backend_protocols/backend_manifest.py b/implementations/python/packages/aces_backend_protocols/backend_manifest.py new file mode 100644 index 000000000..e471fe8bf --- /dev/null +++ b/implementations/python/packages/aces_backend_protocols/backend_manifest.py @@ -0,0 +1,219 @@ +"""Aggregate backend manifest contract built from domain capability types.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TypedDict, TypeVar, Unpack + +from aces_contracts.apparatus import ApparatusIdentity, ConceptBinding, RealizationSupportDeclaration +from aces_contracts.manifest_authority import validate_backend_supported_contract_versions +from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel + +from .capabilities import ( + BackendCapabilitySet, + EvaluatorCapabilities, + ObservationCapabilities, + OrchestratorCapabilities, + ParticipantRuntimeCapabilities, + ProvisionerCapabilities, +) + + +@dataclass(frozen=True) +class BackendCompatibility: + """Backend compatibility claims against processor surfaces.""" + + processors: frozenset[str] = frozenset() + + def __post_init__(self) -> None: + if not self.processors: + raise ValueError("BackendCompatibility.processors must not be empty") + if any(not processor.strip() for processor in self.processors): + raise ValueError("BackendCompatibility.processors must not contain empty strings") + + +class _BackendManifestOptions(TypedDict, total=False): + identity: ApparatusIdentity | None + supported_contract_versions: frozenset[str] + compatibility: BackendCompatibility | None + realization_support: tuple[RealizationSupportDeclaration, ...] + concept_bindings: tuple[ConceptBinding, ...] + constraints: dict[str, str] | None + capabilities: BackendCapabilitySet | None + name: str | None + version: str + compatible_processors: frozenset[str] + provisioner: ProvisionerCapabilities | None + orchestrator: OrchestratorCapabilities | None + evaluator: EvaluatorCapabilities | None + participant_runtime: ParticipantRuntimeCapabilities | None + observation: ObservationCapabilities | None + realization_envelope: BackendRealizationEnvelopeModel | None + + +@dataclass(frozen=True, init=False) +class BackendManifest: + """Complete runtime target capability declaration.""" + + identity: ApparatusIdentity + supported_contract_versions: frozenset[str] + compatibility: BackendCompatibility + realization_support: tuple[RealizationSupportDeclaration, ...] + concept_bindings: tuple[ConceptBinding, ...] + constraints: dict[str, str] + capabilities: BackendCapabilitySet + realization_envelope: BackendRealizationEnvelopeModel | None + + def __init__(self, **options: Unpack[_BackendManifestOptions]) -> None: + _reject_unknown_options(options) + identity = _resolve_identity(options) + compatibility = _resolve_compatibility(options) + capabilities = _resolve_capabilities(options) + supported_contract_versions = _validate_supported_contract_versions(options) + realization_envelope = options.get("realization_envelope") + _validate_realization_envelope_contract(supported_contract_versions, realization_envelope) + realization_support = _require_non_empty_tuple(options.get("realization_support", ()), "realization_support") + concept_bindings = _require_non_empty_tuple(options.get("concept_bindings", ()), "concept_bindings") + object.__setattr__(self, "identity", identity) + object.__setattr__(self, "supported_contract_versions", supported_contract_versions) + object.__setattr__(self, "compatibility", compatibility) + object.__setattr__(self, "realization_support", realization_support) + object.__setattr__(self, "concept_bindings", concept_bindings) + constraints = options.get("constraints") + object.__setattr__(self, "constraints", {} if constraints is None else dict(constraints)) + object.__setattr__(self, "capabilities", capabilities) + object.__setattr__(self, "realization_envelope", realization_envelope) + + @property + def name(self) -> str: + return self.identity.name + + @property + def version(self) -> str: + return self.identity.version + + @property + def compatible_processors(self) -> frozenset[str]: + return self.compatibility.processors + + @property + def provisioner(self) -> ProvisionerCapabilities: + return self.capabilities.provisioner + + @property + def orchestrator(self) -> OrchestratorCapabilities | None: + return self.capabilities.orchestrator + + @property + def evaluator(self) -> EvaluatorCapabilities | None: + return self.capabilities.evaluator + + @property + def participant_runtime(self) -> ParticipantRuntimeCapabilities | None: + return self.capabilities.participant_runtime + + @property + def observation(self) -> ObservationCapabilities | None: + return self.capabilities.observation + + @property + def has_orchestrator(self) -> bool: + return self.orchestrator is not None + + @property + def has_evaluator(self) -> bool: + return self.evaluator is not None + + @property + def has_participant_runtime(self) -> bool: + return self.participant_runtime is not None + + @property + def has_observation(self) -> bool: + return self.observation is not None + + @property + def evaluator_supported_sections(self) -> frozenset[str]: + return self.evaluator.supported_sections if self.evaluator is not None else frozenset() + + @property + def supports_scoring(self) -> bool: + return self.evaluator.supports_scoring if self.evaluator is not None else False + + @property + def supports_objectives(self) -> bool: + return self.evaluator.supports_objectives if self.evaluator is not None else False + + +def _reject_unknown_options(options: _BackendManifestOptions) -> None: + unknown = set(options) - set(_BackendManifestOptions.__annotations__) + if unknown: + names = ", ".join(sorted(unknown)) + raise TypeError(f"BackendManifest got unexpected keyword argument(s): {names}") + + +def _resolve_identity(options: _BackendManifestOptions) -> ApparatusIdentity: + identity = options.get("identity") + if identity is not None: + return identity + name = options.get("name") + if name is None: + raise ValueError("BackendManifest requires either identity or name.") + return ApparatusIdentity(name=name, version=options.get("version", "0.0.0+unknown")) + + +def _resolve_compatibility(options: _BackendManifestOptions) -> BackendCompatibility: + compatibility = options.get("compatibility") + if compatibility is not None: + return compatibility + return BackendCompatibility(processors=frozenset(options.get("compatible_processors", frozenset()))) + + +def _resolve_capabilities(options: _BackendManifestOptions) -> BackendCapabilitySet: + capabilities = options.get("capabilities") + if capabilities is not None: + return capabilities + provisioner = options.get("provisioner") + if provisioner is None: + raise ValueError("BackendManifest requires either capabilities or provisioner.") + return BackendCapabilitySet( + provisioner=provisioner, + orchestrator=options.get("orchestrator"), + evaluator=options.get("evaluator"), + participant_runtime=options.get("participant_runtime"), + observation=options.get("observation"), + ) + + +def _validate_supported_contract_versions(options: _BackendManifestOptions) -> frozenset[str]: + versions = frozenset(options.get("supported_contract_versions", frozenset())) + if not versions: + raise ValueError("BackendManifest.supported_contract_versions must not be empty") + if any(not contract_id.strip() for contract_id in versions): + raise ValueError("BackendManifest.supported_contract_versions must not contain empty strings") + validate_backend_supported_contract_versions(versions) + return versions + + +def _validate_realization_envelope_contract( + supported_contract_versions: frozenset[str], + realization_envelope: BackendRealizationEnvelopeModel | None, +) -> None: + envelope_contract_declared = "realization-envelope-v1" in supported_contract_versions + if realization_envelope is not None and not envelope_contract_declared: + raise ValueError("realization_envelope requires realization-envelope-v1 support") + if envelope_contract_declared and realization_envelope is None: + raise ValueError("realization-envelope-v1 support requires realization_envelope") + + +_T = TypeVar("_T") + + +def _require_non_empty_tuple(values: tuple[_T, ...], field_name: str) -> tuple[_T, ...]: + result = tuple(values) + if not result: + raise ValueError(f"BackendManifest.{field_name} must not be empty") + return result + + +__all__ = ["BackendCompatibility", "BackendManifest"] diff --git a/implementations/python/packages/aces_backend_protocols/capabilities.py b/implementations/python/packages/aces_backend_protocols/capabilities.py index fe6638056..0176998e0 100644 --- a/implementations/python/packages/aces_backend_protocols/capabilities.py +++ b/implementations/python/packages/aces_backend_protocols/capabilities.py @@ -3,16 +3,15 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import TYPE_CHECKING -from aces_contracts.apparatus import ( - ApparatusIdentity, - ConceptBinding, - RealizationSupportDeclaration, -) from aces_contracts.controlled_vocabularies import validate_controlled_vocabulary_scope_values from aces_contracts.manifest_authority import validate_backend_supported_contract_versions from aces_contracts.vocabulary import ParticipantFeatureSupportLevel, WorkflowFeature, WorkflowStatePredicateFeature +if TYPE_CHECKING: + from .backend_manifest import BackendManifest + 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" @@ -144,7 +143,7 @@ class OrchestratorCapabilities: name: str supported_sections: frozenset[str] = frozenset() supports_workflows: bool = False - supports_condition_refs: bool = True + supports_assertion_refs: bool = True supports_inject_bindings: bool = True supported_workflow_features: frozenset[WorkflowFeature] = frozenset() supported_workflow_state_predicates: frozenset[WorkflowStatePredicateFeature] = frozenset() @@ -187,6 +186,12 @@ class EvaluatorCapabilities: supported_sections: frozenset[str] = frozenset() supports_scoring: bool = True supports_objectives: bool = True + supported_predicate_families: frozenset[str] = frozenset() + supported_quantifiers: frozenset[str] = frozenset() + supported_truth_outcomes: frozenset[str] = frozenset() + supported_evidence_channels: frozenset[str] = frozenset() + supported_time_domains: frozenset[str] = frozenset() + preserves_binding_provenance: bool = False constraints: dict[str, str] = field(default_factory=dict) def __post_init__(self) -> None: @@ -202,6 +207,23 @@ def __post_init__(self) -> None: ) if not self.supports_scoring and not self.supports_objectives: raise ValueError("EvaluatorCapabilities must support scoring, objectives, or both") + self._validate_proposition_support() + + def _validate_proposition_support(self) -> None: + proposition_sections = {"propositions", "assertions"} + if not proposition_sections.intersection(self.supported_sections): + return + if not proposition_sections.issubset(self.supported_sections): + raise ValueError("Evaluator proposition support requires both propositions and assertions sections") + if not self.supported_predicate_families or not self.supported_quantifiers: + raise ValueError("Evaluator proposition support requires predicate families and quantifiers") + portable_outcomes = {"true", "false", "unknown", "unsupported"} + if set(self.supported_truth_outcomes) != portable_outcomes: + raise ValueError("Evaluator proposition support requires all portable truth outcomes") + if not self.supported_evidence_channels or not self.supported_time_domains: + raise ValueError("Evaluator proposition support requires evidence channels and time domains") + if not self.preserves_binding_provenance: + raise ValueError("Evaluator proposition support requires binding provenance") def _validate_unique_non_empty_strings(field_name: str, values: tuple[str, ...]) -> None: @@ -411,149 +433,6 @@ class BackendCapabilitySet: observation: ObservationCapabilities | None = None -@dataclass(frozen=True) -class BackendCompatibility: - """Backend compatibility claims against processor surfaces.""" - - processors: frozenset[str] = frozenset() - - def __post_init__(self) -> None: - if not self.processors: - raise ValueError("BackendCompatibility.processors must not be empty") - if any(not processor.strip() for processor in self.processors): - raise ValueError("BackendCompatibility.processors must not contain empty strings") - - -@dataclass(frozen=True, init=False) -class BackendManifest: - """Complete runtime target capability declaration.""" - - identity: ApparatusIdentity - supported_contract_versions: frozenset[str] - compatibility: BackendCompatibility - realization_support: tuple[RealizationSupportDeclaration, ...] - concept_bindings: tuple[ConceptBinding, ...] - constraints: dict[str, str] - capabilities: BackendCapabilitySet - - def __init__( - self, - *, - identity: ApparatusIdentity | None = None, - supported_contract_versions: frozenset[str] = frozenset(), - compatibility: BackendCompatibility | None = None, - realization_support: tuple[RealizationSupportDeclaration, ...] = (), - concept_bindings: tuple[ConceptBinding, ...] = (), - constraints: dict[str, str] | None = None, - capabilities: BackendCapabilitySet | None = None, - name: str | None = None, - version: str = "0.0.0+unknown", - compatible_processors: frozenset[str] = frozenset(), - provisioner: ProvisionerCapabilities | None = None, - orchestrator: OrchestratorCapabilities | None = None, - evaluator: EvaluatorCapabilities | None = None, - participant_runtime: ParticipantRuntimeCapabilities | None = None, - observation: ObservationCapabilities | None = None, - ) -> None: - if identity is None: - if name is None: - raise ValueError("BackendManifest requires either identity or name.") - identity = ApparatusIdentity(name=name, version=version) - if compatibility is None: - compatibility = BackendCompatibility(processors=frozenset(compatible_processors)) - if capabilities is None: - if provisioner is None: - raise ValueError("BackendManifest requires either capabilities or provisioner.") - capabilities = BackendCapabilitySet( - provisioner=provisioner, - orchestrator=orchestrator, - evaluator=evaluator, - participant_runtime=participant_runtime, - observation=observation, - ) - supported_contract_versions = frozenset(supported_contract_versions) - if not supported_contract_versions: - raise ValueError("BackendManifest.supported_contract_versions must not be empty") - if any(not version.strip() for version in supported_contract_versions): - raise ValueError("BackendManifest.supported_contract_versions must not contain empty strings") - validate_backend_supported_contract_versions(supported_contract_versions) - realization_support = tuple(realization_support) - if not realization_support: - raise ValueError("BackendManifest.realization_support must not be empty") - concept_bindings = tuple(concept_bindings) - if not concept_bindings: - raise ValueError("BackendManifest.concept_bindings must not be empty") - object.__setattr__(self, "identity", identity) - object.__setattr__(self, "supported_contract_versions", supported_contract_versions) - object.__setattr__(self, "compatibility", compatibility) - object.__setattr__(self, "realization_support", realization_support) - object.__setattr__(self, "concept_bindings", concept_bindings) - object.__setattr__(self, "constraints", {} if constraints is None else dict(constraints)) - object.__setattr__(self, "capabilities", capabilities) - - @property - def name(self) -> str: - return self.identity.name - - @property - def version(self) -> str: - return self.identity.version - - @property - def compatible_processors(self) -> frozenset[str]: - return self.compatibility.processors - - @property - def provisioner(self) -> ProvisionerCapabilities: - return self.capabilities.provisioner - - @property - def orchestrator(self) -> OrchestratorCapabilities | None: - return self.capabilities.orchestrator - - @property - def evaluator(self) -> EvaluatorCapabilities | None: - return self.capabilities.evaluator - - @property - def participant_runtime(self) -> ParticipantRuntimeCapabilities | None: - return self.capabilities.participant_runtime - - @property - def observation(self) -> ObservationCapabilities | None: - return self.capabilities.observation - - @property - def has_orchestrator(self) -> bool: - return self.orchestrator is not None - - @property - def has_evaluator(self) -> bool: - return self.evaluator is not None - - @property - def has_participant_runtime(self) -> bool: - return self.participant_runtime is not None - - @property - def has_observation(self) -> bool: - return self.observation is not None - - @property - def evaluator_supported_sections(self) -> frozenset[str]: - if self.evaluator is None: - return frozenset() - return self.evaluator.supported_sections - - @property - def supports_scoring(self) -> bool: - return self.evaluator.supports_scoring if self.evaluator is not None else False - - @property - def supports_objectives(self) -> bool: - return self.evaluator.supports_objectives if self.evaluator is not None else False - - def participant_runtime_capability_contract_gaps(manifest: BackendManifest) -> tuple[str, ...]: """Return missing contract surfaces for declared standard API-405 claims. @@ -598,3 +477,13 @@ def observation_capability_contract_gaps(manifest: BackendManifest) -> tuple[str if missing: gaps.append(f"capabilities.observation missing required contracts: {', '.join(missing)}") return tuple(gaps) + + +def __getattr__(name: str) -> object: + """Preserve the historical manifest imports without a circular import.""" + + if name in {"BackendCompatibility", "BackendManifest"}: + from . import backend_manifest + + return getattr(backend_manifest, name) + raise AttributeError(name) diff --git a/implementations/python/packages/aces_backend_protocols/manifest.py b/implementations/python/packages/aces_backend_protocols/manifest.py index 77839ff68..2d168af4f 100644 --- a/implementations/python/packages/aces_backend_protocols/manifest.py +++ b/implementations/python/packages/aces_backend_protocols/manifest.py @@ -18,6 +18,29 @@ from .capabilities import BackendManifest +def _evaluator_capability_payload(manifest: BackendManifest) -> dict[str, Any] | None: + evaluator = manifest.evaluator + if evaluator is None: + return None + payload: dict[str, Any] = { + "name": evaluator.name, + "supported_sections": sorted(evaluator.supported_sections), + "supports_scoring": evaluator.supports_scoring, + "supports_objectives": evaluator.supports_objectives, + "constraints": dict(evaluator.constraints), + } + if {"propositions", "assertions"}.issubset(evaluator.supported_sections): + payload.update( + supported_predicate_families=sorted(evaluator.supported_predicate_families), + supported_quantifiers=sorted(evaluator.supported_quantifiers), + supported_truth_outcomes=sorted(evaluator.supported_truth_outcomes), + supported_evidence_channels=sorted(evaluator.supported_evidence_channels), + supported_time_domains=sorted(evaluator.supported_time_domains), + preserves_binding_provenance=evaluator.preserves_binding_provenance, + ) + return payload + + def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Model: """Render a backend manifest as the authoritative v2 contract model.""" @@ -45,6 +68,11 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod ) for declaration in manifest.realization_support ], + realization_envelope=( + manifest.realization_envelope.identity.model_dump(mode="json") + if manifest.realization_envelope is not None + else None + ), concept_bindings=[ ConceptBindingEntryModel(scope=binding.scope, family=binding.family) for binding in manifest.concept_bindings @@ -67,7 +95,7 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod "name": manifest.orchestrator.name, "supported_sections": sorted(manifest.orchestrator.supported_sections), "supports_workflows": manifest.orchestrator.supports_workflows, - "supports_condition_refs": manifest.orchestrator.supports_condition_refs, + "supports_assertion_refs": manifest.orchestrator.supports_assertion_refs, "supports_inject_bindings": manifest.orchestrator.supports_inject_bindings, "supported_workflow_features": sorted( feature for feature in manifest.orchestrator.supported_workflow_features @@ -80,17 +108,7 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod if manifest.orchestrator is not None else None ), - "evaluator": ( - { - "name": manifest.evaluator.name, - "supported_sections": sorted(manifest.evaluator.supported_sections), - "supports_scoring": manifest.evaluator.supports_scoring, - "supports_objectives": manifest.evaluator.supports_objectives, - "constraints": dict(manifest.evaluator.constraints), - } - if manifest.evaluator is not None - else None - ), + "evaluator": _evaluator_capability_payload(manifest), "participant_runtime": ( { "name": manifest.participant_runtime.name, @@ -136,4 +154,7 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod def backend_manifest_payload(manifest: BackendManifest) -> dict[str, Any]: """Render a backend manifest as JSON-ready data.""" - return backend_manifest_v2_model(manifest).model_dump(mode="json") + payload = backend_manifest_v2_model(manifest).model_dump(mode="json") + if payload.get("realization_envelope") is None: + payload.pop("realization_envelope", None) + return payload diff --git a/implementations/python/packages/aces_backend_protocols/naming.py b/implementations/python/packages/aces_backend_protocols/naming.py new file mode 100644 index 000000000..a2bfa3904 --- /dev/null +++ b/implementations/python/packages/aces_backend_protocols/naming.py @@ -0,0 +1,36 @@ +"""Deterministic provider-name derivation from compiler-owned addresses.""" + +from __future__ import annotations + +import hashlib +import re + +from aces_contracts.addressing import require_compiled_address + +_UNSAFE_PROVIDER_NAME = re.compile(r"[^a-z0-9_.-]+", re.ASCII) +_DIGEST_LENGTH = 12 + + +def provider_resource_name( + address: str, + *, + prefix: str = "", + maximum_length: int = 63, +) -> str: + """Return a bounded readable name whose suffix commits to *address*.""" + + require_compiled_address(address) + if isinstance(maximum_length, bool) or not isinstance(maximum_length, int) or maximum_length < 16: + raise ValueError("provider resource name maximum_length must be an integer >= 16") + digest = hashlib.sha256(address.encode("utf-8")).hexdigest()[:_DIGEST_LENGTH] + readable = _UNSAFE_PROVIDER_NAME.sub("-", address.lower()).strip("-._") or "resource" + safe_prefix = _UNSAFE_PROVIDER_NAME.sub("-", prefix.lower()).strip("-._") + if safe_prefix: + readable = f"{safe_prefix}-{readable}" + suffix = f"-{digest}" + readable_budget = maximum_length - len(suffix) + head = readable[:readable_budget].rstrip("-._") or "resource"[:readable_budget] + return f"{head}{suffix}" + + +__all__ = ["provider_resource_name"] diff --git a/implementations/python/packages/aces_backend_stubs/evaluation_support.py b/implementations/python/packages/aces_backend_stubs/evaluation_support.py new file mode 100644 index 000000000..a34ff4581 --- /dev/null +++ b/implementations/python/packages/aces_backend_stubs/evaluation_support.py @@ -0,0 +1,90 @@ +"""Evaluation-operation support for the in-memory reference stub.""" + +from aces_contracts.planning import ChangeAction, EvaluationOp, RuntimeDomain +from aces_contracts.runtime_state import SnapshotEntry +from aces_contracts.versions import EVALUATION_STATE_SCHEMA_VERSION + + +def _result_payload(op: EvaluationOp, now: str) -> dict[str, object]: + result_contract = op.payload.get("result_contract", {}) + resource_type = str(result_contract.get("resource_type", op.resource_type)) + payload: dict[str, object] = { + "state_schema_version": result_contract.get("state_schema_version", EVALUATION_STATE_SCHEMA_VERSION), + "resource_type": resource_type, + "run_id": "evaluation-run", + "status": "ready", + "observed_at": now, + "updated_at": now, + "detail": f"stub result for {op.address}", + "evidence_refs": [], + } + if result_contract.get("supports_score"): + fixed_max_score = result_contract.get("fixed_max_score") + payload["score"] = fixed_max_score if fixed_max_score is not None else 100 + payload["max_score"] = fixed_max_score if fixed_max_score is not None else 100 + if result_contract.get("supports_passed"): + payload["passed"] = True + return payload + + +def _history(result: dict[str, object], now: str) -> list[dict[str, object]]: + return [ + { + "event_type": "evaluation_started", + "timestamp": now, + "status": "running", + "passed": None, + "score": None, + "max_score": None, + "detail": None, + "evidence_refs": [], + "details": {}, + }, + { + "event_type": "evaluation_ready", + "timestamp": now, + "status": "ready", + "passed": result.get("passed"), + "score": result.get("score"), + "max_score": result.get("max_score"), + "detail": result.get("detail"), + "evidence_refs": list(result.get("evidence_refs", [])), + "details": {}, + }, + ] + + +def apply_evaluation_operation( + op: EvaluationOp, + entries: dict[str, SnapshotEntry], + results: dict[str, dict[str, object]], + history: dict[str, list[dict[str, object]]], + changed_addresses: list[str], + now: str, +) -> None: + """Apply one evaluation plan operation to mutable stub state.""" + + if op.action == ChangeAction.DELETE: + entries.pop(op.address, None) + results.pop(op.address, None) + history.pop(op.address, None) + changed_addresses.append(op.address) + return + entries[op.address] = SnapshotEntry( + address=op.address, + domain=RuntimeDomain.EVALUATION, + resource_type=op.resource_type, + payload=op.payload, + ordering_dependencies=op.ordering_dependencies, + refresh_dependencies=op.refresh_dependencies, + status="admitted" if op.resource_type in {"proposition", "assertion"} else "evaluating", + ) + if op.resource_type not in {"proposition", "assertion"}: + result = _result_payload(op, now) + results[op.address] = result + history[op.address] = _history(result, now) + if op.action != ChangeAction.UNCHANGED: + changed_addresses.append(op.address) + + +__all__ = ["apply_evaluation_operation"] diff --git a/implementations/python/packages/aces_backend_stubs/stubs.py b/implementations/python/packages/aces_backend_stubs/stubs.py index 1365aa895..4ef5a80f6 100644 --- a/implementations/python/packages/aces_backend_stubs/stubs.py +++ b/implementations/python/packages/aces_backend_stubs/stubs.py @@ -25,11 +25,12 @@ from aces_contracts.manifest_authority import BACKEND_SUPPORTED_CONTRACT_IDS from aces_contracts.planning import ChangeAction, EvaluationPlan, OrchestrationPlan, ProvisioningPlan, RuntimeDomain from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot, SnapshotEntry -from aces_contracts.versions import EVALUATION_STATE_SCHEMA_VERSION from aces_contracts.vocabulary import RealizationSupportMode from aces_runtime.registry import RuntimeTarget, RuntimeTargetComponents -REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS = BACKEND_SUPPORTED_CONTRACT_IDS +from .evaluation_support import apply_evaluation_operation + +REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS = frozenset(BACKEND_SUPPORTED_CONTRACT_IDS) - {"realization-envelope-v1"} REFERENCE_PARTICIPANT_ROLES = frozenset( PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_ROLE_SCOPE] ) @@ -165,7 +166,7 @@ def create_stub_manifest( name="stub-orchestrator", supported_sections=frozenset({"injects", "events", "scripts", "stories", "workflows"}), supports_workflows=True, - supports_condition_refs=True, + supports_assertion_refs=True, supports_inject_bindings=True, supported_workflow_features=frozenset( { @@ -189,9 +190,15 @@ def create_stub_manifest( ), evaluator=EvaluatorCapabilities( name="stub-evaluator", - supported_sections=frozenset({"conditions", "objectives"}), + supported_sections=frozenset({"conditions", "propositions", "assertions", "objectives"}), supports_scoring=True, supports_objectives=True, + supported_predicate_families=frozenset({"presence", "boolean", "string", "number"}), + supported_quantifiers=frozenset({"all", "any", "at_least"}), + supported_truth_outcomes=frozenset({"true", "false", "unknown", "unsupported"}), + supported_evidence_channels=frozenset({"log", "api_response", "file_artifact"}), + supported_time_domains=frozenset({"scenario_time"}), + preserves_binding_provenance=True, ), participant_runtime=( ParticipantRuntimeCapabilities( @@ -424,69 +431,7 @@ def start( history = {address: list(events) for address, events in snapshot.evaluation_history.items()} now = datetime.now(UTC).isoformat().replace("+00:00", "Z") for op in plan.operations: - if op.action == ChangeAction.DELETE: - entries.pop(op.address, None) - results.pop(op.address, None) - history.pop(op.address, None) - changed_addresses.append(op.address) - continue - entries[op.address] = SnapshotEntry( - address=op.address, - domain=RuntimeDomain.EVALUATION, - resource_type=op.resource_type, - payload=op.payload, - ordering_dependencies=op.ordering_dependencies, - refresh_dependencies=op.refresh_dependencies, - status="evaluating", - ) - result_contract = op.payload.get("result_contract", {}) - resource_type = str(result_contract.get("resource_type", op.resource_type)) - result_payload: dict[str, object] = { - "state_schema_version": result_contract.get( - "state_schema_version", - EVALUATION_STATE_SCHEMA_VERSION, - ), - "resource_type": resource_type, - "run_id": "evaluation-run", - "status": "ready", - "observed_at": now, - "updated_at": now, - "detail": f"stub result for {op.address}", - "evidence_refs": [], - } - if result_contract.get("supports_score"): - fixed_max_score = result_contract.get("fixed_max_score") - result_payload["score"] = fixed_max_score if fixed_max_score is not None else 100 - result_payload["max_score"] = fixed_max_score if fixed_max_score is not None else 100 - if result_contract.get("supports_passed"): - result_payload["passed"] = True - results[op.address] = result_payload - history[op.address] = [ - { - "event_type": "evaluation_started", - "timestamp": now, - "status": "running", - "passed": None, - "score": None, - "max_score": None, - "detail": None, - "evidence_refs": [], - "details": {}, - }, - { - "event_type": "evaluation_ready", - "timestamp": now, - "status": "ready", - "passed": result_payload.get("passed"), - "score": result_payload.get("score"), - "max_score": result_payload.get("max_score"), - "detail": result_payload.get("detail"), - "evidence_refs": list(result_payload.get("evidence_refs", [])), - "details": {}, - }, - ] - if op.action != ChangeAction.UNCHANGED: - changed_addresses.append(op.address) + apply_evaluation_operation(op, entries, results, history, changed_addresses, now) self._running = bool(plan.resources) self._startup_order = list(plan.startup_order) self._results = results diff --git a/implementations/python/packages/aces_cli/libvirt.py b/implementations/python/packages/aces_cli/libvirt.py index af2a79933..80db5852b 100644 --- a/implementations/python/packages/aces_cli/libvirt.py +++ b/implementations/python/packages/aces_cli/libvirt.py @@ -4,11 +4,14 @@ from datetime import UTC, datetime from pathlib import Path +from urllib.parse import urlsplit import typer from aces_operations.libvirt_evidence_run import LibvirtEvidenceRunConfig, run_libvirt_evidence_run from aces_operations.techvault_live import TechVaultLiveConfig, validate_techvault_live +_DEFAULT_CONNECTION_URI = "qemu:///system" + app = typer.Typer(help="Libvirt backend operations.") techvault_app = typer.Typer(help="TechVault operational scenario checks.") app.add_typer(techvault_app, name="techvault") @@ -20,6 +23,20 @@ scenario and write a live-gate archive under the output directory. """ +_GUEST_WARNING = """\ +This will boot a guest-observing libvirt/QEMU appliance for the selected +scenario through the production apply path, read realization facts back from +inside the guest, and write a machine-readable scenario-evidence artifact under +the output directory. Native resources are created and then torn down. +""" + + +def _noncredential_connection_uri(value: str) -> str: + parsed = urlsplit(value) + if parsed.username is not None or parsed.password is not None: + raise typer.BadParameter("connection URI must not contain credentials") + return value + @techvault_app.command("validate-live") def validate_live( @@ -46,22 +63,11 @@ def validate_live( help="Skip the native libvirt resource confirmation prompt.", ), connection_uri: str = typer.Option( - "qemu:///system", + _DEFAULT_CONNECTION_URI, "--connection-uri", + callback=_noncredential_connection_uri, help="libvirt connection URI.", ), - appliance_memory_mib: int = typer.Option( - 128, - "--appliance-memory-mib", - min=64, - help="Memory per generated TechVault appliance VM.", - ), - boot_timeout_seconds: int = typer.Option( - 180, - "--boot-timeout-seconds", - min=1, - help="Maximum native appliance readiness wait.", - ), ) -> None: """Boot TechVault through native ACES/libvirt and run the live validation gate.""" @@ -77,8 +83,50 @@ def validate_live( run_id=resolved_run_id, config=TechVaultLiveConfig( connection_uri=connection_uri, - appliance_memory_mib=appliance_memory_mib, - boot_timeout_seconds=boot_timeout_seconds, + ), + ) + typer.echo(report.render()) + if not report.passed: + raise typer.Exit(code=1) + + +@techvault_app.command("guest-certify") +def guest_certify( + scenario: Path = typer.Option( + Path("examples/scenarios/techvault-guest-certified.sdl.yaml"), + "--scenario", + help="ACES SDL scenario to boot and certify from inside the guest.", + ), + project_dir: Path = typer.Option( + Path("."), + "--project-dir", + "--output-dir", + help="Output directory for the guest-certified scenario-evidence artifact.", + ), + run_id: str | None = typer.Option(None, "--run-id", help="Run id for the evidence artifact."), + yes: bool = typer.Option(False, "--yes", "-y", help="Skip the native libvirt resource confirmation prompt."), + connection_uri: str = typer.Option( + _DEFAULT_CONNECTION_URI, + "--connection-uri", + callback=_noncredential_connection_uri, + help="libvirt connection URI.", + ), +) -> None: + """Boot a guest-observing appliance and emit the guest-certified evidence artifact.""" + + if not yes: + typer.echo(_GUEST_WARNING) + 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") + report = run_libvirt_evidence_run( + scenario_path=scenario.resolve(), + project_dir=project_dir.resolve(), + run_id=resolved_run_id, + config=LibvirtEvidenceRunConfig( + evidence_source_mode="guest-certified", + connection_uri=connection_uri, ), ) typer.echo(report.render()) @@ -110,8 +158,9 @@ def validate_evidence( help="Realize the libvirt substrate natively (requires a libvirt daemon); default is deterministic.", ), connection_uri: str = typer.Option( - "qemu:///system", + _DEFAULT_CONNECTION_URI, "--connection-uri", + callback=_noncredential_connection_uri, help="libvirt connection URI (native-live only).", ), ) -> None: diff --git a/implementations/python/packages/aces_cli/main.py b/implementations/python/packages/aces_cli/main.py index 7abcbda4e..6d932959b 100644 --- a/implementations/python/packages/aces_cli/main.py +++ b/implementations/python/packages/aces_cli/main.py @@ -24,7 +24,9 @@ def _version_callback(value: bool) -> None: try: current_version = version("aces-sdl") except PackageNotFoundError: - current_version = "0.1.0" + # Honest PEP 440 not-installed sentinel (GOV-901): do not report a + # plausible-looking release when the distribution is absent. + current_version = "0.0.0+unknown" typer.echo(f"aces {current_version}") raise typer.Exit() diff --git a/implementations/python/packages/aces_cli/sdl.py b/implementations/python/packages/aces_cli/sdl.py index 2a89fde45..cd0bd4b95 100644 --- a/implementations/python/packages/aces_cli/sdl.py +++ b/implementations/python/packages/aces_cli/sdl.py @@ -6,6 +6,7 @@ from pathlib import Path import typer +from aces_sdl import SDLParseError, format_sdl_source from aces_sdl.module_registry import ( LOCKFILE_NAME, load_lockfile, @@ -17,6 +18,42 @@ app = typer.Typer(help="SDL composition and packaging.") +@app.command("format") +def format_source( + path: Path = typer.Argument(..., exists=True, readable=True), + write: bool = typer.Option(False, "--write", help="Replace the source file with canonical SDL YAML."), + check: bool = typer.Option(False, "--check", help="Fail when the source is not already canonical."), +) -> None: + """Migrate recognized legacy syntax and emit canonical sdl-yaml/v1.""" + if write and check: + raise typer.BadParameter("--write and --check are mutually exclusive") + try: + original = path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + raise typer.BadParameter("SDL source must be valid UTF-8") from exc + try: + result = format_sdl_source(original, path=path) + except SDLParseError as exc: + raise typer.BadParameter(exc.details) from exc + + for diagnostic in result.diagnostics: + start = diagnostic.primary_range.start + typer.echo( + f"{path}:{start.line}:{start.column}: {diagnostic.severity} [{diagnostic.code}] {diagnostic.message}", + err=True, + ) + if check: + if result.content != original: + typer.echo(f"{path}: not canonical", err=True) + raise typer.Exit(code=1) + return + if write: + path.write_text(result.content, encoding="utf-8") + typer.echo(str(path)) + return + typer.echo(result.content, nl=False) + + @app.command("resolve") def resolve( path: Path = typer.Argument(..., exists=True, readable=True), diff --git a/implementations/python/packages/aces_conformance/conformance.py b/implementations/python/packages/aces_conformance/conformance.py index 558e7b3d0..2262adafc 100644 --- a/implementations/python/packages/aces_conformance/conformance.py +++ b/implementations/python/packages/aces_conformance/conformance.py @@ -23,14 +23,18 @@ load_backend_profile_from_path, ) from aces_contracts.contracts import ( + AssociatedArtifactManifestModel, BackendManifestV2Model, EvaluationHistoryEventModel, EvaluationPlanModel, EvaluationResultStateModel, + ExperimentApparatusContextModel, ExperimentCaptureSpecModel, ExperimentDerivedMeasureModel, ExperimentEvidenceRecordModel, ExperimentRunModel, + ExperimentSpecModel, + ExperimentStudyModel, OperationReceiptModel, OperationStatusModel, OrchestrationPlanModel, @@ -60,7 +64,12 @@ ) from aces_contracts.participant_shared_state import iter_participant_shared_state_snapshot_violations from aces_contracts.planning import ProvisioningPlan, RuntimeDomain +from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel from aces_contracts.runtime_state import RuntimeSnapshot, RuntimeSnapshotEnvelope, SnapshotEntry +from aces_contracts.scientific_completeness import ( + ScientificCompletenessAssessmentModel, + ScientificCompletenessTaxonomyModel, +) from aces_contracts.workflow import WorkflowExecutionState from aces_processor.models import ( ParticipantActionContractRuntime, @@ -95,14 +104,14 @@ ) _RUN_REFINEMENT_CONCERN_KINDS = frozenset({"capture-window", "measurement-channel"}) -# Default reference scenario the target-conformance live probe drives when the +# Default reference scenario the target-conformance adapter probe drives when the # caller supplies none. Backend-neutral: a single generic linux vm node. # # Issue #663 makes this a *default*, not a universal assumption. A fixed-topology # emulation or bounded simulation backend that cannot realize this generic # scenario supplies one it can realize via -# ``run_target_conformance(reference_scenario=...)``; the live probe then holds -# it to full realization of *that* scenario. This is a temporary runner-parameter +# ``run_target_conformance(reference_scenario=...)``; the target probe then holds +# it to adapter-level accounting for *that* scenario. This runner-parameter # bridge — superseded by the realizability-envelope design (#667) and the # scenario/envelope subsumption relation (#668), which will let the probe # negotiate an in-envelope witness instead of carrying a default at all. @@ -117,13 +126,27 @@ conditions: {health: ops} roles: {ops: operator} conditions: - health: {command: /bin/true, interval: 15} + health: {proposition: health-state, command: /bin/true, interval: 15} entities: blue: {role: blue} + propositions: + health-state: + description: The conformance VM is declared in the admitted scenario. + subjects: [nodes.vm] + basis: declared_state + predicate: + kind: presence + property: node + semantic_ref: urn:aces:declared-property:node + operator: exists + assertions: + health: + proposition: health-state + role: postcondition objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -131,7 +154,7 @@ run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """ ) @@ -237,6 +260,17 @@ class BackendConformanceReport: "experiment-run-v1": ExperimentRunModel.model_validate, } +_STRUCTURAL_ONLY_VALIDATORS = { + "associated-artifact-manifest-v1": AssociatedArtifactManifestModel.model_validate, + "experiment-apparatus-context-v1": ExperimentApparatusContextModel.model_validate, + "experiment-authoring-input-v1": ExperimentSpecModel.model_validate, + "experiment-study-v1": ExperimentStudyModel.model_validate, + "realization-envelope-v1": BackendRealizationEnvelopeModel.model_validate, + "scientific-completeness-assessment-v1": ScientificCompletenessAssessmentModel.model_validate, + "scientific-completeness-taxonomy-v1": ScientificCompletenessTaxonomyModel.model_validate, +} +_SEMANTIC_CONTEXT_REQUIRED_CONTRACTS = frozenset({"associated-artifact-manifest-v1"}) + _EVENT_STREAM_VALIDATORS: dict[str, tuple[type, str]] = { "workflow-history-event-stream-v1": (WorkflowHistoryEventModel, "workflow"), @@ -432,7 +466,7 @@ def _validate_event_stream( def _validate_payload(contract_name: str, payload: Any) -> list[Diagnostic]: diagnostics: list[Diagnostic] = [] - validator = _MODEL_VALIDATORS.get(contract_name) + validator = _MODEL_VALIDATORS.get(contract_name) or _STRUCTURAL_ONLY_VALIDATORS.get(contract_name) if validator is not None: try: validator(payload) @@ -985,6 +1019,12 @@ def _semantic_diagnostics(contract_name: str, payload: Any) -> list[Diagnostic]: return _runtime_snapshot_semantic_diagnostics(payload) +def validate_contract_payload(contract_name: str, payload: object) -> tuple[Diagnostic, ...]: + """Validate one payload through the registered structural contract boundary.""" + + return tuple(_validate_payload(contract_name, payload)) + + def _fixture_case_diagnostics(contract_name: str, payload: object) -> list[Diagnostic]: """Schema-gate first; only run semantic analysis on a schema-valid payload. @@ -997,6 +1037,19 @@ def _fixture_case_diagnostics(contract_name: str, payload: object) -> list[Diagn schema_diagnostics = _validate_payload(contract_name, payload) if schema_diagnostics: return schema_diagnostics + if contract_name in _SEMANTIC_CONTEXT_REQUIRED_CONTRACTS: + return [ + Diagnostic( + code="conformance.semantic-context-required", + domain="conformance", + address="#", + message=( + "full associated-artifact conformance requires a concrete parent and bounded byte readers; " + "the generic fixture runner establishes structural validity only" + ), + severity=Severity.ERROR, + ) + ] return _semantic_diagnostics(contract_name, payload) @@ -1076,7 +1129,7 @@ def profile_for_manifest(manifest: BackendManifest) -> BackendCapabilityProfile: A backend that declares orchestrator, evaluator, AND participant runtime capabilities is treated as ``FULL_REMOTE_CONTROL_PLANE``, so the default ``run_target_conformance`` path automatically - validates the live target against the participant-episode contract + validates the active target against the participant-episode contract family (RUN-311). Backends that only declare orchestrator/evaluator fall back to ``ORCHESTRATION_EVALUATION``; orchestrator-only declarations fall back to ``ORCHESTRATION_CAPABLE``; provisioner-only @@ -1184,14 +1237,14 @@ def run_target_conformance( if any(diag.code == "conformance.profile-load-failed" for diag in fixture_report.diagnostics): # The published profile is the contract set we are supposed to validate # against. With no profile loaded we must NOT mutate the backend via - # ``_live_target_cases`` — there is nothing to validate against. The + # ``_target_adapter_cases`` — there is nothing to validate against. The # fixture report already carries the structured profile-load # diagnostic and ``passed=False``; surface it as the conformance # result for this target. return fixture_report if _to_known_profile(effective_profile) is None: # Target conformance enforces runtime-surface gates (which capability - # roles the target must implement, which live probes to run). Those + # roles the target must implement, which target probes to run). Those # gates depend on knowing the profile's runtime surface contract. For # an unknown profile id we have NO runtime-surface authority — letting # the run continue would silently certify a target that's missing every @@ -1228,7 +1281,7 @@ def run_target_conformance( participant_claim_gaps = participant_runtime_capability_contract_gaps(target.manifest) observation_claim_gaps = observation_capability_contract_gaps(target.manifest) claim_gaps = (*participant_claim_gaps, *observation_claim_gaps) - capability_gaps = tuple((*surface_gaps, *claim_gaps)) + capability_gaps = (*surface_gaps, *claim_gaps) passed = fixture_report.passed and not contract_gaps and not capability_gaps diagnostics = list(fixture_report.diagnostics) if contract_gaps: @@ -1256,9 +1309,9 @@ def run_target_conformance( + "; ".join(claim_gaps), ) ) - live_cases = _live_target_cases(target, effective_profile, reference_scenario=reference_scenario) - cases = tuple((*fixture_report.cases, *live_cases)) - passed = passed and all(case.passed for case in live_cases) + target_cases = _target_adapter_cases(target, effective_profile, reference_scenario=reference_scenario) + cases = (*fixture_report.cases, *target_cases) + passed = passed and all(case.passed for case in target_cases) return BackendConformanceReport( profile=_to_profile_id(effective_profile), passed=passed, @@ -1445,7 +1498,7 @@ def _provisioning_probe_case( ) ) return ConformanceCaseResult( - name="live-provisioning", + name="target-provisioning", contract_name="operation-status-v1", valid=True, passed=not diagnostics, @@ -1523,7 +1576,7 @@ def _live_snapshot_case(control_plane: RuntimeControlPlane) -> ConformanceCaseRe ) ) return ConformanceCaseResult( - name="live-snapshot", + name="target-snapshot", contract_name="runtime-snapshot-v1", valid=True, passed=not diagnostics, @@ -1531,27 +1584,27 @@ def _live_snapshot_case(control_plane: RuntimeControlPlane) -> ConformanceCaseRe ) -def _live_target_cases( +def _target_adapter_cases( target: RuntimeTarget, profile: BackendProfileSelector, *, reference_scenario: ScenarioInput | None = None, ) -> tuple[ConformanceCaseResult, ...]: - """Run live probes appropriate for known runtime surfaces only. + """Run target-adapter probes appropriate for known runtime surfaces only. Every known profile requires a provisioner, so target conformance always - runs a backend-neutral provisioning probe that proves real snapshot - mutation (issue #606) — provisioning-only backends included, which must not + runs a backend-neutral provisioning probe that proves adapter-driven snapshot + mutation (issue #606), not daemon or guest observation — provisioning-only backends included, which must not pass on manifest validation alone. Orchestration, evaluation, and the participant-episode probe additionally run for the richer runtime surfaces that declare those roles. For an unknown profile id we run only the - universally-safe manifest validation case and skip the live probes, since + universally-safe manifest validation case and skip the target probes, since their runtime contract is not known to this implementation. The probe drives ``reference_scenario`` when supplied, else ``_DEFAULT_CONFORMANCE_SCENARIO`` (issue #663). Whichever scenario is - selected is held to full realization (the #606 mutation guard is - unchanged); the parameter only stops the probe assuming *every* backend can + selected is held to adapter-level operation accounting (the #606 mutation + guard is unchanged); the parameter only stops the probe assuming *every* backend can realize one hard-coded scenario. """ @@ -1560,7 +1613,7 @@ def _live_target_cases( manifest_diags = _validate_payload("backend-manifest-v2", manifest_payload) cases.append( ConformanceCaseResult( - name="live-manifest", + name="target-manifest", contract_name="backend-manifest-v2", valid=True, passed=not manifest_diags, diff --git a/implementations/python/packages/aces_contracts/addressing.py b/implementations/python/packages/aces_contracts/addressing.py new file mode 100644 index 000000000..d59ca79ae --- /dev/null +++ b/implementations/python/packages/aces_contracts/addressing.py @@ -0,0 +1,65 @@ +"""Wire contract for processor-owned runtime addresses.""" + +from __future__ import annotations + +import re +from typing import Annotated, Any + +from pydantic import AfterValidator, WithJsonSchema + +COMPILED_ADDRESS_MAX_LENGTH = 2048 +_ADDRESS_SEGMENT = r"(?:[a-z0-9][a-z0-9_-]{0,63}|__private)" +COMPILED_ADDRESS_PATTERN = rf"{_ADDRESS_SEGMENT}(?:\.{_ADDRESS_SEGMENT})+" +_COMPILED_ADDRESS_RE = re.compile(COMPILED_ADDRESS_PATTERN, re.ASCII) +COMPILED_ADDRESS_JSON_SCHEMA: dict[str, Any] = { + "type": "string", + "minLength": 3, + "maxLength": COMPILED_ADDRESS_MAX_LENGTH, + "pattern": rf"^{COMPILED_ADDRESS_PATTERN}$", + "not": {"pattern": "[^a-z0-9_.-]"}, +} +PLAN_ADDRESS_ROOT_BY_DOMAIN = { + "provisioning": "provision", + "orchestration": "orchestration", + "evaluation": "evaluation", +} +PLAN_RESOURCE_TYPES_BY_DOMAIN = { + "provisioning": frozenset({"network", "node", "feature-binding", "content-placement", "account-placement"}), + "orchestration": frozenset({"inject-binding", "inject", "event", "script", "story", "workflow"}), + "evaluation": frozenset({"condition-binding", "objective"}), +} + + +def require_compiled_address(value: object, *, field_name: str = "address") -> str: + if ( + not isinstance(value, str) + or len(value) > COMPILED_ADDRESS_MAX_LENGTH + or _COMPILED_ADDRESS_RE.fullmatch(value) is None + ): + raise ValueError(f"{field_name} must be a canonical compiled address") + return value + + +def _validate_compiled_address(value: str) -> str: + return require_compiled_address(value) + + +CompiledAddress = Annotated[ + str, + AfterValidator(_validate_compiled_address), + WithJsonSchema(COMPILED_ADDRESS_JSON_SCHEMA), +] + + +def render_compiled_address(*parts: str) -> str: + address = ".".join(part for part in parts if part) + return require_compiled_address(address) + + +__all__ = [ + "COMPILED_ADDRESS_JSON_SCHEMA", + "COMPILED_ADDRESS_MAX_LENGTH", + "CompiledAddress", + "render_compiled_address", + "require_compiled_address", +] diff --git a/implementations/python/packages/aces_contracts/associated_artifacts.py b/implementations/python/packages/aces_contracts/associated_artifacts.py new file mode 100644 index 000000000..f575bade9 --- /dev/null +++ b/implementations/python/packages/aces_contracts/associated_artifacts.py @@ -0,0 +1,338 @@ +"""Canonical identity and bounded byte binding for associated artifacts.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping +from dataclasses import dataclass +from typing import BinaryIO, cast + +import rfc8785 +from aces_sdl import canonical_sdl_digest +from aces_sdl.scenario import Scenario +from blake3 import blake3 + +from .contracts import ( + AssociatedArtifactManifestModel, + ExperimentApparatusContextModel, + ExperimentArtifactRefModel, + ExperimentRunModel, + ExperimentSpecModel, + ExperimentStudyModel, + ExperimentTaskModel, +) +from .diagnostics import Diagnostic, Severity + +_DOMAIN = "associated-artifact" +_CHUNK_SIZE = 64 * 1024 +_ARTIFACTS_ADDRESS = "#/artifacts" + +JSONValue = None | bool | int | float | str | list["JSONValue"] | dict[str, "JSONValue"] + + +@dataclass(frozen=True) +class AssociatedArtifactValidationLimits: + """Caller policy limits for one manifest validation.""" + + max_artifacts: int = 1024 + max_artifact_bytes: int = 1024 * 1024 * 1024 + max_total_bytes: int = 4 * 1024 * 1024 * 1024 + + def __post_init__(self) -> None: + if self.max_artifacts < 1 or self.max_artifact_bytes < 0 or self.max_total_bytes < 0: + raise ValueError("associated-artifact validation limits must be non-negative and allow an artifact") + + +def _normalized_canonical_value(value: JSONValue) -> JSONValue: + if isinstance(value, dict): + normalized = {key: _normalized_canonical_value(item) for key, item in value.items()} + checksum = normalized.get("checksum") + if isinstance(checksum, dict) and isinstance(checksum.get("value"), str): + checksum["value"] = checksum["value"].casefold() + if isinstance(normalized.get("ref_digest"), str): + normalized["ref_digest"] = normalized["ref_digest"].casefold() + return normalized + if isinstance(value, list): + return [_normalized_canonical_value(item) for item in value] + return value + + +def associated_artifact_set_bytes(manifest: AssociatedArtifactManifestModel) -> bytes: + """Return RFC 8785 bytes for the abstract parent-plus-reference set.""" + + projection = { + "profile": manifest.canonicalization_profile, + "scope": manifest.scope, + "parent_ref": manifest.parent_ref.model_dump(mode="json", exclude_none=True), + "artifacts": { + artifact_id: artifact.model_dump(mode="json", exclude_none=True) + for artifact_id, artifact in manifest.artifacts.items() + }, + } + try: + return rfc8785.dumps(_normalized_canonical_value(projection)) + except rfc8785.CanonicalizationError as exc: + raise ValueError("associated-artifact set canonicalization failed") from exc + + +def associated_artifact_set_digest(manifest: AssociatedArtifactManifestModel) -> str: + """Derive the v1 lowercase SHA-256 identity for a manifest's artifact set.""" + + digest = hashlib.sha256(associated_artifact_set_bytes(manifest)).hexdigest() + return f"sha256:{digest}" + + +def _reject_duplicate_members(pairs: list[tuple[str, JSONValue]]) -> dict[str, JSONValue]: + result: dict[str, JSONValue] = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON member {key!r}") + result[key] = value + return result + + +def load_associated_artifact_manifest_json(source: str | bytes | bytearray) -> AssociatedArtifactManifestModel: + """Parse one manifest while rejecting duplicate JSON members before construction.""" + + payload = json.loads(source, object_pairs_hook=_reject_duplicate_members) + return AssociatedArtifactManifestModel.model_validate(payload) + + +def _diagnostic(code: str, address: str, message: str) -> Diagnostic: + return Diagnostic(code=code, domain=_DOMAIN, address=address, message=message, severity=Severity.ERROR) + + +def _scenario_parent_matches(manifest: AssociatedArtifactManifestModel, parent: object) -> bool: + reference = manifest.parent_ref + matches = isinstance(parent, Scenario) and parent.name == reference.ref_id + if matches and reference.ref_kind == "scenario-snapshot": + matches = reference.ref_version is None or parent.version == reference.ref_version + if matches and reference.ref_digest is not None: + matches = canonical_sdl_digest(parent).value.casefold() == reference.ref_digest.casefold() + return matches + + +def _experiment_parent_matches(manifest: AssociatedArtifactManifestModel, parent: object) -> bool: + reference = manifest.parent_ref + parent_shapes: dict[str, tuple[type[object], str, str]] = { + "task": (ExperimentTaskModel, "task_id", "task_version"), + "authoring-input": (ExperimentSpecModel, "spec_id", "spec_version"), + "apparatus-context": (ExperimentApparatusContextModel, "apparatus_context_id", "context_version"), + "run": (ExperimentRunModel, "run_id", "run_version"), + "study": (ExperimentStudyModel, "study_id", "study_version"), + } + expected_type, id_field, version_field = parent_shapes[reference.ref_kind] + matches = isinstance(parent, expected_type) and getattr(parent, id_field) == reference.ref_id + if matches and reference.ref_version is not None: + matches = getattr(parent, version_field) == reference.ref_version + return matches + + +def _parent_matches(manifest: AssociatedArtifactManifestModel, parent: object) -> bool: + if manifest.parent_ref.ref_kind in {"scenario", "scenario-snapshot"}: + return _scenario_parent_matches(manifest, parent) + return _experiment_parent_matches(manifest, parent) + + +def _read_and_hash(reader: BinaryIO, algorithm: str, declared_size: int) -> tuple[int, str] | None: + if algorithm == "blake3": + digest = blake3() + else: + try: + digest = hashlib.new(algorithm) + except ValueError: + return None + total = 0 + while total <= declared_size: + chunk = reader.read(min(_CHUNK_SIZE, declared_size + 1 - total)) + if not isinstance(chunk, bytes): + raise TypeError("artifact reader must return bytes") + if not chunk: + break + digest.update(chunk) + total += len(chunk) + return total, digest.hexdigest() + + +def _resource_limit_diagnostics( + manifest: AssociatedArtifactManifestModel, + limits: AssociatedArtifactValidationLimits, +) -> tuple[Diagnostic, ...]: + diagnostics: list[Diagnostic] = [] + if len(manifest.artifacts) > limits.max_artifacts: + diagnostics.append( + _diagnostic( + "associated-artifact.resource-limit-exceeded", + _ARTIFACTS_ADDRESS, + "artifact count exceeds the caller-supplied validation limit", + ) + ) + declared_total = sum(artifact.size_bytes for artifact in manifest.artifacts.values()) + oversized = [ + artifact_id + for artifact_id, artifact in manifest.artifacts.items() + if artifact.size_bytes > limits.max_artifact_bytes + ] + if oversized or declared_total > limits.max_total_bytes: + diagnostics.append( + _diagnostic( + "associated-artifact.resource-limit-exceeded", + _ARTIFACTS_ADDRESS, + "declared artifact bytes exceed the caller-supplied validation limits", + ) + ) + return tuple(diagnostics) + + +def _identity_diagnostics(manifest: AssociatedArtifactManifestModel, parent: object) -> tuple[Diagnostic, ...]: + diagnostics: list[Diagnostic] = [] + if not _parent_matches(manifest, parent): + diagnostics.append( + _diagnostic( + "associated-artifact.parent-mismatch", + "#/parent_ref", + "the supplied concrete parent does not match parent_ref", + ) + ) + if associated_artifact_set_digest(manifest) != manifest.set_digest: + diagnostics.append( + _diagnostic( + "associated-artifact.set-digest-mismatch", + "#/set_digest", + "set_digest does not match the canonical parent-plus-artifact-reference set", + ) + ) + return tuple(diagnostics) + + +def _binding_presence_diagnostics( + manifest_ids: set[str], + supplied_ids: set[str], +) -> tuple[Diagnostic, ...]: + diagnostics: list[Diagnostic] = [] + for artifact_id in sorted(manifest_ids - supplied_ids): + diagnostics.append( + _diagnostic( + "associated-artifact.payload-binding-missing", + f"#/artifacts/{artifact_id}", + "no concrete byte reader was supplied for this artifact", + ) + ) + for artifact_id in sorted(supplied_ids - manifest_ids): + diagnostics.append( + _diagnostic( + "associated-artifact.payload-binding-unexpected", + _ARTIFACTS_ADDRESS, + f"a byte reader was supplied for undeclared artifact id {artifact_id!r}", + ) + ) + return tuple(diagnostics) + + +def _read_payload( + artifact_id: str, + artifact: ExperimentArtifactRefModel, + reader: object, +) -> tuple[tuple[int, str] | None, Diagnostic | None]: + address = f"#/artifacts/{artifact_id}" + result: tuple[int, str] | None = None + diagnostic: Diagnostic | None = None + if not hasattr(reader, "read"): + diagnostic = _diagnostic( + "associated-artifact.payload-binding-invalid", + address, + "the supplied binding is not a concrete byte reader", + ) + else: + try: + result = _read_and_hash(cast(BinaryIO, reader), artifact.checksum.algorithm, artifact.size_bytes) + except (OSError, TypeError, ValueError): + diagnostic = _diagnostic( + "associated-artifact.payload-binding-invalid", + address, + "the concrete byte reader failed without yielding a valid bounded byte stream", + ) + if result is None and diagnostic is None: + diagnostic = _diagnostic( + "associated-artifact.checksum-algorithm-unsupported", + f"{address}/checksum/algorithm", + "the checksum algorithm is unavailable to this validator", + ) + return result, diagnostic + + +def _payload_diagnostics( + artifact_id: str, + artifact: ExperimentArtifactRefModel, + reader: object, +) -> tuple[Diagnostic, ...]: + address = f"#/artifacts/{artifact_id}" + result, read_diagnostic = _read_payload(artifact_id, artifact, reader) + if read_diagnostic is not None: + return (read_diagnostic,) + + diagnostics: list[Diagnostic] = [] + assert result is not None + actual_size, actual_checksum = result + if actual_size != artifact.size_bytes: + diagnostics.append( + _diagnostic( + "associated-artifact.payload-size-mismatch", + f"{address}/size_bytes", + "concrete payload size does not match size_bytes", + ) + ) + if actual_checksum.casefold() != artifact.checksum.value.casefold(): + diagnostics.append( + _diagnostic( + "associated-artifact.payload-checksum-mismatch", + f"{address}/checksum", + "concrete payload bytes do not match the declared checksum", + ) + ) + return tuple(diagnostics) + + +def validate_associated_artifact_manifest( + manifest: AssociatedArtifactManifestModel, + *, + parent: object, + artifact_readers: Mapping[str, BinaryIO], + limits: AssociatedArtifactValidationLimits | None = None, +) -> tuple[Diagnostic, ...]: + """Validate parent/set identity and every payload through bounded readers. + + The caller acquires and immutably stages payloads. This function performs no + URI fetching, directory traversal, archive extraction, or ambient lookup. + """ + + limit_diagnostics = _resource_limit_diagnostics( + manifest, + limits or AssociatedArtifactValidationLimits(), + ) + if limit_diagnostics: + return limit_diagnostics + + diagnostics = list(_identity_diagnostics(manifest, parent)) + manifest_ids = set(manifest.artifacts) + supplied_ids = set(artifact_readers) + diagnostics.extend(_binding_presence_diagnostics(manifest_ids, supplied_ids)) + for artifact_id in sorted(manifest_ids & supplied_ids): + diagnostics.extend( + _payload_diagnostics( + artifact_id, + manifest.artifacts[artifact_id], + artifact_readers[artifact_id], + ) + ) + return tuple(diagnostics) + + +__all__ = [ + "AssociatedArtifactValidationLimits", + "associated_artifact_set_bytes", + "associated_artifact_set_digest", + "load_associated_artifact_manifest_json", + "validate_associated_artifact_manifest", +] diff --git a/implementations/python/packages/aces_contracts/contracts.py b/implementations/python/packages/aces_contracts/contracts.py index 4e3ffb215..bdad0dff4 100644 --- a/implementations/python/packages/aces_contracts/contracts.py +++ b/implementations/python/packages/aces_contracts/contracts.py @@ -6,12 +6,17 @@ import json import re from collections.abc import Mapping +from copy import deepcopy from datetime import UTC, datetime, timedelta +from enum import Enum from functools import lru_cache from typing import Annotated, Any, Literal +from urllib.parse import parse_qsl, urlsplit from aces_sdl import VARIABLE_TOKEN_PATTERN +from aces_sdl.canonical import InstantiatedScenarioSnapshot from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance +from aces_sdl.identifiers import PORTABLE_IDENTIFIER_JSON_SCHEMA, QUALIFIED_IDENTIFIER_MAX_LENGTH from aces_sdl.observability_plane_semantics import classify_contract_plane from aces_sdl.participant_attribution_semantics import ( ParticipantAttributionCandidateKind, @@ -33,10 +38,12 @@ ParticipantTimeDomain, ) from aces_sdl.scenario import InstantiatedScenario, Scenario +from aces_sdl.schema_catalogs import HASHMAP_SECTIONS, RUNTIME_SERVICE_FAMILIES, RuntimeReferenceChild from pydantic import BaseModel, ConfigDict, Field, GetJsonSchemaHandler, StrictInt, model_validator from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema +from .addressing import COMPILED_ADDRESS_JSON_SCHEMA, CompiledAddress from .corpus import CONCEPT_AUTHORITY, corpus_family_root from .manifest_authority import ( BACKEND_SUPPORTED_CONTRACT_IDS, @@ -58,7 +65,14 @@ ParticipantRuntimeLifecyclePhase, participant_lifecycle_field_violation_messages, ) +from .planning import ( + PLAN_ADDRESS_ROOT_BY_DOMAIN, + PLAN_RESOURCE_TYPES_BY_DOMAIN, + RuntimeDomain, + require_plan_operation_identity, +) from .versions import ( + ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION, ATLAS_TACTICS_SOURCE_SCHEMA_VERSION, ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION, BACKEND_MANIFEST_V2_SCHEMA_VERSION, @@ -66,6 +80,7 @@ CONTROLLED_VOCABULARIES_SCHEMA_VERSION, EVALUATION_STATE_SCHEMA_VERSION, EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION, + EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION, EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION, EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION, EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION, @@ -77,6 +92,7 @@ PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION, PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION, PROCESSOR_MANIFEST_V2_SCHEMA_VERSION, + PROPOSITION_TRUTH_RESULT_SCHEMA_VERSION, REFERENCE_MODELS_SCHEMA_VERSION, REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION, RUNTIME_SNAPSHOT_SCHEMA_VERSION, @@ -430,6 +446,15 @@ def _extend_reported_value_status_schema(json_schema: JsonSchemaValue) -> None: _DEFS_KEY = "$defs" _INSTANTIATION_INVARIANT_CONTRACT_ID = "instantiated-scenario-v1" +_INSTANTIATED_SNAPSHOT_CONTRACT_ID = "instantiated-scenario-snapshot-v1" +_SDL_AUTHORING_CONTRACT_ID = "sdl-authoring-input-v1" +_SDL_IDENTIFIER_CONTRACT_IDS = frozenset( + { + _SDL_AUTHORING_CONTRACT_ID, + _INSTANTIATION_INVARIANT_CONTRACT_ID, + _INSTANTIATED_SNAPSHOT_CONTRACT_ID, + } +) _SCHEMA_MAP_KEYS = ("properties", "patternProperties", _DEFS_KEY) _SCHEMA_SUBSCHEMA_KEYS = ( "additionalProperties", @@ -442,6 +467,147 @@ def _extend_reported_value_status_schema(json_schema: JsonSchemaValue) -> None: ) +def _portable_property_names(*, maximum: int = 64) -> dict[str, Any]: + schema = deepcopy(PORTABLE_IDENTIFIER_JSON_SCHEMA) + schema["maxLength"] = maximum + return schema + + +def _qualified_property_names(*, local_maximum: int = 64) -> dict[str, Any]: + local_tail = local_maximum - 1 + segment = r"(?:[a-z0-9][a-z0-9_-]{0,63}|__private)" + return { + "type": "string", + "minLength": 1, + "maxLength": QUALIFIED_IDENTIFIER_MAX_LENGTH, + "pattern": rf"^(?:{segment}\.)*[a-z0-9][a-z0-9_-]{{0,{local_tail}}}$", + "not": {"pattern": "[^a-z0-9_.-]"}, + } + + +def _resolve_local_ref(schema: dict[str, Any], node: object) -> dict[str, Any] | None: + if not isinstance(node, dict): + return None + ref = node.get("$ref") + if isinstance(ref, str) and ref.startswith("#/$defs/"): + resolved = schema.get(_DEFS_KEY, {}).get(ref.removeprefix("#/$defs/")) + return resolved if isinstance(resolved, dict) else None + return node + + +def _collection_item_schema( + schema: dict[str, Any], owner: dict[str, Any], collection_name: str +) -> dict[str, Any] | None: + collection = owner.get("properties", {}).get(collection_name) + collection = _resolve_local_ref(schema, collection) + if not isinstance(collection, dict): + return None + items = collection.get("items") + return _resolve_local_ref(schema, items) + + +def _constrain_runtime_children( + schema: dict[str, Any], + owner: dict[str, Any], + children: tuple[RuntimeReferenceChild, ...], +) -> None: + for child in children: + child_schema = _collection_item_schema(schema, owner, child.collection_name) + if child_schema is None: + continue + child_schema.setdefault("properties", {})[child.id_field] = _portable_property_names() + _constrain_runtime_children(schema, child_schema, child.children) + + +def _attach_runtime_identifier_constraints(schema: dict[str, Any]) -> None: + runtime = schema.get(_DEFS_KEY, {}).get("RuntimeConfiguration") + if not isinstance(runtime, dict): + return + for family in RUNTIME_SERVICE_FAMILIES: + item_schema = _collection_item_schema(schema, runtime, family.collection_name) + if item_schema is None: + continue + if family.collection_name != "forwarding_agents": + item_schema.setdefault("properties", {})[family.id_field] = _portable_property_names() + _constrain_runtime_children(schema, item_schema, family.child_refs) + + +def _constrain_collection_item_field( + owner: dict[str, Any], + collection_name: str, + field_name: str, + field_schema: dict[str, Any], +) -> None: + collection = owner.get("properties", {}).get(collection_name) + if not isinstance(collection, dict): + return + items = collection.get("items") + if not isinstance(items, dict): + return + collection["items"] = { + "allOf": [ + items, + {"properties": {field_name: field_schema}, "type": "object"}, + ] + } + + +def _attach_instantiation_request_identifier_constraints(schema: dict[str, Any]) -> None: + parameters = schema.get("properties", {}).get("parameters") + if isinstance(parameters, dict): + parameters["propertyNames"] = _portable_property_names() + + +def _attach_sdl_identifier_constraints(contract_id: str, schema: dict[str, Any]) -> None: + if contract_id == "scenario-instantiation-request-v1": + _attach_instantiation_request_identifier_constraints(schema) + elif contract_id in _SDL_IDENTIFIER_CONTRACT_IDS: + _attach_scenario_identifier_constraints(contract_id, schema) + + +def _scenario_schema_for_identifier_constraints( + contract_id: str, + schema: dict[str, Any], +) -> dict[str, Any] | None: + if contract_id != _INSTANTIATED_SNAPSHOT_CONTRACT_ID: + return schema + nested = schema.get(_DEFS_KEY, {}).get("InstantiatedScenario") + return nested if isinstance(nested, dict) else None + + +def _attach_scenario_identifier_constraints(contract_id: str, schema: dict[str, Any]) -> None: + scenario_schema = _scenario_schema_for_identifier_constraints(contract_id, schema) + if scenario_schema is None: + return + qualified = contract_id != _SDL_AUTHORING_CONTRACT_ID + for section_name in HASHMAP_SECTIONS: + section = scenario_schema.get("properties", {}).get(section_name) + if not isinstance(section, dict): + continue + local_maximum = 35 if section_name == "nodes" else 64 + section["propertyNames"] = ( + _qualified_property_names(local_maximum=local_maximum) + if qualified + else _portable_property_names(maximum=local_maximum) + ) + _attach_runtime_identifier_constraints(schema) + forwarding_id_schema = _qualified_property_names() if qualified else _portable_property_names() + _constrain_collection_item_field( + scenario_schema, + "forwarding_agents", + "forwarding_agent_id", + forwarding_id_schema, + ) + runtime = schema.get(_DEFS_KEY, {}).get("RuntimeConfiguration") + if isinstance(runtime, dict): + _constrain_collection_item_field( + runtime, + "forwarding_agents", + "forwarding_agent_id", + _portable_property_names(), + ) + + def _apply_string_token_constraint(node: dict[str, Any]) -> None: """Forbid the ``${name}`` token on a single free string subschema. @@ -491,16 +657,16 @@ def _forbid_variable_tokens_in_strings(node: object) -> None: def _attach_instantiation_invariants(contract_id: str, json_schema: dict[str, Any]) -> None: - """Differentiate the instantiated-scenario contract from authoring-input. - - The authoring (``Scenario``) and instantiated (``InstantiatedScenario``) - models share every field, so their generated schemas are identical apart - from metadata. An instantiated scenario is fully concrete, so the - instantiated schema additionally forbids unresolved ``${var}`` tokens in - string values — both whole-string placeholders and embedded tokens (issue - #500). The matching model-level invariant lives on ``InstantiatedScenario``. + """Apply the no-substitution-token invariant to concrete SDL artifacts. + + An instantiated scenario is fully concrete, so its payload and canonical + snapshot forbid unresolved ``${var}`` tokens in string values. The matching + model-level invariant lives on ``InstantiatedScenario``. """ - if contract_id != _INSTANTIATION_INVARIANT_CONTRACT_ID: + if contract_id not in { + _INSTANTIATION_INVARIANT_CONTRACT_ID, + _INSTANTIATED_SNAPSHOT_CONTRACT_ID, + }: return _forbid_variable_tokens_in_strings(json_schema) @@ -516,6 +682,39 @@ def _attach_json_schema_metadata(contract_id: str, json_schema: dict[str, Any]) json_schema.setdefault("$id", _schema_id_for_contract_id(contract_id)) +def _attach_compiled_address_map_constraints(contract_id: str, json_schema: dict[str, Any]) -> None: + if contract_id != "runtime-snapshot-v1": + return + entries = json_schema.get("properties", {}).get("entries") + if not isinstance(entries, dict): + return + entries["propertyNames"] = deepcopy(COMPILED_ADDRESS_JSON_SCHEMA) + entries["additionalProperties"] = False + + +_PLAN_CONTRACT_DOMAIN = { + "provisioning-plan-v1": RuntimeDomain.PROVISIONING, + "orchestration-plan-v1": RuntimeDomain.ORCHESTRATION, + "evaluation-plan-v1": RuntimeDomain.EVALUATION, +} + + +def _attach_plan_identity_constraints(contract_id: str, json_schema: dict[str, Any]) -> None: + domain = _PLAN_CONTRACT_DOMAIN.get(contract_id) + if domain is None: + return + operation = json_schema.get(_DEFS_KEY, {}).get("PlanOperationModel") + if not isinstance(operation, dict): + return + properties = operation.get("properties", {}) + address = properties.get("address") + resource_type = properties.get("resource_type") + if isinstance(address, dict): + address.setdefault("allOf", []).append({"pattern": rf"^{PLAN_ADDRESS_ROOT_BY_DOMAIN[domain]}\."}) + if isinstance(resource_type, dict): + resource_type["enum"] = sorted(PLAN_RESOURCE_TYPES_BY_DOMAIN[domain]) + + _SEMANTIC_PROFILE_PHASE_ALLOWED_BINDING_SCOPES = { "authoring": frozenset(), "exchange": frozenset(), @@ -729,6 +928,238 @@ class EvaluationHistoryEventModel(ContractModel): details: dict[str, Any] = Field(default_factory=dict) +class PropositionTruthOutcome(str, Enum): + """Portable proposition-evaluation outcome domain. + + ``unsupported`` is a realization disposition included in the portable + envelope, not a logical truth value. + """ + + TRUE = "true" + FALSE = "false" + UNKNOWN = "unknown" + UNSUPPORTED = "unsupported" + + +class PropositionEvaluationBasis(str, Enum): + DECLARED_STATE = "declared_state" + OBSERVED_STATE = "observed_state" + + +class PropositionAssertionPolarity(str, Enum): + POSITIVE = "positive" + NEGATIVE = "negative" + + +class PropositionIndeterminacyReason(str, Enum): + MISSING_EVIDENCE = "missing_evidence" + STALE_EVIDENCE = "stale_evidence" + PARTIAL_EVIDENCE = "partial_evidence" + CONFLICTING_EVIDENCE = "conflicting_evidence" + REDACTED_EVIDENCE = "redacted_evidence" + LOSSY_EVIDENCE = "lossy_evidence" + PROBE_FAILURE = "probe_failure" + + +class PropositionLossKind(str, Enum): + MISSING = "missing" + STALE = "stale" + PARTIAL = "partial" + CONFLICTING = "conflicting" + REDACTED = "redacted" + LOSSY = "lossy" + PROBE_FAILURE = "probe_failure" + + +class PropositionProbeBindingModel(ContractModel): + """Provenance for one capability-bound proposition realization.""" + + binding_id: NonEmptyString + implementation_id: NonEmptyString + implementation_version: NonEmptyString + artifact_digest: Annotated[str, Field(pattern=r"^sha256:[0-9a-f]{64}$")] + backend_manifest_ref: NonEmptyString + proposition_address: NonEmptyString + capability_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_unique_capabilities(self) -> PropositionProbeBindingModel: + if len(set(self.capability_refs)) != len(self.capability_refs): + raise ValueError("probe binding capability_refs must be unique") + return self + + +class PropositionTemporalContextModel(ContractModel): + """Governed boundary and clock context used for one evaluation.""" + + boundary_ref: NonEmptyString + time_domain: NonEmptyString + clock_authority: NonEmptyString + + +class PropositionLossDisclosureModel(ContractModel): + kind: PropositionLossKind + within_admissible_bound: bool + + +class PropositionTruthResultModel(ContractModel): + """Evidence-bearing truth result separate from evaluator lifecycle state.""" + + schema_version: Literal[PROPOSITION_TRUTH_RESULT_SCHEMA_VERSION] = PROPOSITION_TRUTH_RESULT_SCHEMA_VERSION + result_id: NonEmptyString + proposition_address: NonEmptyString + assertion_address: NonEmptyString + assertion_polarity: PropositionAssertionPolarity + proposition_outcome: PropositionTruthOutcome + assertion_outcome: PropositionTruthOutcome + evaluation_basis: PropositionEvaluationBasis + indeterminacy_reason: PropositionIndeterminacyReason | None = None + probe_binding: PropositionProbeBindingModel | None = None + evidence_refs: list[NonEmptyString] = Field(default_factory=list) + declared_artifact_digest: Annotated[str, Field(pattern=r"^sha256:[0-9a-f]{64}$")] | None = None + temporal_context: PropositionTemporalContextModel | None = None + loss_disclosures: list[PropositionLossDisclosureModel] = Field(default_factory=list) + unsupported_capability_refs: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_truth_result(self) -> PropositionTruthResultModel: + self._validate_unique_refs() + self._validate_polarity() + self._validate_outcome_explanation() + self._validate_basis_evidence() + self._validate_loss_bounds() + return self + + def _validate_unique_refs(self) -> None: + for field_name in ("evidence_refs", "unsupported_capability_refs"): + refs = getattr(self, field_name) + if len(set(refs)) != len(refs): + raise ValueError(f"{field_name} must be unique") + + def _validate_polarity(self) -> None: + expected = self.proposition_outcome + if self.assertion_polarity is PropositionAssertionPolarity.NEGATIVE: + if expected is PropositionTruthOutcome.TRUE: + expected = PropositionTruthOutcome.FALSE + elif expected is PropositionTruthOutcome.FALSE: + expected = PropositionTruthOutcome.TRUE + if self.assertion_outcome is not expected: + raise ValueError( + f"{self.assertion_polarity.value} assertion outcome must preserve or invert the proposition outcome" + ) + + def _validate_outcome_explanation(self) -> None: + if self.proposition_outcome is PropositionTruthOutcome.UNKNOWN: + if self.indeterminacy_reason is None: + raise ValueError("unknown proposition outcome requires indeterminacy_reason") + elif self.indeterminacy_reason is not None: + raise ValueError("indeterminacy_reason is valid only for unknown outcomes") + if self.proposition_outcome is PropositionTruthOutcome.UNSUPPORTED: + if not self.unsupported_capability_refs: + raise ValueError("unsupported outcome requires unsupported_capability_refs") + elif self.unsupported_capability_refs: + raise ValueError("unsupported_capability_refs are valid only for unsupported outcomes") + + def _validate_basis_evidence(self) -> None: + decided = self.proposition_outcome in {PropositionTruthOutcome.TRUE, PropositionTruthOutcome.FALSE} + if decided and self.evaluation_basis is PropositionEvaluationBasis.OBSERVED_STATE: + self._validate_observed_evidence() + if self.probe_binding is not None and self.probe_binding.proposition_address != self.proposition_address: + raise ValueError("probe_binding proposition_address must match proposition_address") + if ( + decided + and self.evaluation_basis is PropositionEvaluationBasis.DECLARED_STATE + and self.declared_artifact_digest is None + ): + raise ValueError("declared-state decided truth requires declared_artifact_digest") + + def _validate_observed_evidence(self) -> None: + if self.probe_binding is None: + raise ValueError("observed-state decided truth requires probe_binding") + if not self.evidence_refs: + raise ValueError("observed-state decided truth requires evidence_refs") + if self.temporal_context is None: + raise ValueError("observed-state decided truth requires temporal_context") + + def _validate_loss_bounds(self) -> None: + decided = self.proposition_outcome in {PropositionTruthOutcome.TRUE, PropositionTruthOutcome.FALSE} + if decided and any(not disclosure.within_admissible_bound for disclosure in self.loss_disclosures): + raise ValueError("lossy or partial evidence outside the admitted bound cannot decide truth") + + def semantic_claim(self) -> tuple[object, ...]: + """Return the backend-independent portion used for equivalence checks.""" + + return ( + self.proposition_address, + self.assertion_address, + self.assertion_polarity, + self.proposition_outcome, + self.assertion_outcome, + self.evaluation_basis, + self.indeterminacy_reason, + self.temporal_context, + tuple(self.loss_disclosures), + tuple(self.unsupported_capability_refs), + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + schema = handler(core_schema) + schema = handler.resolve_ref_schema(schema) + decided = {"enum": ["true", "false"]} + loss_items = deepcopy(schema["properties"]["loss_disclosures"]["items"]) + loss_items["properties"] = {"within_admissible_bound": {"const": True}} + loss_items["required"] = ["within_admissible_bound"] + schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": { + "evaluation_basis": {"const": "observed_state"}, + "proposition_outcome": decided, + }, + "required": ["evaluation_basis", "proposition_outcome"], + }, + "then": { + "properties": { + "probe_binding": {"not": {"type": "null"}}, + "evidence_refs": {"minItems": 1}, + "temporal_context": {"not": {"type": "null"}}, + } + }, + }, + { + "if": { + "properties": {"proposition_outcome": {"const": "unknown"}}, + "required": ["proposition_outcome"], + }, + "then": {"properties": {"indeterminacy_reason": {"not": {"type": "null"}}}}, + "else": {"properties": {"indeterminacy_reason": {"type": "null"}}}, + }, + { + "if": { + "properties": {"proposition_outcome": {"const": "unsupported"}}, + "required": ["proposition_outcome"], + }, + "then": {"properties": {"unsupported_capability_refs": {"minItems": 1}}}, + "else": {"properties": {"unsupported_capability_refs": {"maxItems": 0}}}, + }, + { + "if": { + "properties": {"proposition_outcome": decided}, + "required": ["proposition_outcome"], + }, + "then": {"properties": {"loss_disclosures": {"items": loss_items}}}, + }, + ] + ) + return schema + + class ParticipantEpisodeStateModel(ContractModel): state_schema_version: Literal[PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION] = PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION participant_address: str @@ -1947,37 +2378,91 @@ def __get_pydantic_json_schema__( class PlanOperationModel(ContractModel): action: str - address: str + address: CompiledAddress resource_type: str payload: dict[str, Any] = Field(default_factory=dict) - ordering_dependencies: list[str] = Field(default_factory=list) - refresh_dependencies: list[str] = Field(default_factory=list) + ordering_dependencies: list[CompiledAddress] = Field(default_factory=list) + refresh_dependencies: list[CompiledAddress] = Field(default_factory=list) + + +def _require_unique_operation_addresses(operations: list[PlanOperationModel]) -> None: + addresses = [operation.address for operation in operations] + if len(addresses) != len(set(addresses)): + raise ValueError("Plan operation addresses must be unique") + + +def _require_operation_identities(operations: list[PlanOperationModel], domain: RuntimeDomain) -> None: + for operation in operations: + require_plan_operation_identity(domain, operation.address, operation.resource_type) + + +def _require_startup_order_addresses( + operations: list[PlanOperationModel], + startup_order: list[str], +) -> None: + if len(startup_order) != len(set(startup_order)): + raise ValueError("Plan startup_order addresses must be unique") + operation_addresses = {operation.address for operation in operations} + unknown = set(startup_order) - operation_addresses + if unknown: + raise ValueError("Plan startup_order must reference admitted operation addresses") + + +class RealizationEnvelopeIdentityModel(ContractModel): + """Immutable realization-envelope identity carried across runtime contracts.""" + + contract_id: Literal["realization-envelope-v1"] = "realization-envelope-v1" + envelope_id: NonEmptyString + schema_version: Literal["realization-envelope/v1"] = "realization-envelope/v1" + digest: Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] + configuration_digest: Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] class ProvisioningPlanModel(ContractModel): operations: list[PlanOperationModel] = Field(default_factory=list) diagnostics: list[dict[str, Any]] = Field(default_factory=list) + realization_envelope: RealizationEnvelopeIdentityModel | None = None + + @model_validator(mode="after") + def _validate_operation_addresses(self) -> ProvisioningPlanModel: + _require_unique_operation_addresses(self.operations) + _require_operation_identities(self.operations, RuntimeDomain.PROVISIONING) + return self class OrchestrationPlanModel(ContractModel): operations: list[PlanOperationModel] = Field(default_factory=list) - startup_order: list[str] = Field(default_factory=list) + startup_order: list[CompiledAddress] = Field(default_factory=list) diagnostics: list[dict[str, Any]] = Field(default_factory=list) + @model_validator(mode="after") + def _validate_operation_addresses(self) -> OrchestrationPlanModel: + _require_unique_operation_addresses(self.operations) + _require_operation_identities(self.operations, RuntimeDomain.ORCHESTRATION) + _require_startup_order_addresses(self.operations, self.startup_order) + return self + class EvaluationPlanModel(ContractModel): operations: list[PlanOperationModel] = Field(default_factory=list) - startup_order: list[str] = Field(default_factory=list) + startup_order: list[CompiledAddress] = Field(default_factory=list) diagnostics: list[dict[str, Any]] = Field(default_factory=list) + @model_validator(mode="after") + def _validate_operation_addresses(self) -> EvaluationPlanModel: + _require_unique_operation_addresses(self.operations) + _require_operation_identities(self.operations, RuntimeDomain.EVALUATION) + _require_startup_order_addresses(self.operations, self.startup_order) + return self + class SnapshotEntryModel(ContractModel): - address: str + address: CompiledAddress domain: str resource_type: str payload: dict[str, Any] = Field(default_factory=dict) - ordering_dependencies: list[str] = Field(default_factory=list) - refresh_dependencies: list[str] = Field(default_factory=list) + ordering_dependencies: list[CompiledAddress] = Field(default_factory=list) + refresh_dependencies: list[CompiledAddress] = Field(default_factory=list) status: str = "ready" @@ -2014,11 +2499,12 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): """ schema_version: Literal[RUNTIME_SNAPSHOT_SCHEMA_VERSION] = RUNTIME_SNAPSHOT_SCHEMA_VERSION - entries: dict[str, SnapshotEntryModel] = Field(default_factory=dict) + entries: dict[CompiledAddress, SnapshotEntryModel] = Field(default_factory=dict) orchestration_results: dict[str, WorkflowExecutionStateModel] = Field(default_factory=dict) orchestration_history: dict[str, list[WorkflowHistoryEventModel]] = Field(default_factory=dict) evaluation_results: dict[str, EvaluationResultStateModel] = Field(default_factory=dict) evaluation_history: dict[str, list[EvaluationHistoryEventModel]] = Field(default_factory=dict) + proposition_truth_results: dict[str, PropositionTruthResultModel] = Field(default_factory=dict) 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) @@ -2027,8 +2513,16 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): joint_action_records: dict[str, ParticipantJointActionRecordModel] = Field(default_factory=dict) time_management_contexts: dict[str, ParticipantTimeManagementContextModel] = Field(default_factory=dict) realization_provenance: list[RealizationProvenanceEntryModel] = Field(default_factory=list) + realization_envelope: RealizationEnvelopeIdentityModel | None = None metadata: dict[str, Any] = Field(default_factory=dict) + @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") + return self + class OperationReceiptModel(ContractModel): schema_version: Literal[OPERATION_SCHEMA_VERSION] = OPERATION_SCHEMA_VERSION @@ -2047,7 +2541,13 @@ class OperationStatusModel(ContractModel): submitted_at: str updated_at: str diagnostics: list[dict[str, Any]] = Field(default_factory=list) - changed_addresses: list[str] = Field(default_factory=list) + changed_addresses: list[CompiledAddress] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_changed_addresses(self) -> OperationStatusModel: + if len(self.changed_addresses) != len(set(self.changed_addresses)): + raise ValueError("changed addresses must be unique") + return self class ProvisionerCapabilitiesModel(ContractModel): @@ -2123,7 +2623,7 @@ class OrchestratorCapabilitiesModel(ContractModel): name: NonEmptyString supported_sections: list[NonEmptyString] = Field(min_length=1) supports_workflows: bool = False - supports_condition_refs: bool = True + supports_assertion_refs: bool = True supports_inject_bindings: bool = True supported_workflow_features: list[WorkflowFeature] = Field(default_factory=list) supported_workflow_state_predicates: list[WorkflowStatePredicateFeature] = Field(default_factory=list) @@ -2195,6 +2695,12 @@ class EvaluatorCapabilitiesModel(ContractModel): supported_sections: list[NonEmptyString] = Field(min_length=1) supports_scoring: bool = True supports_objectives: bool = True + supported_predicate_families: list[NonEmptyString] = Field(default_factory=list) + supported_quantifiers: list[NonEmptyString] = Field(default_factory=list) + supported_truth_outcomes: list[NonEmptyString] = Field(default_factory=list) + supported_evidence_channels: list[NonEmptyString] = Field(default_factory=list) + supported_time_domains: list[NonEmptyString] = Field(default_factory=list) + preserves_binding_provenance: bool = False constraints: dict[str, str] = Field(default_factory=dict) @model_validator(mode="after") @@ -2205,6 +2711,33 @@ def _validate_evaluator_support(self) -> EvaluatorCapabilitiesModel: ) if not self.supports_scoring and not self.supports_objectives: raise ValueError("evaluators must support scoring, objectives, or both") + for field_name in ( + "supported_predicate_families", + "supported_quantifiers", + "supported_truth_outcomes", + "supported_evidence_channels", + "supported_time_domains", + ): + values = getattr(self, field_name) + if len(set(values)) != len(values): + raise ValueError(f"evaluator {field_name} must be unique") + proposition_sections = {"propositions", "assertions"} + if proposition_sections.intersection(self.supported_sections): + if not proposition_sections.issubset(self.supported_sections): + raise ValueError("evaluator proposition support requires propositions and assertions") + if set(self.supported_truth_outcomes) != {"true", "false", "unknown", "unsupported"}: + raise ValueError("evaluator proposition support requires all portable truth outcomes") + if not all( + ( + self.supported_predicate_families, + self.supported_quantifiers, + self.supported_evidence_channels, + self.supported_time_domains, + ) + ): + raise ValueError("evaluator proposition support requires typed capability dimensions") + if not self.preserves_binding_provenance: + raise ValueError("evaluator proposition support requires binding provenance") return self @classmethod @@ -2556,6 +3089,7 @@ class BackendManifestV2Model(ContractModel): supported_contract_versions: list[NonEmptyString] = Field(min_length=1) compatibility: BackendCompatibilityModel realization_support: list[RealizationSupportDeclarationModel] = Field(min_length=1) + realization_envelope: RealizationEnvelopeIdentityModel | None = None concept_bindings: list[ConceptBindingEntryModel] = Field(min_length=1) constraints: dict[str, str] = Field(default_factory=dict) capabilities: BackendCapabilitiesV2Model @@ -2563,6 +3097,11 @@ class BackendManifestV2Model(ContractModel): @model_validator(mode="after") def _validate_unique_binding_scopes(self) -> BackendManifestV2Model: validate_backend_supported_contract_versions(self.supported_contract_versions) + envelope_contract_declared = "realization-envelope-v1" in self.supported_contract_versions + if self.realization_envelope is not None and not envelope_contract_declared: + raise ValueError("realization_envelope requires realization-envelope-v1 support") + if envelope_contract_declared and self.realization_envelope is None: + raise ValueError("realization-envelope-v1 support requires realization_envelope identity") scopes = [binding.scope for binding in self.concept_bindings] if len(scopes) != len(set(scopes)): raise ValueError("concept_bindings must not contain duplicate scopes") @@ -2578,6 +3117,33 @@ def __get_pydantic_json_schema__( json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) json_schema["properties"]["supported_contract_versions"]["items"]["enum"] = list(BACKEND_SUPPORTED_CONTRACT_IDS) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"realization_envelope": {"not": {"type": "null"}}}, + "required": ["realization_envelope"], + }, + "then": { + "properties": { + "supported_contract_versions": {"contains": {"const": "realization-envelope-v1"}} + } + }, + }, + { + "if": { + "properties": { + "supported_contract_versions": {"contains": {"const": "realization-envelope-v1"}} + }, + "required": ["supported_contract_versions"], + }, + "then": { + "properties": {"realization_envelope": {"not": {"type": "null"}}}, + "required": ["realization_envelope"], + }, + }, + ] + ) return json_schema @@ -2772,6 +3338,7 @@ class ExperimentReferenceModel(ContractModel): "scenario", "scenario-snapshot", "task", + "authoring-input", "protocol", "apparatus-context", "run", @@ -2868,6 +3435,32 @@ class ExperimentScenarioSnapshotReferenceModel(ExperimentReferenceModel): ref_kind: Literal["scenario-snapshot"] +class AssociatedArtifactParentReferenceModel(ExperimentReferenceModel): + """Reference constrained to a supported associated-artifact parent.""" + + ref_kind: Literal[ + "scenario", + "scenario-snapshot", + "task", + "authoring-input", + "apparatus-context", + "run", + "study", + ] + + @model_validator(mode="after") + def _validate_associated_artifact_parent(self) -> AssociatedArtifactParentReferenceModel: + if self.ref_kind == "scenario": + if self.ref_version is not None or self.ref_digest is not None or self.ref_path is not None: + raise ValueError("generic scenario parents are id-only; use scenario-snapshot for snapshot binding") + elif self.ref_kind != "scenario-snapshot" and (self.ref_digest is not None or self.ref_path is not None): + raise ValueError( + "experiment associated-artifact parents must not carry ref_digest or ref_path without a " + "normative parent canonicalization profile" + ) + return self + + class ExperimentManifestReferenceModel(ExperimentReferenceModel): """Reference constrained to an apparatus or capability manifest.""" @@ -3435,6 +4028,11 @@ class ExperimentArtifactRefModel(ContractModel): "scaffold", "baseline", "cost-resource-trace", + "documentation", + "operator-guide", + "configuration", + "profile", + "dataset", "other", ] media_type: NonEmptyString @@ -3453,6 +4051,161 @@ def _validate_artifact_created_at(self) -> ExperimentArtifactRefModel: return self +AssociatedArtifactSetDigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] +_ASSOCIATED_ARTIFACT_SECRET_QUERY_NAMES = frozenset( + { + "access_token", + "api_key", + "apikey", + "auth", + "credential", + "key", + "password", + "secret", + "sig", + "signature", + "token", + } +) +_ASSOCIATED_ARTIFACT_SECRET_QUERY_FRAGMENTS = ( + "api-key", + "api_key", + "apikey", + "credential", + "password", + "secret", + "signature", + "token", +) + + +def _validate_associated_artifact_uri(artifact_id: str, uri: str) -> None: + parsed = urlsplit(uri) + if not parsed.scheme or (parsed.scheme in {"http", "https"} and not parsed.netloc): + raise ValueError(f"associated artifact {artifact_id!r} uri must be an absolute URI") + if parsed.username is not None or parsed.password is not None: + raise ValueError(f"associated artifact {artifact_id!r} uri must not contain credential userinfo") + query_names = {name.casefold() for name, _value in parse_qsl(parsed.query, keep_blank_values=True)} + secret_names = { + name + for name in query_names + if name in _ASSOCIATED_ARTIFACT_SECRET_QUERY_NAMES + or any(fragment in name for fragment in _ASSOCIATED_ARTIFACT_SECRET_QUERY_FRAGMENTS) + } + if secret_names: + raise ValueError(f"associated artifact {artifact_id!r} uri must not contain secret-bearing query fields") + + +class AssociatedArtifactManifestModel(ContractModel): + """One exact non-semantic artifact-reference set attached to one parent.""" + + schema_version: Literal[ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION] + manifest_id: NonEmptyString + manifest_version: NonEmptyString + canonicalization_profile: Literal["associated-artifact-set/v1"] + scope: Literal["scenario", "experiment"] + parent_ref: AssociatedArtifactParentReferenceModel + artifacts: dict[NonEmptyString, ExperimentArtifactRefModel] = Field(min_length=1) + set_digest: AssociatedArtifactSetDigestString + + @model_validator(mode="after") + def _validate_associated_artifact_manifest(self) -> AssociatedArtifactManifestModel: + scenario_kinds = {"scenario", "scenario-snapshot"} + parent_is_scenario = self.parent_ref.ref_kind in scenario_kinds + if (self.scope == "scenario") != parent_is_scenario: + raise ValueError("associated-artifact scope and parent kind must agree") + + descriptor_owners: dict[tuple[Any, ...], str] = {} + locator_claims: dict[str, tuple[Any, ...]] = {} + for artifact_key, artifact in self.artifacts.items(): + if artifact_key != artifact.artifact_id: + raise ValueError( + f"associated artifact map key {artifact_key!r} must equal embedded artifact_id " + f"{artifact.artifact_id!r}" + ) + _validate_associated_artifact_uri(artifact.artifact_id, artifact.uri) + descriptor_key = ( + artifact.role, + artifact.media_type, + artifact.uri, + artifact.checksum.algorithm, + artifact.checksum.value.casefold(), + artifact.size_bytes, + artifact.created_at, + artifact.source, + tuple(ref.model_dump_json() for ref in artifact.satisfies_refs), + artifact.sensitivity, + artifact.description, + ) + prior_descriptor_owner = descriptor_owners.get(descriptor_key) + if prior_descriptor_owner is not None: + raise ValueError( + f"associated artifacts {prior_descriptor_owner!r} and {artifact.artifact_id!r} are exact " + "descriptor aliases; duplicate descriptors require one stable artifact id" + ) + descriptor_owners[descriptor_key] = artifact.artifact_id + + locator_claim = ( + artifact.media_type, + artifact.checksum.algorithm, + artifact.checksum.value.casefold(), + artifact.size_bytes, + ) + prior_locator_claim = locator_claims.get(artifact.uri) + if prior_locator_claim is not None and prior_locator_claim != locator_claim: + raise ValueError("one associated-artifact locator must not carry conflicting payload claims") + locator_claims[artifact.uri] = locator_claim + 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": {"scope": {"const": "scenario"}}, "required": ["scope"]}, + "then": { + "properties": { + "parent_ref": { + "properties": { + "ref_kind": {"enum": ["scenario", "scenario-snapshot"]}, + } + } + } + }, + }, + { + "if": {"properties": {"scope": {"const": "experiment"}}, "required": ["scope"]}, + "then": { + "properties": { + "parent_ref": { + "properties": { + "ref_kind": { + "enum": ["task", "authoring-input", "apparatus-context", "run", "study"] + }, + } + } + } + }, + }, + ] + ) + _add_aces_invariant( + json_schema, + "associated-artifact-parent-set-and-byte-binding", + "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.", + validator="aces_contracts.associated_artifacts.validate_associated_artifact_manifest", + inputs=[{"contract_id": "associated-artifact-manifest-v1", "instance_path": "#"}], + ) + return json_schema + + class ExperimentValidityNoteModel(ContractModel): """Validity threat, limitation, or mitigation note for experiment interpretation.""" @@ -5476,6 +6229,148 @@ def __get_pydantic_json_schema__( return json_schema +class ExperimentEpisodeControlModel(ContractModel): + """Declarative episode execution controls for a planned experiment. + + Captures the pre-run execution-control facts — turn order, logical step + count, and episode termination — that ADR-069 requires for CAGE-2 + execution-control equivalence but that the archival experiment-core + contracts only record after a run has executed. + """ + + turn_order: Literal["sequential", "simultaneous", "round-robin", "scenario-defined", "other"] + termination_rule: NonEmptyString + max_steps: PositiveInteger | None = None + termination_condition_refs: list[NonEmptyString] = Field( + default_factory=list, json_schema_extra={"uniqueItems": True} + ) + description: NonEmptyString | None = None + + +class ExperimentRedVariantSelectionModel(ContractModel): + """Selection of one red-agent variant bound into a planned experiment.""" + + variant_id: NonEmptyString + agent_ref: NonEmptyString + parameters: list[ExperimentParameterModel] = Field(default_factory=list) + description: NonEmptyString | None = None + + +class ExperimentRunPlanModel(ContractModel): + """Pre-run replication, stochastic, episode, and red-variant plan. + + Reuses the archival-family value models for stochastic controls, run + allocation, and clock intent, and adds the authoring-only episode and + red-variant selections. Exactly one of ``allocation`` (condition-based) + or ``target_run_count`` (simple, no-condition) declares the run count. + """ + + stochastic_controls: list[ExperimentStochasticControlModel] = Field(min_length=1) + episode_control: ExperimentEpisodeControlModel + allocation: ExperimentRunAllocationPlanModel | None = None + target_run_count: PositiveInteger | None = None + red_variant_selections: dict[NonEmptyString, ExperimentRedVariantSelectionModel] = Field(default_factory=dict) + clock_intent: ExperimentClockContextModel | None = None + + @model_validator(mode="after") + def _validate_run_plan(self) -> ExperimentRunPlanModel: + if (self.allocation is None) == (self.target_run_count is None): + raise ValueError("run_plan must declare exactly one of allocation or target_run_count") + for key, selection in self.red_variant_selections.items(): + if selection.variant_id != key: + raise ValueError( + f"run_plan red_variant_selections key '{key}' must match embedded variant_id " + f"'{selection.variant_id}'" + ) + 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": ["allocation"], + "properties": {"allocation": {"not": {"type": "null"}}, "target_run_count": {"type": "null"}}, + }, + { + "required": ["target_run_count"], + "properties": {"target_run_count": {"not": {"type": "null"}}, "allocation": {"type": "null"}}, + }, + ] + ) + _add_aces_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 " + "selection map key must equal its embedded variant_id.", + validator="aces_contracts.contracts.ExperimentRunPlanModel._validate_run_plan", + inputs=[{"contract_id": "experiment-authoring-input-v1", "instance_path": "#/run_plan"}], + ) + return json_schema + + +class ExperimentSpecModel(ContractModel): + """Pre-run experiment authoring input: a design that binds a task to a run plan. + + This is the authoring/input counterpart to the archival experiment-core + outputs (run/study/apparatus-context). It references the separately + authored task (and optionally a scenario snapshot) and declares the + pre-run experimental design — apparatus intent, run plan, factors, + intended capture, and validity notes — before any run executes. It is + never a run, study, or apparatus-context record (ADR-055 / ADR-074). + """ + + schema_version: Literal[EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION] + spec_id: NonEmptyString + spec_version: NonEmptyString + title: NonEmptyString + description: NonEmptyString + task_ref: ExperimentTaskReferenceModel + run_plan: ExperimentRunPlanModel + intended_scenario_ref: ExperimentScenarioReferenceModel | None = None + apparatus_intent: ExperimentApparatusConstraintModel | None = None + factors: dict[NonEmptyString, ExperimentStudyFactorModel] = Field(default_factory=dict) + 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) + + @model_validator(mode="after") + def _validate_experiment_spec(self) -> ExperimentSpecModel: + allocation = self.run_plan.allocation + if allocation is not None: + factor_names = set(self.factors) + for blocking_factor in allocation.blocking_factors: + if blocking_factor not in factor_names: + raise ValueError( + f"run_plan allocation blocking factor '{blocking_factor}' must be a declared factor" + ) + 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, + "experiment-spec-blocking-factors-declared", + "When a run plan declares an allocation with blocking factors, every blocking factor must be a " + "declared experiment-spec factor.", + validator="aces_contracts.contracts.ExperimentSpecModel._validate_experiment_spec", + inputs=[{"contract_id": "experiment-authoring-input-v1", "instance_path": "#"}], + ) + return json_schema + + def validate_experiment_task_archival_datetimes(task: ExperimentTaskModel | Mapping[str, Any]) -> None: """Validate task-level archival timestamp semantics not carried by generic JSON Schema.""" @@ -6693,6 +7588,7 @@ def _event_stream_schema(title: str, item_schema: dict[str, Any]) -> dict[str, A REUSABLE_ASSET_FAMILIES: tuple[str, ...] = ( "reusable_scenario", + "associated_artifact_set", "sdl_module", "experiment_task", "experiment_study", @@ -6704,6 +7600,7 @@ def _event_stream_schema(title: str, item_schema: dict[str, Any]) -> dict[str, A ReusableAssetFamily = Literal[ "reusable_scenario", + "associated_artifact_set", "sdl_module", "experiment_task", "experiment_study", @@ -7005,12 +7902,22 @@ def __get_pydantic_json_schema__( def schema_bundle() -> dict[str, dict[str, Any]]: """Return the repo-published JSON Schemas for external contracts.""" + from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel + + from .provenance import SDLLineageLedgerModel + from .scientific_completeness import ( + ScientificCompletenessAssessmentModel, + ScientificCompletenessTaxonomyModel, + ) + bundle = { "aces-semantic-invariants-v1": _aces_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(), "scenario-instantiation-request-v1": InstantiationRequestModel.model_json_schema(), "backend-manifest-v2": BackendManifestV2Model.model_json_schema(), + "realization-envelope-v1": BackendRealizationEnvelopeModel.model_json_schema(), "processor-manifest-v2": ProcessorManifestV2Model.model_json_schema(), "participant-implementation-manifest-v1": ParticipantImplementationManifestModel.model_json_schema(), "participant-implementation-provenance-v1": ParticipantImplementationProvenanceModel.model_json_schema(), @@ -7023,6 +7930,7 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "semantic-profile-v1": SemanticProfileModel.model_json_schema(), "backend-profile-v1": _backend_profile_schema_for_bundle(), "experiment-apparatus-context-v1": ExperimentApparatusContextModel.model_json_schema(), + "experiment-authoring-input-v1": ExperimentSpecModel.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(), @@ -7040,6 +7948,10 @@ def schema_bundle() -> dict[str, dict[str, Any]]: ), "workflow-cancellation-request-v1": WorkflowCancellationRequestModel.model_json_schema(), "evaluation-result-envelope-v1": EvaluationResultStateModel.model_json_schema(), + "proposition-truth-result-v1": PropositionTruthResultModel.model_json_schema(), + "sdl-lineage-ledger-v1": SDLLineageLedgerModel.model_json_schema(), + "scientific-completeness-taxonomy-v1": ScientificCompletenessTaxonomyModel.model_json_schema(), + "scientific-completeness-assessment-v1": ScientificCompletenessAssessmentModel.model_json_schema(), "evaluation-history-event-stream-v1": _event_stream_schema( "EvaluationHistoryEventStream", EvaluationHistoryEventModel.model_json_schema(), @@ -7064,12 +7976,44 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "participant-context-view-v1": ParticipantContextViewModel.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(), "reusable-asset-trust-policy-v1": ReusableAssetTrustPolicyModel.model_json_schema(), } + _add_aces_invariant( + bundle["scientific-completeness-taxonomy-v1"], + "scientific-completeness-taxonomy-rectangular", + "Concern and profile ids must be unique, and every profile disposition " + "map must exactly cover the taxonomy concern set.", + validator="aces_contracts.scientific_completeness.ScientificCompletenessTaxonomyModel", + inputs=[{"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + bundle["scientific-completeness-assessment-v1"], + "scientific-completeness-assessment-status-evidence", + "Concern ids must be unique and each delivery status must carry its " + "required executable evidence, external binding, issue refs, or " + "exclusion rationale.", + validator="aces_contracts.scientific_completeness.ScientificCompletenessAssessmentModel", + inputs=[{"contract_id": "scientific-completeness-assessment-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + bundle["scientific-completeness-assessment-v1"], + "scientific-completeness-taxonomy-assessment-join", + "Assessment family, taxonomy revision, and concern ids must exactly " + "match the joined taxonomy before completeness is computed.", + validator="aces_contracts.scientific_completeness.evaluate_profile_completeness", + inputs=[ + {"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#"}, + {"contract_id": "scientific-completeness-assessment-v1", "instance_path": "#"}, + ], + ) for contract_id, json_schema in bundle.items(): + _attach_sdl_identifier_constraints(contract_id, json_schema) _attach_instantiation_invariants(contract_id, json_schema) _attach_experiment_datetime_invariants(contract_id, json_schema) _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) known_contract_ids = frozenset(bundle) for contract_id, json_schema in bundle.items(): @@ -7092,6 +8036,10 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "AttackEnterpriseTacticsSourceModel", "AtlasTacticSourceTermModel", "AtlasTacticsSourceModel", + "ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION", + "AssociatedArtifactManifestModel", + "AssociatedArtifactParentReferenceModel", + "AssociatedArtifactSetDigestString", "BACKEND_MANIFEST_V2_SCHEMA_VERSION", "ApparatusIdentityModel", "BackendCompatibilityModel", @@ -7129,6 +8077,7 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ExperimentDerivedMeasureMethodModel", "ExperimentDerivedMeasureModel", "ExperimentDerivedMeasureReferenceModel", + "ExperimentEpisodeControlModel", "ExperimentEvidenceRecordModel", "ExperimentEvidenceRecordReferenceModel", "ExperimentEvidenceReferenceModel", @@ -7142,13 +8091,16 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ExperimentParameterModel", "ExperimentProcessorReferenceModel", "ExperimentRealizedFormDisclosureModel", + "ExperimentRedVariantSelectionModel", "ExperimentReferenceModel", "ExperimentResultSummaryModel", "ExperimentRunAllocationPlanModel", "ExperimentRunModel", + "ExperimentRunPlanModel", "ExperimentRunTraceabilityModel", "ExperimentScenarioReferenceModel", "ExperimentScenarioSnapshotReferenceModel", + "ExperimentSpecModel", "ExperimentSplitAndLeakageControlsModel", "ExperimentStatisticalMethodModel", "ExperimentStochasticControlModel", @@ -7160,6 +8112,7 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ExperimentUncertaintyMethodModel", "ExperimentValidityNoteModel", "EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION", + "EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION", "EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION", "EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION", "EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION", @@ -7169,6 +8122,15 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "EvaluationHistoryEventModel", "EvaluationPlanModel", "EvaluationResultStateModel", + "PropositionAssertionPolarity", + "PropositionEvaluationBasis", + "PropositionIndeterminacyReason", + "PropositionLossDisclosureModel", + "PropositionLossKind", + "PropositionProbeBindingModel", + "PropositionTemporalContextModel", + "PropositionTruthOutcome", + "PropositionTruthResultModel", "EVALUATION_STATE_SCHEMA_VERSION", "EvaluatorCapabilitiesModel", "EventClassificationModel", @@ -7233,6 +8195,7 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ProcessorCapabilitiesV2Model", "ProvisionerCapabilitiesModel", "ProvisioningPlanModel", + "RealizationEnvelopeIdentityModel", "RawDataIntegrityModel", "RealizationProvenanceEntryModel", "RealizationSupportDeclarationModel", diff --git a/implementations/python/packages/aces_contracts/corpus.py b/implementations/python/packages/aces_contracts/corpus.py index c19d7d5aa..d37cb22d4 100644 --- a/implementations/python/packages/aces_contracts/corpus.py +++ b/implementations/python/packages/aces_contracts/corpus.py @@ -39,6 +39,8 @@ FIXTURES = "fixtures" CONCEPT_AUTHORITY = "concept-authority" SCHEMAS = "schemas" +REALIZATION_ENVELOPES = "realization-envelopes" +PROVENANCE = "provenance" def _bundled_corpus_root() -> Path | None: @@ -110,7 +112,7 @@ def corpus_family_root(family: str) -> Path: """Return the root directory of a normative corpus family. ``family`` is one of :data:`PROFILES`, :data:`FIXTURES`, - :data:`CONCEPT_AUTHORITY`, or :data:`SCHEMAS`. + :data:`CONCEPT_AUTHORITY`, :data:`PROVENANCE`, or :data:`SCHEMAS`. """ return corpus_root() / family @@ -120,6 +122,8 @@ def corpus_family_root(family: str) -> Path: "CONCEPT_AUTHORITY", "FIXTURES", "PROFILES", + "PROVENANCE", + "REALIZATION_ENVELOPES", "SCHEMAS", "corpus_family_root", "corpus_root", diff --git a/implementations/python/packages/aces_contracts/experiment_spec.py b/implementations/python/packages/aces_contracts/experiment_spec.py new file mode 100644 index 000000000..4aaf9d4e3 --- /dev/null +++ b/implementations/python/packages/aces_contracts/experiment_spec.py @@ -0,0 +1,76 @@ +"""Loading helpers for authored experiment specifications. + +An experiment specification is the pre-run authoring/input counterpart to the +archival experiment-core outputs (run/study/apparatus-context). Unlike SDL, +the authoring input is a single nested contract document with no shorthand, +key normalization, or cross-file resolution, so loading is a thin +``yaml.safe_load`` followed by ``ExperimentSpecModel`` validation — mirroring +how the JSON fixtures under ``contracts/fixtures/experiment-core`` are checked. +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +import yaml +from pydantic import ValidationError + +from .contracts import ExperimentSpecModel + +log = logging.getLogger("aces.experiment_spec") + + +class ExperimentSpecError(Exception): + """Base exception for experiment-specification loading.""" + + +class ExperimentSpecValidationError(ExperimentSpecError): + """An experiment specification failed to parse or validate.""" + + def __init__(self, message: str, path: Path | None = None) -> None: + self.path = path + self.details = message + prefix = f"{path}: " if path else "" + super().__init__(f"{prefix}{message}") + + +def parse_experiment_spec(text: str, *, path: Path | None = None) -> ExperimentSpecModel: + """Parse and validate an experiment specification from YAML text.""" + raw = text.strip() + if not raw: + raise ExperimentSpecValidationError("Experiment spec is empty", path=path) + + try: + payload = yaml.safe_load(raw) + except yaml.YAMLError as exc: + raise ExperimentSpecValidationError(f"YAML parse error: {exc}", path=path) from exc + + if not isinstance(payload, dict): + raise ExperimentSpecValidationError("Experiment spec must be a YAML mapping", path=path) + + try: + return ExperimentSpecModel.model_validate(payload) + except ValidationError as exc: + raise ExperimentSpecValidationError(str(exc), path=path) from exc + + +def load_experiment_spec(path: Path) -> ExperimentSpecModel: + """Load and validate an authored experiment specification from a YAML file.""" + if not path.exists(): + raise FileNotFoundError(f"Experiment spec file not found: {path}") + + spec = parse_experiment_spec(path.read_text(encoding="utf-8"), path=path) + log.info("Loaded experiment spec '%s' from %s", spec.spec_id, path) + return spec + + +def find_experiment_specs(search_dir: Path) -> list[Path]: + """Find all authored experiment-spec files in a directory (non-recursive).""" + if not search_dir.is_dir(): + log.debug("Experiments directory does not exist: %s", search_dir) + return [] + + paths = sorted(search_dir.glob("*.exp.yaml")) + log.debug("Found %d experiment spec files in %s", len(paths), search_dir) + return paths diff --git a/implementations/python/packages/aces_contracts/manifest_authority.py b/implementations/python/packages/aces_contracts/manifest_authority.py index 7c747fd60..38008e8a1 100644 --- a/implementations/python/packages/aces_contracts/manifest_authority.py +++ b/implementations/python/packages/aces_contracts/manifest_authority.py @@ -22,6 +22,7 @@ "workflow-result-envelope-v1", "workflow-history-event-stream-v1", "evaluation-result-envelope-v1", + "proposition-truth-result-v1", "evaluation-history-event-stream-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", @@ -34,6 +35,7 @@ # separate authority surfaces and do not belong in this declaration field. BACKEND_SUPPORTED_CONTRACT_IDS = ( "backend-manifest-v2", + "realization-envelope-v1", "provisioning-plan-v1", "orchestration-plan-v1", "evaluation-plan-v1", @@ -43,6 +45,7 @@ "workflow-result-envelope-v1", "workflow-history-event-stream-v1", "evaluation-result-envelope-v1", + "proposition-truth-result-v1", "evaluation-history-event-stream-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", diff --git a/implementations/python/packages/aces_contracts/planning.py b/implementations/python/packages/aces_contracts/planning.py index eb3041af6..1a8812ffe 100644 --- a/implementations/python/packages/aces_contracts/planning.py +++ b/implementations/python/packages/aces_contracts/planning.py @@ -1,11 +1,17 @@ """Shared runtime planning contracts.""" +from __future__ import annotations + from dataclasses import dataclass, field from enum import Enum -from typing import Any +from typing import TYPE_CHECKING, Any +from aces_contracts.addressing import require_compiled_address from aces_contracts.diagnostics import Diagnostic +if TYPE_CHECKING: + from aces_contracts.contracts import RealizationEnvelopeIdentityModel + class RuntimeDomain(str, Enum): """Top-level runtime concern.""" @@ -16,6 +22,32 @@ class RuntimeDomain(str, Enum): PARTICIPANT = "participant" +PLAN_ADDRESS_ROOT_BY_DOMAIN = { + RuntimeDomain.PROVISIONING: "provision", + RuntimeDomain.ORCHESTRATION: "orchestration", + RuntimeDomain.EVALUATION: "evaluation", +} +PLAN_RESOURCE_TYPES_BY_DOMAIN = { + RuntimeDomain.PROVISIONING: frozenset( + {"network", "node", "feature-binding", "content-placement", "account-placement"} + ), + RuntimeDomain.ORCHESTRATION: frozenset({"inject-binding", "inject", "event", "script", "story", "workflow"}), + RuntimeDomain.EVALUATION: frozenset({"condition-binding", "proposition", "assertion", "objective"}), +} + + +def require_plan_operation_identity(domain: RuntimeDomain, address: object, resource_type: object) -> None: + """Reject operations outside a plan endpoint's closed identity domain.""" + + canonical = require_compiled_address(address) + root = PLAN_ADDRESS_ROOT_BY_DOMAIN.get(domain) + resource_types = PLAN_RESOURCE_TYPES_BY_DOMAIN.get(domain) + if root is None or not canonical.startswith(f"{root}."): + raise ValueError("Plan operation address must belong to its runtime domain") + if not isinstance(resource_type, str) or resource_types is None or resource_type not in resource_types: + raise ValueError("Plan operation resource_type must belong to its runtime domain") + + class ChangeAction(str, Enum): """Planner reconciliation result for a resource.""" @@ -36,6 +68,11 @@ class PlannedResource: ordering_dependencies: tuple[str, ...] = () refresh_dependencies: tuple[str, ...] = () + def __post_init__(self) -> None: + require_compiled_address(self.address) + for dependency in (*self.ordering_dependencies, *self.refresh_dependencies): + require_compiled_address(dependency, field_name="dependency address") + @dataclass(frozen=True) class PlanOperation: @@ -48,6 +85,11 @@ class PlanOperation: ordering_dependencies: tuple[str, ...] = () refresh_dependencies: tuple[str, ...] = () + def __post_init__(self) -> None: + require_compiled_address(self.address) + for dependency in (*self.ordering_dependencies, *self.refresh_dependencies): + require_compiled_address(dependency, field_name="dependency address") + class ProvisionOp(PlanOperation): """Provisioning reconciliation operation.""" @@ -68,6 +110,10 @@ class ProvisioningPlan: resources: dict[str, PlannedResource] = field(default_factory=dict) operations: list[ProvisionOp] = field(default_factory=list) diagnostics: list[Diagnostic] = field(default_factory=list) + realization_envelope: RealizationEnvelopeIdentityModel | None = None + + def __post_init__(self) -> None: + _validate_plan_addresses(self.resources, self.operations, domain=RuntimeDomain.PROVISIONING) @property def actionable_operations(self) -> list[ProvisionOp]: @@ -83,6 +129,14 @@ class OrchestrationPlan: startup_order: list[str] = field(default_factory=list) diagnostics: list[Diagnostic] = field(default_factory=list) + def __post_init__(self) -> None: + _validate_plan_addresses( + self.resources, + self.operations, + self.startup_order, + domain=RuntimeDomain.ORCHESTRATION, + ) + @property def actionable_operations(self) -> list[OrchestrationOp]: return [op for op in self.operations if op.action != ChangeAction.UNCHANGED] @@ -97,11 +151,49 @@ class EvaluationPlan: startup_order: list[str] = field(default_factory=list) diagnostics: list[Diagnostic] = field(default_factory=list) + def __post_init__(self) -> None: + _validate_plan_addresses( + self.resources, + self.operations, + self.startup_order, + domain=RuntimeDomain.EVALUATION, + ) + @property def actionable_operations(self) -> list[EvaluationOp]: return [op for op in self.operations if op.action != ChangeAction.UNCHANGED] +def _validate_plan_addresses( + resources: dict[str, PlannedResource], + operations: list[PlanOperation], + startup_order: list[str] | None = None, + *, + domain: RuntimeDomain, +) -> None: + for map_key, resource in resources.items(): + require_compiled_address(map_key, field_name="resource map key") + if map_key != resource.address: + raise ValueError("Plan resource map key must equal embedded address") + if resource.domain is not domain: + raise ValueError("Plan resource domain must equal the plan domain") + require_plan_operation_identity(domain, resource.address, resource.resource_type) + operation_addresses = [operation.address for operation in operations] + for operation in operations: + require_plan_operation_identity(domain, operation.address, operation.resource_type) + if len(operation_addresses) != len(set(operation_addresses)): + raise ValueError("Plan operation addresses must be unique") + if startup_order is None: + return + for address in startup_order: + require_compiled_address(address, field_name="startup_order address") + if len(startup_order) != len(set(startup_order)): + raise ValueError("Plan startup_order addresses must be unique") + unknown = set(startup_order) - set(operation_addresses) + if unknown: + raise ValueError("Plan startup_order must reference admitted operation addresses") + + __all__ = ( "ChangeAction", "EvaluationOp", @@ -109,8 +201,11 @@ def actionable_operations(self) -> list[EvaluationOp]: "OrchestrationOp", "OrchestrationPlan", "PlanOperation", + "PLAN_ADDRESS_ROOT_BY_DOMAIN", + "PLAN_RESOURCE_TYPES_BY_DOMAIN", "PlannedResource", "ProvisionOp", "ProvisioningPlan", "RuntimeDomain", + "require_plan_operation_identity", ) diff --git a/implementations/python/packages/aces_contracts/provenance.py b/implementations/python/packages/aces_contracts/provenance.py new file mode 100644 index 000000000..abda0a195 --- /dev/null +++ b/implementations/python/packages/aces_contracts/provenance.py @@ -0,0 +1,349 @@ +"""Normative SDL lineage and third-party provenance contracts.""" + +from __future__ import annotations + +import json +from enum import Enum +from pathlib import Path +from typing import Annotated, Literal + +from pydantic import Field, model_validator + +from .contracts import ContractModel, NonEmptyString +from .corpus import PROVENANCE, corpus_family_root +from .versions import SDL_LINEAGE_LEDGER_SCHEMA_VERSION + +PublicUrl = Annotated[str, Field(pattern=r"^https://[^\s]+$")] +Sha1 = Annotated[str, Field(pattern=r"^[0-9a-f]{40}$")] +FragmentPointer = Annotated[str, Field(pattern=r"^#[^\s]*$")] +IsoDate = Annotated[str, Field(pattern=r"^\d{4}-\d{2}-\d{2}$")] + + +class LineageSourceKind(str, Enum): + GIT = "git" + STANDARD = "standard" + PUBLICATION = "publication" + + +class LineagePlane(str, Enum): + SYNTAX = "syntax" + SEMANTICS = "semantics" + ARTIFACT_CODE = "artifact_code" + EXAMPLE = "example" + + +class LineageClassification(str, Enum): + ADOPTED_SYNTAX = "adopted_syntax" + ADOPTED_SEMANTICS = "adopted_semantics" + ADAPTED = "adapted" + ACES_NATIVE = "aces_native" + + +class LineageDisposition(str, Enum): + CURRENT = "current" + REMOVED = "removed" + PLANNED = "planned" + + +class CompatibilityStatus(str, Enum): + NOT_APPLICABLE = "not_applicable" + NONE = "none" + PARTIAL = "partial" + COMPATIBLE = "compatible" + + +class CompatibilityDirection(str, Enum): + ACES_RELATIVE_TO_SOURCE = "aces_relative_to_source" + NOT_APPLICABLE = "not_applicable" + + +class NoticeDecision(str, Enum): + REQUIRED_INCLUDED = "required_included" + NOT_REQUIRED = "not_required" + UNKNOWN = "unknown" + + +class BibliographicIdentityModel(ContractModel): + citation_id: NonEmptyString + title: NonEmptyString + authors_or_maintainer: list[NonEmptyString] = Field(min_length=1) + year: int = Field(ge=1800, le=2200) + container_title: NonEmptyString | None = None + doi: Annotated[str, Field(pattern=r"^10\.\d{4,9}/\S+$")] | None = None + canonical_url: PublicUrl + verified_on: IsoDate + verification_evidence: NonEmptyString + + +class LineageSourceBaseModel(ContractModel): + source_id: NonEmptyString + title: NonEmptyString + version_or_edition: NonEmptyString + canonical_url: PublicUrl + citation_ref: NonEmptyString + + +class GitLineageSourceModel(LineageSourceBaseModel): + kind: Literal[LineageSourceKind.GIT] + repository_url: PublicUrl + commit: Sha1 + license_expression: NonEmptyString | None = None + license_url: PublicUrl | None = None + license_evidence_ref: NonEmptyString | None = None + + @model_validator(mode="after") + def validate_revision_urls(self) -> GitLineageSourceModel: + if self.commit not in self.canonical_url: + raise ValueError("git canonical_url must contain the declared commit") + if self.license_url is not None and self.commit not in self.license_url: + raise ValueError("git license_url must contain the declared commit") + return self + + +class StandardLineageSourceModel(LineageSourceBaseModel): + kind: Literal[LineageSourceKind.STANDARD] + maintaining_body: NonEmptyString + + +class PublicationLineageSourceModel(LineageSourceBaseModel): + kind: Literal[LineageSourceKind.PUBLICATION] + + +LineageSource = Annotated[ + GitLineageSourceModel | StandardLineageSourceModel | PublicationLineageSourceModel, + Field(discriminator="kind"), +] + + +class AuthorityCoordinateModel(ContractModel): + artifact: NonEmptyString + pointer: FragmentPointer + contract_id: NonEmptyString | None = None + + +class ArtifactBoundaryModel(ContractModel): + artifact: NonEmptyString + symbol_or_pointer: NonEmptyString + + +class LineageClaimModel(ContractModel): + plane: LineagePlane + classification: LineageClassification + source_refs: list[NonEmptyString] = Field(default_factory=list) + aces_boundaries: list[ArtifactBoundaryModel] = Field(min_length=1) + source_boundaries: list[ArtifactBoundaryModel] = Field(default_factory=list) + divergence: NonEmptyString + compatibility: CompatibilityStatus + compatibility_direction: CompatibilityDirection + citation_refs: list[NonEmptyString] = Field(default_factory=list) + internal_authority_refs: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def validate_claim_dimensions(self) -> LineageClaimModel: + if self.classification is LineageClassification.ACES_NATIVE: + self._validate_native_dimensions() + else: + self._validate_external_dimensions() + return self + + def _validate_native_dimensions(self) -> None: + if self.source_refs or self.source_boundaries: + raise ValueError("ACES-native claims must not name an external source boundary") + if not self.internal_authority_refs: + raise ValueError("ACES-native claims require internal authority refs") + if self.compatibility is not CompatibilityStatus.NOT_APPLICABLE: + raise ValueError("ACES-native claims have no source compatibility relation") + if self.compatibility_direction is not CompatibilityDirection.NOT_APPLICABLE: + raise ValueError("ACES-native claims have no source compatibility direction") + + def _validate_external_dimensions(self) -> None: + if not self.source_refs or not self.source_boundaries or not self.citation_refs: + raise ValueError("non-native claims require source, source boundary, and citation refs") + if self.classification is LineageClassification.ADOPTED_SYNTAX and self.plane is not LineagePlane.SYNTAX: + 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: + raise ValueError("non-native claims assess ACES relative to the named source") + + +class LineageSubjectModel(ContractModel): + subject_id: Annotated[str, Field(pattern=r"^[a-z][a-z0-9-]*:[a-z][a-z0-9_-]*$")] + subject_kind: Literal["top_level_field", "runtime_family", "concept_family", "reference_model"] + disposition: LineageDisposition + authority: AuthorityCoordinateModel + claims: list[LineageClaimModel] = Field(min_length=1) + + @model_validator(mode="after") + def validate_subject_namespace(self) -> LineageSubjectModel: + expected_namespace = { + "top_level_field": "sdl-field", + "runtime_family": "runtime-family", + "concept_family": "concept-family", + "reference_model": "reference-model", + }[self.subject_kind] + namespace = self.subject_id.split(":", 1)[0] + if namespace != expected_namespace: + raise ValueError(f"subject namespace {namespace!r} does not match subject_kind {self.subject_kind!r}") + return self + + +class ThirdPartyDispositionModel(ContractModel): + source_ref: NonEmptyString + derivation_scope: list[ArtifactBoundaryModel] = Field(min_length=1) + derivation_extent: NonEmptyString + reviewed_license_ref: NonEmptyString + notice_decision: NoticeDecision + notice_artifact: NonEmptyString | None = None + rationale: NonEmptyString + reviewed_on: IsoDate + evidence_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def validate_notice_resolution(self) -> ThirdPartyDispositionModel: + if self.notice_decision is NoticeDecision.REQUIRED_INCLUDED and self.notice_artifact is None: + raise ValueError("required notice disposition must name notice_artifact") + if self.notice_decision is not NoticeDecision.REQUIRED_INCLUDED and self.notice_artifact is not None: + raise ValueError("notice_artifact is valid only when a required notice is included") + return self + + +class SDLLineageLedgerModel(ContractModel): + schema_version: Literal[SDL_LINEAGE_LEDGER_SCHEMA_VERSION] = SDL_LINEAGE_LEDGER_SCHEMA_VERSION + reviewed_on: IsoDate + citations: list[BibliographicIdentityModel] = Field(min_length=1) + sources: list[LineageSource] = Field(min_length=1) + subjects: list[LineageSubjectModel] = Field(min_length=1) + third_party_dispositions: list[ThirdPartyDispositionModel] = Field(default_factory=list) + + @model_validator(mode="after") + def validate_ledger_references(self) -> SDLLineageLedgerModel: + citation_ids = self._unique_ids("citation", [item.citation_id for item in self.citations]) + source_ids = self._unique_ids("source", [item.source_id for item in self.sources]) + source_citations = {item.source_id: item.citation_ref for item in self.sources} + self._unique_ids("subject", [item.subject_id for item in self.subjects]) + self._unique_ids( + "third-party disposition source", + [item.source_ref for item in self.third_party_dispositions], + ) + disposition_sources = {item.source_ref for item in self.third_party_dispositions} + disposition_artifacts = { + item.source_ref: {boundary.artifact for boundary in item.derivation_scope} + for item in self.third_party_dispositions + } + resolved_disposition_sources = { + item.source_ref + for item in self.third_party_dispositions + if item.notice_decision is not NoticeDecision.UNKNOWN + } + for source in self.sources: + if source.citation_ref not in citation_ids: + raise ValueError(f"source {source.source_id!r} has unknown citation_ref") + for subject in self.subjects: + self._validate_subject( + subject, + source_ids, + source_citations, + citation_ids, + disposition_sources, + resolved_disposition_sources, + disposition_artifacts, + ) + for disposition in self.third_party_dispositions: + if disposition.source_ref not in source_ids: + raise ValueError(f"third-party disposition has unknown source_ref {disposition.source_ref!r}") + return self + + @staticmethod + def _unique_ids(label: str, values: list[str]) -> set[str]: + if len(set(values)) != len(values): + raise ValueError(f"{label} ids must be unique") + return set(values) + + @staticmethod + def _validate_subject( + subject: LineageSubjectModel, + source_ids: set[str], + source_citations: dict[str, str], + citation_ids: set[str], + disposition_sources: set[str], + resolved_disposition_sources: set[str], + disposition_artifacts: dict[str, set[str]], + ) -> None: + for claim in subject.claims: + SDLLineageLedgerModel._validate_claim_references( + subject.subject_id, + claim, + source_ids, + source_citations, + citation_ids, + ) + SDLLineageLedgerModel._validate_artifact_code_claim( + subject.subject_id, + claim, + disposition_sources, + resolved_disposition_sources, + disposition_artifacts, + ) + SDLLineageLedgerModel._validate_planned_compatibility(subject, claim) + + @staticmethod + def _validate_claim_references( + subject_id: str, + claim: LineageClaimModel, + source_ids: set[str], + source_citations: dict[str, str], + citation_ids: set[str], + ) -> None: + if set(claim.source_refs) - source_ids: + raise ValueError(f"subject {subject_id!r} has unknown source refs") + if set(claim.citation_refs) - citation_ids: + raise ValueError(f"subject {subject_id!r} has unknown citation refs") + expected_citations = {source_citations[source_ref] for source_ref in claim.source_refs} + if set(claim.citation_refs) != expected_citations: + raise ValueError(f"subject {subject_id!r} claim citation refs do not identify its sources") + + @staticmethod + def _validate_artifact_code_claim( + subject_id: str, + claim: LineageClaimModel, + disposition_sources: set[str], + resolved_disposition_sources: set[str], + disposition_artifacts: dict[str, set[str]], + ) -> None: + if claim.plane is not LineagePlane.ARTIFACT_CODE: + return + claim_sources = set(claim.source_refs) + if not claim_sources.issubset(disposition_sources): + 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} + for source_ref in claim.source_refs: + uncovered = claim_artifacts - disposition_artifacts.get(source_ref, set()) + if uncovered: + raise ValueError( + f"subject {subject_id!r} artifact/code claim is outside the audited " + f"derivation scope for {source_ref!r}: {sorted(uncovered)}" + ) + + @staticmethod + def _validate_planned_compatibility(subject: LineageSubjectModel, claim: LineageClaimModel) -> None: + current_compatibility = { + CompatibilityStatus.COMPATIBLE, + CompatibilityStatus.PARTIAL, + } + if subject.disposition is LineageDisposition.PLANNED and claim.compatibility in current_compatibility: + raise ValueError(f"planned subject {subject.subject_id!r} cannot claim current compatibility") + + +def sdl_lineage_ledger_path() -> Path: + return corpus_family_root(PROVENANCE) / "sdl-lineage-ledger-v1.json" + + +def load_sdl_lineage_ledger() -> SDLLineageLedgerModel: + payload = json.loads(sdl_lineage_ledger_path().read_text(encoding="utf-8")) + return SDLLineageLedgerModel.model_validate(payload) + + +__all__ = ["SDLLineageLedgerModel", "load_sdl_lineage_ledger", "sdl_lineage_ledger_path"] diff --git a/implementations/python/packages/aces_contracts/realization_envelope.py b/implementations/python/packages/aces_contracts/realization_envelope.py new file mode 100644 index 000000000..9297e1866 --- /dev/null +++ b/implementations/python/packages/aces_contracts/realization_envelope.py @@ -0,0 +1,423 @@ +"""Realization-envelope expression contract (ADR-070, envelope-semantics.md). + +A realization envelope is a closed, versioned expression that denotes a *set* of +SDL scenario instances. The same expression is used in both directions: an author +describes an acceptable scenario family, and a backend describes the family it can +realize. The membership / subsumption / witness / negative-probe relation over +this contract lives in :mod:`aces_sdl.realization_envelope`. + +The model is deliberately *closed* (``extra="forbid"`` plus a finite discriminated +union of domain kinds). That closedness is the portability guarantee of +``envelope-semantics.md`` R3 / ADR-070 §3: arbitrary Python predicates, backend +callbacks, external queries, unbounded regex, recursion, and non-linear arithmetic +are simply not representable, which keeps membership and subsumption reducible to +local structural checks and witness generation deterministic. + +Issue #100 publishes the expression inside a configuration-bound backend carrier +at ``contracts/schemas/realization-envelope/realization-envelope-v1.json``. The carrier +adds typed material-configuration, transformation, support, and observation +disclosures while the relation continues to operate on the same expression. +""" + +from __future__ import annotations + +from collections.abc import Callable +from enum import Enum +from typing import Annotated, Literal + +from pydantic import Field, model_validator + +from aces_contracts.contracts import ContractModel, NonEmptyString, RealizationEnvelopeIdentityModel +from aces_contracts.versions import REALIZATION_ENVELOPE_SCHEMA_VERSION + +__all__ = [ + "BackendRealizationEnvelopeModel", + "REALIZATION_ENVELOPE_SCHEMA_VERSION", + "BooleanDomain", + "Closure", + "ClosureOverlay", + "ConcernDisposition", + "DomainDescriptor", + "EnumDomain", + "EnvelopeBinding", + "EnvelopeScope", + "ExactDomain", + "GovernedReferenceDomain", + "NumericIntervalDomain", + "NumericType", + "ObservationStrength", + "Posture", + "RealizationEnvelopeModel", + "RealizationEnvelopeIdentityModel", + "RealizationConcern", + "RealizationConcernDisclosureModel", + "RealizerConfigurationModel", + "IntegerBoundsModel", + "RecordDomain", + "WitnessPolicy", + "TransformationKind", + "scalar_in_domain", + "scalar_matches_numeric_type", + "realization_envelope_digest", + "realizer_configuration_digest", + "validate_backend_realization_envelope", +] + +# Portable envelope values are JSON scalars. ``bool`` is intentionally distinct +# from ``int`` here (see ``scalar_matches_numeric_type``); Pydantic preserves the +# authored Python type on a closed model, so ``True`` never collapses to ``1``. +DomainScalar = bool | int | float | str + + +class EnvelopeScope(str, Enum): + """Semantic extent where a posture or closure applies (most local first).""" + + FIELD = "field" + NODE = "node" + TOPOLOGY = "topology" + APP = "app" + SCENARIO = "scenario" + + +class Posture(str, Enum): + """Author/backend intent for a bound value or child scope.""" + + OPEN = "open" + CONSTRAINED = "constrained" + EXACT = "exact" + + +class Closure(str, Enum): + """Whether unspecified realizable dimensions under a scope are admitted.""" + + OPEN_WORLD = "open-world" + CLOSED_WORLD = "closed-world" + + +class NumericType(str, Enum): + """Declared numeric type for a numeric-interval domain.""" + + INTEGER = "integer" + NUMBER = "number" + + +class ExactDomain(ContractModel): + """A singleton value set: values equal to ``value``.""" + + kind: Literal["exact"] = "exact" + value: DomainScalar + + +class EnumDomain(ContractModel): + """A finite value set: values equal to one listed member.""" + + kind: Literal["enum"] = "enum" + values: list[DomainScalar] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_unique(self) -> EnumDomain: + # ``list`` (not ``set``) preserves authored order and the bool/int + # distinction; dedupe on a type-tagged key so ``True`` and ``1`` stay + # separate members. + seen: set[tuple[str, object]] = set() + for member in self.values: + key = (type(member).__name__, member) + if key in seen: + raise ValueError("enum domain values must be unique") + seen.add(key) + return self + + +class BooleanDomain(ContractModel): + """Booleans: both ``true``/``false``, or an exact boolean when ``value`` set.""" + + kind: Literal["boolean"] = "boolean" + value: bool | None = None + + +class NumericIntervalDomain(ContractModel): + """Numbers of ``numeric_type`` inside a bounded interval. + + Both endpoints are required (the fragment admits only *bounded* intervals, + ``envelope-semantics.md`` R3). An integer interval requires integral + endpoints. Empty intervals are rejected at construction. + """ + + kind: Literal["numeric-interval"] = "numeric-interval" + numeric_type: NumericType + lower: float + upper: float + lower_closed: bool = True + upper_closed: bool = True + + @model_validator(mode="after") + def _validate_interval(self) -> NumericIntervalDomain: + if self.numeric_type is NumericType.INTEGER: + if self.lower != int(self.lower) or self.upper != int(self.upper): + raise ValueError("integer numeric-interval endpoints must be integral") + if self.lower > self.upper: + raise ValueError("numeric-interval lower endpoint must not exceed upper") + if self.lower == self.upper and not (self.lower_closed and self.upper_closed): + raise ValueError("degenerate numeric-interval must be closed on both endpoints") + return self + + +class GovernedReferenceDomain(ContractModel): + """References in a finite governed set under a named authority.""" + + kind: Literal["governed-reference"] = "governed-reference" + authority: NonEmptyString + allowed_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_unique(self) -> GovernedReferenceDomain: + if len(self.allowed_refs) != len(set(self.allowed_refs)): + raise ValueError("governed-reference allowed_refs must be unique") + return self + + +class RecordDomain(ContractModel): + """Product structure: each declared field references another named domain. + + ``extra`` controls undeclared fields: ``False`` (closed) rejects any field not + named in ``fields``; ``True`` (open) admits them. Field values reference domain + names resolved against the envelope's ``domains`` map, keeping the structure + acyclic and free of inline recursion. + """ + + kind: Literal["record"] = "record" + fields: dict[NonEmptyString, NonEmptyString] = Field(min_length=1) + extra: bool = False + + +DomainDescriptor = Annotated[ + ExactDomain | EnumDomain | BooleanDomain | NumericIntervalDomain | GovernedReferenceDomain | RecordDomain, + Field(discriminator="kind"), +] + + +class EnvelopeBinding(ContractModel): + """Binds an SDL path (or governed scope ref) to a domain at a scope. + + ``domain`` names a descriptor in the envelope's ``domains`` map and is required + for ``constrained`` / ``exact`` posture and forbidden for ``open`` posture (an + open value is left to a downstream realizer). ``overrideable`` allows a + more-specific binding to widen a value an enclosing closed scope fixed + (``envelope-semantics.md`` R2). + """ + + path: NonEmptyString + scope: EnvelopeScope + posture: Posture + domain: str | None = None + overrideable: bool = False + + @model_validator(mode="after") + def _validate_posture_domain(self) -> EnvelopeBinding: + if self.posture is Posture.OPEN and self.domain is not None: + raise ValueError("open posture binding must not name a domain") + if self.posture in (Posture.CONSTRAINED, Posture.EXACT) and not self.domain: + raise ValueError(f"{self.posture.value} posture binding must name a domain") + return self + + +class ClosureOverlay(ContractModel): + """Declares open-world or closed-world closure at a scope path.""" + + path: str = "" + scope: EnvelopeScope + closure: Closure + + +class WitnessPolicy(ContractModel): + """Deterministic default-selection policy for witness generation. + + ``selections`` overrides the default choice for named domains (each value must + be a member of the referenced domain); ``seed`` records the selection basis for + reproducibility. Neither introduces randomness: witness generation stays a pure + function of ``(envelope, policy)``. + """ + + seed: str | None = None + selections: dict[NonEmptyString, DomainScalar] = Field(default_factory=dict) + + +class RealizationEnvelopeModel(ContractModel): + """A versioned expression denoting a set of SDL scenario instances.""" + + schema_version: Literal["realization-envelope/v1"] = "realization-envelope/v1" + id: NonEmptyString + scope: EnvelopeScope + domains: dict[NonEmptyString, DomainDescriptor] = Field(default_factory=dict) + bindings: list[EnvelopeBinding] = Field(default_factory=list) + closure: list[ClosureOverlay] = Field(default_factory=list) + witness_policy: WitnessPolicy | None = None + source_ref: str | None = None + contract_id: str | None = None + digest: str | None = None + + @model_validator(mode="after") + def _validate_envelope(self) -> RealizationEnvelopeModel: + self._validate_domain_references() + self._validate_acyclic_records() + self._validate_bindings() + self._validate_witness_policy() + return self + + def _validate_domain_references(self) -> None: + for name, descriptor in self.domains.items(): + if isinstance(descriptor, RecordDomain): + for field_name, domain_name in descriptor.fields.items(): + if domain_name not in self.domains: + raise ValueError( + f"record domain '{name}' field '{field_name}' references unknown domain '{domain_name}'" + ) + + def _validate_acyclic_records(self) -> None: + # DFS over record field references; ADR-070 §3 admits only acyclic + # record/product structure. + visiting: set[str] = set() + done: set[str] = set() + + def visit(name: str) -> None: + if name in done: + return + if name in visiting: + raise ValueError(f"record domain reference cycle through '{name}'") + descriptor = self.domains.get(name) + visiting.add(name) + if isinstance(descriptor, RecordDomain): + for domain_name in descriptor.fields.values(): + visit(domain_name) + visiting.discard(name) + done.add(name) + + for name in self.domains: + visit(name) + + def _validate_bindings(self) -> None: + seen: dict[tuple[str, str], EnvelopeBinding] = {} + for binding in self.bindings: + if binding.domain is not None and binding.domain not in self.domains: + raise ValueError(f"binding path '{binding.path}' references unknown domain '{binding.domain}'") + if binding.posture is Posture.EXACT and binding.domain is not None: + if not _is_singleton_domain(self.domains[binding.domain]): + raise ValueError(f"exact posture binding path '{binding.path}' requires a singleton domain") + key = (binding.path, binding.scope.value) + existing = seen.get(key) + if existing is not None and (existing.domain, existing.posture) != (binding.domain, binding.posture): + # Equal-specificity, incompatible binding is invalid, not + # merge-order dependent (envelope-semantics.md R2). + raise ValueError( + f"conflicting equal-specificity bindings for path '{binding.path}' at scope '{binding.scope.value}'" + ) + seen[key] = binding + + def _validate_witness_policy(self) -> None: + if self.witness_policy is None: + return + for domain_name, value in self.witness_policy.selections.items(): + descriptor = self.domains.get(domain_name) + if descriptor is None: + raise ValueError(f"witness policy selects unknown domain '{domain_name}'") + if not scalar_in_domain(value, descriptor): + raise ValueError(f"witness policy selection for domain '{domain_name}' is not a domain member") + + +# Per-domain-kind predicates, dispatched by type to keep the public entry points +# flat (SonarCloud caps returns/complexity per function; type-dispatch chains +# otherwise trip S1142 / cognitive-complexity). +_SINGLETON_DOMAIN_CHECKS: dict[type, Callable[..., bool]] = { + ExactDomain: lambda descriptor: True, + EnumDomain: lambda descriptor: len(descriptor.values) == 1, + BooleanDomain: lambda descriptor: descriptor.value is not None, + NumericIntervalDomain: lambda descriptor: descriptor.lower == descriptor.upper, + GovernedReferenceDomain: lambda descriptor: len(descriptor.allowed_refs) == 1, +} + + +def _is_singleton_domain(descriptor: DomainDescriptor) -> bool: + check = _SINGLETON_DOMAIN_CHECKS.get(type(descriptor)) + return bool(check(descriptor)) if check is not None else False + + +def scalar_matches_numeric_type(value: object, numeric_type: NumericType) -> bool: + """Return whether ``value`` is a number of the declared numeric type. + + ``bool`` is never a number here (it is its own domain kind). + """ + if isinstance(value, bool): + return False + if numeric_type is NumericType.INTEGER: + return isinstance(value, int) + return isinstance(value, (int, float)) + + +def _scalar_eq(value: object, member: DomainScalar) -> bool: + """Type-strict scalar equality so ``True`` never equals ``1``.""" + return type(value) is type(member) and value == member + + +def _exact_member(value: object, descriptor: ExactDomain) -> bool: + return _scalar_eq(value, descriptor.value) + + +def _enum_member(value: object, descriptor: EnumDomain) -> bool: + return any(_scalar_eq(value, member) for member in descriptor.values) + + +def _boolean_member(value: object, descriptor: BooleanDomain) -> bool: + return isinstance(value, bool) and (descriptor.value is None or value == descriptor.value) + + +def _interval_member(value: object, descriptor: NumericIntervalDomain) -> bool: + if not scalar_matches_numeric_type(value, descriptor.numeric_type): + return False + # narrowed to a number by scalar_matches_numeric_type above + numeric = float(value) + lower_ok = numeric >= descriptor.lower if descriptor.lower_closed else numeric > descriptor.lower + upper_ok = numeric <= descriptor.upper if descriptor.upper_closed else numeric < descriptor.upper + return lower_ok and upper_ok + + +def _governed_member(value: object, descriptor: GovernedReferenceDomain) -> bool: + return isinstance(value, str) and value in descriptor.allowed_refs + + +_SCALAR_MEMBER_CHECKS: dict[type, Callable[..., bool]] = { + ExactDomain: _exact_member, + EnumDomain: _enum_member, + BooleanDomain: _boolean_member, + NumericIntervalDomain: _interval_member, + GovernedReferenceDomain: _governed_member, +} + + +def scalar_in_domain(value: object, descriptor: DomainDescriptor) -> bool: + """Structural membership for the scalar domain kinds. + + The single scalar-membership engine, hosted in the contract layer so both the + contract's own ``witness_policy`` validation and the relation in + ``aces_sdl.realization_envelope`` (which cannot import this module without a + dependency cycle) share one definition. Record domains are product structures, + not scalars, and are handled by the relation engine. + """ + check = _SCALAR_MEMBER_CHECKS.get(type(descriptor)) + return check(value, descriptor) if check is not None else False + + +# Import after the expression types are defined: the carrier embeds +# RealizationEnvelopeModel and this module preserves the original public API. +from aces_contracts.realization_envelope_carrier import ( + BackendRealizationEnvelopeModel, + ConcernDisposition, + IntegerBoundsModel, + ObservationStrength, + RealizationConcern, + RealizationConcernDisclosureModel, + RealizerConfigurationModel, + TransformationKind, + realization_envelope_digest, + realizer_configuration_digest, + validate_backend_realization_envelope, +) diff --git a/implementations/python/packages/aces_contracts/realization_envelope_carrier.py b/implementations/python/packages/aces_contracts/realization_envelope_carrier.py new file mode 100644 index 000000000..2420f546f --- /dev/null +++ b/implementations/python/packages/aces_contracts/realization_envelope_carrier.py @@ -0,0 +1,311 @@ +"""Published configuration-bound backend realization-envelope carrier.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping +from enum import Enum +from typing import Annotated, Any, Literal + +from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from aces_contracts.contracts import ContractModel, NonEmptyString, RealizationEnvelopeIdentityModel +from aces_contracts.realization_envelope import RealizationEnvelopeModel + +DigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] + + +class ObservationStrength(str, Enum): + """Strongest evidence a backend configuration emits for one concern.""" + + NONE = "none" + DRIVER_REPORTED = "driver-reported" + DAEMON_OBSERVED = "daemon-observed" + GUEST_OBSERVED = "guest-observed" + + +class ConcernDisposition(str, Enum): + """How the selected realizer treats a governed concern.""" + + REALIZED = "realized" + TRANSFORMED = "transformed" + DESCRIPTOR_ONLY = "descriptor-only" + UNSUPPORTED = "unsupported" + + +class RealizationConcern(str, Enum): + """Closed concern taxonomy shared by backend envelope artifacts.""" + + TOPOLOGY = "topology" + ARCHITECTURE = "architecture" + IMAGE = "image" + RESOURCE_ALLOCATION = "resource-allocation" + NETWORK = "network" + CONTENT_PLACEMENT = "content-placement" + ACCOUNT_PLACEMENT = "account-placement" + FEATURE_BINDING = "feature-binding" + SERVICE = "service" + ACL = "acl" + + +class TransformationKind(str, Enum): + """Portable disclosure of a material realization transformation.""" + + BOUNDED_NORMALIZATION = "bounded-normalization" + DEFAULT_SUBSTITUTION = "default-substitution" + DESCRIPTOR_SUBSTITUTION = "descriptor-substitution" + IMAGE_SUBSTITUTION = "image-substitution" + SERVICE_SYNTHESIS = "service-synthesis" + + +class IntegerBoundsModel(ContractModel): + """Closed positive integer interval used by realizer resource claims.""" + + minimum: int = Field(ge=1) + maximum: int | None = Field(default=None, ge=1) + + @model_validator(mode="after") + def _validate_bounds(self) -> IntegerBoundsModel: + if self.maximum is not None and self.maximum < self.minimum: + raise ValueError("maximum must not be less than minimum") + return self + + +class RealizerConfigurationModel(ContractModel): + """Secret-free material configuration identity for one realizer mode.""" + + mode: NonEmptyString + configuration_digest: DigestString + architecture: NonEmptyString + image_policy: NonEmptyString + network_policy: NonEmptyString + supported_node_types: list[NonEmptyString] = Field(min_length=1) + supported_os_families: list[NonEmptyString] = Field(min_length=1) + supported_content_types: list[NonEmptyString] = Field(default_factory=list) + supported_account_features: list[NonEmptyString] = Field(default_factory=list) + supports_acls: bool = False + memory_mib: IntegerBoundsModel + vcpus: IntegerBoundsModel + + @model_validator(mode="after") + def _validate_unique_terms(self) -> RealizerConfigurationModel: + for field_name in ( + "supported_node_types", + "supported_os_families", + "supported_content_types", + "supported_account_features", + ): + values = getattr(self, field_name) + if len(values) != len(set(values)): + raise ValueError(f"{field_name} must not contain duplicates") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler.resolve_ref_schema(handler(core_schema)) + properties = json_schema.get("properties", {}) + for field_name in ( + "supported_node_types", + "supported_os_families", + "supported_content_types", + "supported_account_features", + ): + properties[field_name]["uniqueItems"] = True + return json_schema + + +class RealizationConcernDisclosureModel(ContractModel): + """Typed support, transformation, and observation claim for one concern.""" + + concern: RealizationConcern + disposition: ConcernDisposition + observation_strength: ObservationStrength + mechanism: NonEmptyString | None = None + transformations: list[TransformationKind] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_disposition(self) -> RealizationConcernDisclosureModel: + if len(self.transformations) != len(set(self.transformations)): + raise ValueError("transformations must not contain duplicates") + if self.disposition is ConcernDisposition.TRANSFORMED and not self.transformations: + raise ValueError("transformed disposition requires transformations") + if self.disposition is not ConcernDisposition.TRANSFORMED and self.transformations: + raise ValueError("transformations require transformed disposition") + if self.disposition is ConcernDisposition.UNSUPPORTED: + if self.observation_strength is not ObservationStrength.NONE or self.mechanism is not None: + raise ValueError("unsupported disposition cannot claim observation or mechanism") + elif self.observation_strength is ObservationStrength.NONE or self.mechanism is None: + raise ValueError("supported dispositions require observation and mechanism") + 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"]["transformations"]["uniqueItems"] = True + json_schema.setdefault("allOf", []).extend( + [ + { + "if": {"properties": {"disposition": {"const": "transformed"}}}, + "then": {"properties": {"transformations": {"minItems": 1}}}, + "else": {"properties": {"transformations": {"maxItems": 0}}}, + }, + { + "if": {"properties": {"disposition": {"const": "unsupported"}}}, + "then": { + "properties": { + "observation_strength": {"const": "none"}, + "mechanism": {"type": "null"}, + } + }, + "else": { + "properties": { + "observation_strength": {"not": {"const": "none"}}, + "mechanism": {"type": "string", "minLength": 1}, + } + }, + }, + ] + ) + return json_schema + + +def realizer_configuration_digest(payload: Mapping[str, Any] | RealizerConfigurationModel) -> str: + """Digest the closed, secret-free material configuration projection.""" + + material = payload.model_dump(mode="json") if isinstance(payload, RealizerConfigurationModel) else dict(payload) + material.pop("configuration_digest", None) + canonical = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + return "sha256:" + hashlib.sha256(canonical).hexdigest() + + +def realization_envelope_digest(payload: Mapping[str, Any] | ContractModel) -> str: + """Return the canonical digest of a published envelope, excluding its self-digest.""" + + if isinstance(payload, BackendRealizationEnvelopeModel): + material = payload.model_dump(mode="json") + elif isinstance(payload, Mapping) and {"id", "expression", "configuration", "concerns"} <= payload.keys(): + material = { + "schema_version": payload.get("schema_version", "realization-envelope/v1"), + "contract_id": payload.get("contract_id", "realization-envelope-v1"), + "id": payload["id"], + "expression": RealizationEnvelopeModel.model_validate(payload["expression"]).model_dump(mode="json"), + "configuration": RealizerConfigurationModel.model_validate(payload["configuration"]).model_dump( + mode="json" + ), + "concerns": [ + RealizationConcernDisclosureModel.model_validate(claim).model_dump(mode="json") + for claim in payload["concerns"] + ], + } + else: + material = payload.model_dump(mode="json") if isinstance(payload, ContractModel) else dict(payload) + material.pop("digest", None) + canonical = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + return "sha256:" + hashlib.sha256(canonical).hexdigest() + + +class BackendRealizationEnvelopeModel(ContractModel): + """Published backend carrier: shared set expression plus truthful realization claims.""" + + schema_version: Literal["realization-envelope/v1"] = "realization-envelope/v1" + contract_id: Literal["realization-envelope-v1"] = "realization-envelope-v1" + id: NonEmptyString + expression: RealizationEnvelopeModel + configuration: RealizerConfigurationModel + concerns: list[RealizationConcernDisclosureModel] = Field(min_length=1) + digest: DigestString + + @model_validator(mode="after") + def _validate_carrier(self) -> BackendRealizationEnvelopeModel: + concern_values = [claim.concern for claim in self.concerns] + if len(concern_values) != len(set(concern_values)): + raise ValueError("concerns must not contain duplicate concern values") + missing_concerns = set(RealizationConcern) - set(concern_values) + if missing_concerns: + missing = ", ".join(sorted(concern.value for concern in missing_concerns)) + raise ValueError(f"concerns must disclose every governed concern; missing: {missing}") + if self.configuration.configuration_digest != realizer_configuration_digest(self.configuration): + raise ValueError("realizer configuration digest does not match canonical content") + expected = realization_envelope_digest(self) + if self.digest != expected: + raise ValueError("realization envelope digest does not match canonical content") + return self + + @property + def identity(self) -> RealizationEnvelopeIdentityModel: + return RealizationEnvelopeIdentityModel( + contract_id=self.contract_id, + envelope_id=self.id, + schema_version=self.schema_version, + digest=self.digest, + configuration_digest=self.configuration.configuration_digest, + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler.resolve_ref_schema(handler(core_schema)) + concerns = json_schema["properties"]["concerns"] + concern_count = len(RealizationConcern) + concerns["minItems"] = concern_count + concerns["maxItems"] = concern_count + concerns["allOf"] = [ + { + "contains": { + "type": "object", + "required": ["concern"], + "properties": {"concern": {"const": concern.value}}, + }, + "minContains": 1, + "maxContains": 1, + } + for concern in RealizationConcern + ] + json_schema["x-aces-invariants"] = [ + { + "id": "realization-envelope-canonical-semantics-valid", + "description": ( + "Configuration bounds, expression references, canonical configuration and envelope digests, " + "and all cross-field realization disclosure semantics must validate together." + ), + "level": "error", + "validator": "aces_contracts.realization_envelope.validate_backend_realization_envelope", + "inputs": [{"contract_id": "realization-envelope-v1", "instance_path": "#"}], + } + ] + return json_schema + + +def validate_backend_realization_envelope(payload: Mapping[str, Any]) -> BackendRealizationEnvelopeModel: + """Apply the semantic-invariant validator named by the published schema.""" + + return BackendRealizationEnvelopeModel.model_validate(payload) + + +__all__ = [ + "BackendRealizationEnvelopeModel", + "ConcernDisposition", + "IntegerBoundsModel", + "ObservationStrength", + "RealizationConcern", + "RealizationConcernDisclosureModel", + "RealizerConfigurationModel", + "TransformationKind", + "realization_envelope_digest", + "realizer_configuration_digest", + "validate_backend_realization_envelope", +] diff --git a/implementations/python/packages/aces_contracts/runtime_state.py b/implementations/python/packages/aces_contracts/runtime_state.py index 0d7e45155..c3e7215e4 100644 --- a/implementations/python/packages/aces_contracts/runtime_state.py +++ b/implementations/python/packages/aces_contracts/runtime_state.py @@ -5,14 +5,18 @@ from collections.abc import Mapping from dataclasses import dataclass, field from enum import Enum -from typing import Any +from typing import TYPE_CHECKING, Any from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance +from aces_contracts.addressing import require_compiled_address from aces_contracts.diagnostics import Diagnostic from aces_contracts.planning import RuntimeDomain from aces_contracts.versions import OPERATION_SCHEMA_VERSION, RUNTIME_SNAPSHOT_SCHEMA_VERSION +if TYPE_CHECKING: + from aces_contracts.contracts import RealizationEnvelopeIdentityModel + class OperationState(str, Enum): """Lifecycle for async control-plane operations.""" @@ -36,6 +40,11 @@ class SnapshotEntry: refresh_dependencies: tuple[str, ...] = () status: str = "ready" + def __post_init__(self) -> None: + require_compiled_address(self.address) + for dependency in (*self.ordering_dependencies, *self.refresh_dependencies): + require_compiled_address(dependency, field_name="dependency address") + @dataclass(frozen=True) class RealizationProvenanceEntry: @@ -68,6 +77,7 @@ class RuntimeSnapshot: orchestration_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) evaluation_results: dict[str, dict[str, Any]] = field(default_factory=dict) evaluation_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) + proposition_truth_results: dict[str, dict[str, Any]] = field(default_factory=dict) 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) @@ -78,8 +88,15 @@ class RuntimeSnapshot: # SEM-218 invariant I5: per-concern provenance for realized realization # concerns recorded across this snapshot's result / history surfaces. realization_provenance: tuple[RealizationProvenanceEntry, ...] = () + realization_envelope: RealizationEnvelopeIdentityModel | None = None metadata: dict[str, Any] = field(default_factory=dict) + def __post_init__(self) -> None: + for map_key, entry in self.entries.items(): + require_compiled_address(map_key, field_name="snapshot map key") + if map_key != entry.address: + raise ValueError("RuntimeSnapshot entries map key must equal embedded address") + def get(self, address: str) -> SnapshotEntry | None: return self.entries.get(address) @@ -106,6 +123,11 @@ def with_entries( ), 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", @@ -146,6 +168,11 @@ def with_entries( "realization_provenance", self.realization_provenance, ), + realization_envelope=_identity_update( + updates, + "realization_envelope", + self.realization_envelope, + ), metadata=_mapping_update(updates, "metadata", self.metadata), ) @@ -155,6 +182,7 @@ def with_entries( "orchestration_history", "evaluation_results", "evaluation_history", + "proposition_truth_results", "participant_episode_results", "participant_episode_history", "participant_behavior_history", @@ -163,6 +191,7 @@ def with_entries( "joint_action_records", "time_management_contexts", "realization_provenance", + "realization_envelope", "metadata", } @@ -212,6 +241,21 @@ def _provenance_update( return raw +def _identity_update( + updates: Mapping[str, object], + key: str, + current: RealizationEnvelopeIdentityModel | None, +) -> RealizationEnvelopeIdentityModel | None: + from aces_contracts.contracts import RealizationEnvelopeIdentityModel + + raw = updates.get(key) + if raw is None: + return current + if not isinstance(raw, RealizationEnvelopeIdentityModel): + raise TypeError(f"{key} must be a RealizationEnvelopeIdentityModel") + return raw + + @dataclass class ApplyResult: """Result of applying or starting a runtime plan.""" @@ -222,6 +266,9 @@ class ApplyResult: changed_addresses: list[str] = field(default_factory=list) details: dict[str, Any] = field(default_factory=dict) + def __post_init__(self) -> None: + _validate_changed_addresses(self.changed_addresses) + @dataclass(frozen=True) class OperationReceipt: @@ -248,6 +295,16 @@ class OperationStatus: diagnostics: list[Diagnostic] = field(default_factory=list) changed_addresses: list[str] = field(default_factory=list) + def __post_init__(self) -> None: + _validate_changed_addresses(self.changed_addresses) + + +def _validate_changed_addresses(addresses: list[str]) -> None: + for address in addresses: + require_compiled_address(address, field_name="changed address") + if len(addresses) != len(set(addresses)): + raise ValueError("changed addresses must be unique") + @dataclass(frozen=True) class RuntimeSnapshotEnvelope: diff --git a/implementations/python/packages/aces_contracts/scientific_completeness.py b/implementations/python/packages/aces_contracts/scientific_completeness.py new file mode 100644 index 000000000..89e287d85 --- /dev/null +++ b/implementations/python/packages/aces_contracts/scientific_completeness.py @@ -0,0 +1,271 @@ +"""Scientific-scenario completeness taxonomy and delivery assessment contracts.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from functools import cache +from pathlib import Path +from typing import Annotated, Literal + +from pydantic import AfterValidator, Field, model_validator + +from .contracts import ContractModel, NonEmptyString +from .corpus import PROFILES, corpus_family_root +from .versions import ( + SCIENTIFIC_COMPLETENESS_ASSESSMENT_SCHEMA_VERSION, + SCIENTIFIC_COMPLETENESS_TAXONOMY_SCHEMA_VERSION, +) + +ProfileId = Annotated[str, Field(pattern=r"^[a-z][a-z0-9-]*$")] +ConcernId = Annotated[str, Field(pattern=r"^[a-z][a-z0-9-]*$")] +RevisionId = Annotated[str, Field(pattern=r"^[a-z0-9][a-z0-9.-]*$")] +IssueRef = Annotated[str, Field(pattern=r"^#[1-9][0-9]*$")] +ContractId = Annotated[str, Field(pattern=r"^[a-z0-9][a-z0-9-]*-v[1-9][0-9]*$")] + + +def _validate_repo_path(value: str) -> str: + if value.startswith("/") or ".." in Path(value).parts: + raise ValueError("repository path must be relative and must not contain parent traversal") + return value + + +RepoPath = Annotated[str, Field(pattern=r"^[^/\s][^\s]*$"), AfterValidator(_validate_repo_path)] + + +class ProfileDisposition(str, Enum): + REQUIRED = "required" + ALLOWED_UNDERSPECIFIED = "allowed-underspecified" + EXCLUDED = "excluded" + + +class DeliveryStatus(str, Enum): + IMPLEMENTED = "implemented" + PARTIAL = "partial" + EXTERNAL_CONTRACT = "external-contract" + DELIBERATELY_EXCLUDED = "deliberately-excluded" + MISSING = "missing" + + +class EvidenceKind(str, Enum): + NORMATIVE_SPEC = "normative-spec" + PUBLISHED_SCHEMA = "published-schema" + CONTRACT = "contract" + IMPLEMENTATION = "implementation" + TEST = "test" + CONFORMANCE = "conformance" + EXAMPLE = "example" + DOCUMENTATION = "documentation" + + +_EXECUTABLE_EVIDENCE_KINDS = { + EvidenceKind.IMPLEMENTATION, + EvidenceKind.TEST, + EvidenceKind.CONFORMANCE, +} + + +class CompletenessConcernModel(ContractModel): + concern_id: ConcernId + title: NonEmptyString + definition: NonEmptyString + semantic_owner: NonEmptyString + + +class CompletenessProfileModel(ContractModel): + profile_id: ProfileId + title: NonEmptyString + intended_claim: NonEmptyString + explicit_non_claims: list[NonEmptyString] = Field(min_length=1) + dispositions: dict[ConcernId, ProfileDisposition] + example_refs: list[RepoPath] = Field(default_factory=list) + + +class ScientificCompletenessTaxonomyModel(ContractModel): + schema_version: Literal[SCIENTIFIC_COMPLETENESS_TAXONOMY_SCHEMA_VERSION] = ( + SCIENTIFIC_COMPLETENESS_TAXONOMY_SCHEMA_VERSION + ) + profile_family: Literal["scientific-scenario-completeness"] = "scientific-scenario-completeness" + revision: RevisionId + concerns: list[CompletenessConcernModel] = Field(min_length=1) + profiles: list[CompletenessProfileModel] = Field(min_length=1) + + @model_validator(mode="after") + def validate_taxonomy_identity(self) -> ScientificCompletenessTaxonomyModel: + concern_ids = [concern.concern_id for concern in self.concerns] + if len(concern_ids) != len(set(concern_ids)): + raise ValueError("taxonomy concern ids must be unique") + profile_ids = [profile.profile_id for profile in self.profiles] + if len(profile_ids) != len(set(profile_ids)): + raise ValueError("taxonomy profile ids must be unique") + expected = set(concern_ids) + for profile in self.profiles: + if set(profile.dispositions) != expected: + raise ValueError(f"profile {profile.profile_id!r} dispositions must exactly cover taxonomy concerns") + return self + + +class CompletenessEvidenceModel(ContractModel): + kind: EvidenceKind + path: RepoPath + claim: NonEmptyString + contract_id: ContractId | None = None + + @model_validator(mode="after") + def validate_contract_evidence(self) -> CompletenessEvidenceModel: + if self.kind in {EvidenceKind.CONTRACT, EvidenceKind.PUBLISHED_SCHEMA} and self.contract_id is None: + raise ValueError("contract and published-schema evidence require contract_id") + if self.kind not in {EvidenceKind.CONTRACT, EvidenceKind.PUBLISHED_SCHEMA} and self.contract_id is not None: + raise ValueError("contract_id is valid only for contract or published-schema evidence") + return self + + +class ConcernDeliveryAssessmentModel(ContractModel): + concern_id: ConcernId + status: DeliveryStatus + evidence: list[CompletenessEvidenceModel] = Field(default_factory=list) + limitation: NonEmptyString + issue_refs: list[IssueRef] = Field(default_factory=list) + external_contract_refs: list[ContractId] = Field(default_factory=list) + satisfiability_witness_refs: dict[ContractId, RepoPath] = Field(default_factory=dict) + binding_obligation: NonEmptyString | None = None + exclusion_rationale: NonEmptyString | None = None + + def _validate_implemented_evidence(self) -> None: + if self.status is DeliveryStatus.IMPLEMENTED and not any( + item.kind in _EXECUTABLE_EVIDENCE_KINDS for item in self.evidence + ): + raise ValueError("implemented status requires executable evidence") + + def _validate_external_contract(self) -> None: + if self.status is not DeliveryStatus.EXTERNAL_CONTRACT: + if self._has_external_contract_bindings(): + raise ValueError("external contract bindings are valid only for external-contract status") + return + self._validate_external_contract_bindings() + + def _has_external_contract_bindings(self) -> bool: + return bool( + self.external_contract_refs or self.satisfiability_witness_refs or self.binding_obligation is not None + ) + + def _validate_external_contract_bindings(self) -> None: + if not self.external_contract_refs or not self.satisfiability_witness_refs or self.binding_obligation is None: + raise ValueError( + "external-contract status requires named contract refs, " + "satisfiability witnesses, and a binding obligation" + ) + evidence_contracts = {item.contract_id for item in self.evidence if item.contract_id is not None} + if not set(self.external_contract_refs).issubset(evidence_contracts): + raise ValueError("external-contract status requires evidence for every named contract") + if set(self.satisfiability_witness_refs) != set(self.external_contract_refs): + raise ValueError( + "external-contract status requires exactly one satisfiability witness for every named contract" + ) + + def _validate_exclusion(self) -> None: + if self.status is DeliveryStatus.DELIBERATELY_EXCLUDED: + if self.exclusion_rationale is None: + raise ValueError("deliberately-excluded status requires exclusion_rationale") + elif self.exclusion_rationale is not None: + raise ValueError("exclusion_rationale is valid only for deliberately-excluded status") + + def _validate_issue_refs(self) -> None: + if self.status in {DeliveryStatus.PARTIAL, DeliveryStatus.MISSING} and not self.issue_refs: + raise ValueError("partial and missing statuses require at least one issue ref") + + @model_validator(mode="after") + def validate_status_evidence(self) -> ConcernDeliveryAssessmentModel: + self._validate_implemented_evidence() + self._validate_external_contract() + self._validate_exclusion() + self._validate_issue_refs() + return self + + +class ScientificCompletenessAssessmentModel(ContractModel): + schema_version: Literal[SCIENTIFIC_COMPLETENESS_ASSESSMENT_SCHEMA_VERSION] = ( + SCIENTIFIC_COMPLETENESS_ASSESSMENT_SCHEMA_VERSION + ) + profile_family: Literal["scientific-scenario-completeness"] = "scientific-scenario-completeness" + taxonomy_revision: RevisionId + assessment_revision: RevisionId + assessed_on: Annotated[str, Field(pattern=r"^\d{4}-\d{2}-\d{2}$")] + concerns: list[ConcernDeliveryAssessmentModel] = Field(min_length=1) + + @model_validator(mode="after") + def validate_assessment_identity(self) -> ScientificCompletenessAssessmentModel: + concern_ids = [item.concern_id for item in self.concerns] + if len(concern_ids) != len(set(concern_ids)): + raise ValueError("assessment concern ids must be unique") + return self + + +@dataclass(frozen=True) +class ProfileCompletenessResult: + profile_id: str + complete: bool + blocking_concerns: tuple[str, ...] + + +def evaluate_profile_completeness( + taxonomy: ScientificCompletenessTaxonomyModel, + assessment: ScientificCompletenessAssessmentModel, +) -> tuple[ProfileCompletenessResult, ...]: + """Join one taxonomy revision to one assessment and compute outcomes.""" + + if assessment.profile_family != taxonomy.profile_family: + raise ValueError("assessment profile_family must match taxonomy") + if assessment.taxonomy_revision != taxonomy.revision: + raise ValueError("assessment taxonomy_revision must match taxonomy revision") + taxonomy_ids = {concern.concern_id for concern in taxonomy.concerns} + assessment_by_id = {item.concern_id: item for item in assessment.concerns} + if set(assessment_by_id) != taxonomy_ids: + raise ValueError("assessment concerns must exactly cover taxonomy concerns") + satisfying = {DeliveryStatus.IMPLEMENTED, DeliveryStatus.EXTERNAL_CONTRACT} + results: list[ProfileCompletenessResult] = [] + for profile in taxonomy.profiles: + blocking = tuple( + sorted( + concern_id + for concern_id, disposition in profile.dispositions.items() + if disposition is ProfileDisposition.REQUIRED and assessment_by_id[concern_id].status not in satisfying + ) + ) + results.append( + ProfileCompletenessResult( + profile_id=profile.profile_id, + complete=not blocking, + blocking_concerns=blocking, + ) + ) + return tuple(results) + + +def scientific_completeness_root() -> Path: + return corpus_family_root(PROFILES) / "scientific-completeness" + + +@cache +def load_scientific_completeness_taxonomy() -> ScientificCompletenessTaxonomyModel: + path = scientific_completeness_root() / "scientific-scenario-completeness-rev1.json" + return ScientificCompletenessTaxonomyModel.model_validate_json(path.read_text(encoding="utf-8")) + + +@cache +def load_scientific_completeness_assessment() -> ScientificCompletenessAssessmentModel: + path = scientific_completeness_root() / "delivery-assessment-2026-07-12.json" + return ScientificCompletenessAssessmentModel.model_validate_json(path.read_text(encoding="utf-8")) + + +__all__ = [ + "DeliveryStatus", + "ProfileCompletenessResult", + "ProfileDisposition", + "ScientificCompletenessAssessmentModel", + "ScientificCompletenessTaxonomyModel", + "evaluate_profile_completeness", + "load_scientific_completeness_assessment", + "load_scientific_completeness_taxonomy", + "scientific_completeness_root", +] diff --git a/implementations/python/packages/aces_contracts/versions.py b/implementations/python/packages/aces_contracts/versions.py index 590b15b52..c06613db4 100644 --- a/implementations/python/packages/aces_contracts/versions.py +++ b/implementations/python/packages/aces_contracts/versions.py @@ -2,6 +2,7 @@ SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION = "scenario-instantiation/v1" BACKEND_MANIFEST_V2_SCHEMA_VERSION = "backend-manifest/v2" +REALIZATION_ENVELOPE_SCHEMA_VERSION = "realization-envelope/v1" PROCESSOR_MANIFEST_V2_SCHEMA_VERSION = "processor-manifest/v2" PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION = "participant-implementation-manifest/v1" PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION = "participant-implementation-provenance/v1" @@ -18,6 +19,10 @@ RUNTIME_SNAPSHOT_SCHEMA_VERSION = "runtime-snapshot/v1" OPERATION_SCHEMA_VERSION = "runtime-operation/v1" EVALUATION_STATE_SCHEMA_VERSION = "evaluation-result-state/v1" +PROPOSITION_TRUTH_RESULT_SCHEMA_VERSION = "proposition-truth-result/v1" +SDL_LINEAGE_LEDGER_SCHEMA_VERSION = "sdl-lineage-ledger/v1" +SCIENTIFIC_COMPLETENESS_TAXONOMY_SCHEMA_VERSION = "scientific-completeness-taxonomy/v1" +SCIENTIFIC_COMPLETENESS_ASSESSMENT_SCHEMA_VERSION = "scientific-completeness-assessment/v1" PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION = "participant-episode-state/v1" PARTICIPANT_LIFECYCLE_EVENT_V1_SCHEMA_VERSION = "participant-lifecycle-event/v1" PARTICIPANT_OBSERVATION_ENVELOPE_V1_SCHEMA_VERSION = "participant-observation-envelope/v1" @@ -33,4 +38,6 @@ EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION = "experiment-capture-spec/v1" 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" REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION = "reusable-asset-trust-policy/v1" +ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION = "associated-artifact-manifest/v1" diff --git a/implementations/python/packages/aces_mcp/server.py b/implementations/python/packages/aces_mcp/server.py index cb7978233..e94d21660 100644 --- a/implementations/python/packages/aces_mcp/server.py +++ b/implementations/python/packages/aces_mcp/server.py @@ -11,6 +11,8 @@ from mcp.server.fastmcp import FastMCP from aces_mcp.tools.authoring import register as register_authoring_tools +from aces_mcp.tools.completeness import register as register_completeness_tools +from aces_mcp.tools.experiment_authoring import register as register_experiment_authoring_tools from aces_mcp.tools.inspection import register as register_inspection_tools from aces_mcp.tools.language_service import register as register_language_service_tools from aces_mcp.tools.operations import register as register_operation_tools @@ -27,14 +29,22 @@ Start with `aces_tool_surface` to understand the available tool families, \ then use `aces_agent_guidance` for scope boundaries, invariants, review \ -priorities, and safe-operating expectations. Use `sdl_overview` to orient \ +priorities, and safe-operating expectations. Use `aces_intended_use_profiles` \ +to select the claim scope and inspect current ACES delivery blockers. Use \ +`sdl_overview` to orient \ yourself. Use `sdl_section_reference` for any section you need to understand. \ Use `sdl_get_example` to see real-world annotated scenarios. Use \ `sdl_completions`, `sdl_references`, \ `sdl_format`, `sdl_diagnostics`, and `sdl_apply_edit` for language-service \ workflows. Use `sdl_validate`, `sdl_design_assessment`, `sdl_plan`, and \ `sdl_claims_assessment` to check SDL YAML and avoid overstating what a \ -scenario or dry run can prove.\ +scenario or dry run can prove. + +To author an *experiment* (the pre-run specification that binds a task to a \ +run plan — seeds, episode controls, red-variant selection, and replication — \ +distinct from the archival run/study records), use `experiment_scaffold` to \ +start, `experiment_get_example` to see a worked design, and `experiment_validate` \ +to check it.\ """ @@ -45,7 +55,9 @@ def create_server() -> FastMCP: instructions=_INSTRUCTIONS, ) register_reference_tools(mcp) + register_completeness_tools(mcp) register_authoring_tools(mcp) + register_experiment_authoring_tools(mcp) register_language_service_tools(mcp) register_inspection_tools(mcp) register_operation_tools(mcp) diff --git a/implementations/python/packages/aces_mcp/tools/authoring.py b/implementations/python/packages/aces_mcp/tools/authoring.py index f0e7f1f80..45f55d71d 100644 --- a/implementations/python/packages/aces_mcp/tools/authoring.py +++ b/implementations/python/packages/aces_mcp/tools/authoring.py @@ -29,17 +29,20 @@ def register(mcp: FastMCP) -> None: "Pass the full YAML scenario text as `sdl_content`. Optionally " "set `structural_only=true` to skip semantic cross-reference " "checks (useful for work-in-progress fragments that aren't " - "complete yet)." + "complete yet). Set `accept_migration_syntax=true` only when " + "migrating legacy field spellings; canonical validation is strict." ), ) def sdl_validate( sdl_content: str, structural_only: bool = False, + accept_migration_syntax: bool = False, ) -> str: if len(sdl_content.encode("utf-8", errors="replace")) > _MAX_INPUT_BYTES: return f"INPUT TOO LARGE — limit is {_MAX_INPUT_BYTES} bytes." from aces_sdl import ( + SDLMigrationPolicy, SDLParseError, SDLValidationError, parse_sdl, @@ -49,6 +52,7 @@ def sdl_validate( scenario = parse_sdl( sdl_content, skip_semantic_validation=structural_only, + migration_policy=(SDLMigrationPolicy.ACCEPT if accept_migration_syntax else SDLMigrationPolicy.REJECT), ) except SDLParseError as exc: return ( @@ -78,6 +82,12 @@ def sdl_validate( for adv in scenario.advisories: parts.append(f" - {adv}") + if scenario.source_diagnostics: + parts.append(f"\nSource migration advisories ({len(scenario.source_diagnostics)}):") + for diagnostic in scenario.source_diagnostics: + start = diagnostic.primary_range.start + parts.append(f" - [{diagnostic.code}] line {start.line}, column {start.column}: {diagnostic.message}") + if structural_only: parts.append( "\nNote: semantic validation was skipped. Run without " @@ -113,7 +123,7 @@ def sdl_validate_section( return f"INPUT TOO LARGE — limit is {_MAX_INPUT_BYTES} bytes." import yaml as _yaml - from aces_sdl import SDLParseError, SDLValidationError, parse_sdl + from aces_sdl import SDLParseError, SDLValidationError, load_sdl_fragment, parse_sdl section = section.strip().lower().replace("-", "_") valid_sections = { @@ -140,22 +150,28 @@ def sdl_validate_section( # Build a minimal valid wrapper try: - section_data = _yaml.safe_load(section_yaml) - except _yaml.YAMLError as exc: - return f"YAML ERROR in section content:\n{exc}" + section_data = load_sdl_fragment( + section_yaml, + mapping_keys="literal", + base_pointer=f"/{section}", + ) + except SDLParseError as exc: + label = "PARSE ERROR" if any(item.code != "sdl.parse" for item in exc.diagnostics) else "YAML ERROR" + return f"{label} in section content:\n{exc.details}" wrapper: dict = {} if context_yaml: try: - ctx = _yaml.safe_load(context_yaml) + ctx = load_sdl_fragment(context_yaml) if isinstance(ctx, dict): wrapper.update(ctx) - except _yaml.YAMLError as exc: - return f"YAML ERROR in context_yaml:\n{exc}" + except SDLParseError as exc: + label = "PARSE ERROR" if any(item.code != "sdl.parse" for item in exc.diagnostics) else "YAML ERROR" + return f"{label} in context_yaml:\n{exc.details}" # Force a safe synthetic name — always last so context_yaml cannot # override it and cause confusing error messages. - wrapper["name"] = "__mcp_validation_fragment" + wrapper["name"] = "mcp-validation-fragment" wrapper[section] = section_data combined = _yaml.dump(wrapper, default_flow_style=False, sort_keys=False) @@ -252,9 +268,12 @@ def sdl_instantiate( bullets = "\n".join(f" - {e}" for e in exc.errors) return f"INSTANTIATION ERRORS ({len(exc.errors)}):\n{bullets}" + binding_count = len(concrete.instantiation_provenance.bindings) + sum( + len(item.bindings) for item in concrete.instantiation_provenance.imports + ) parts = [ - f"INSTANTIATED — scenario '{concrete.name}' fully resolved.", - f"Parameters used: {concrete.instantiation_parameters}", + f"INSTANTIATED - scenario '{concrete.name}' fully resolved.", + f"Bindings resolved: {binding_count}", ] section_counts = _section_summary(concrete) if section_counts: @@ -379,9 +398,43 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: conditions: web-healthy: + proposition: web-available command: "curl -sf https://localhost/ || exit 1" interval: 15 +propositions: + web-available: + description: The web service reports its availability as healthy. + subjects: [nodes.web-server.services.https] + basis: observed_state + predicate: + kind: boolean + property: service-healthy + semantic_ref: urn:aces:observable:service-healthy + operator: equals + expected: true + evidence_requirements: [web-health-evidence] + +assertions: + web-healthy: + proposition: web-available + role: postcondition + +evidence_requirements: + web-health-evidence: + description: Capture evidence used to decide web availability. + source_refs: [nodes.web-server.services.https] + scope_refs: [nodes.web-server] + boundary_kind: objective_completion + channel: api_response + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: run_lifetime + loss_disclosure: required + vulnerabilities: sqli: name: SQL Injection @@ -397,7 +450,7 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: name: Red Team role: Red -# Objective success references observable state (conditions) per ADR-073. +# Objective success references assertions over typed propositions per ADR-079. # Graded scoring/reward, if a study needs it, lives in the experiment/evaluator # plane (ADR-055/064/069), not in the SDL. objectives: @@ -405,7 +458,7 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: description: Keep the web application available entity: blue-team success: - conditions: [web-healthy] + assertions: [web-healthy] accounts: web-admin-account: @@ -491,9 +544,43 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: conditions: web-healthy: + proposition: web-available command: "curl -sf https://localhost/ || exit 1" interval: 15 +propositions: + web-available: + description: The web service reports its availability as healthy. + subjects: [nodes.web-server.services.https] + basis: observed_state + predicate: + kind: boolean + property: service-healthy + semantic_ref: urn:aces:observable:service-healthy + operator: equals + expected: true + evidence_requirements: [web-health-evidence] + +assertions: + web-healthy: + proposition: web-available + role: postcondition + +evidence_requirements: + web-health-evidence: + description: Capture evidence used to decide web availability. + source_refs: [nodes.web-server.services.https] + scope_refs: [nodes.web-server] + boundary_kind: objective_completion + channel: api_response + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: run_lifetime + loss_disclosure: required + vulnerabilities: sqli: name: SQL Injection @@ -525,8 +612,8 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: scripts: main-timeline: - start-time: 0 - end-time: 4 hour + start_time: 0 + end_time: 4 hour speed: ${exercise_speed} events: attack-start: 30 min @@ -580,13 +667,13 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: actions: [Scan, Exploit] targets: [web-server, sqli] success: - conditions: [web-healthy] + assertions: [web-healthy] window: stories: [exercise] blue-defend: entity: blue-team success: - conditions: [web-healthy] + assertions: [web-healthy] depends_on: [red-access] # --- Workflows --- @@ -597,11 +684,11 @@ def _section_summary(scenario: object) -> list[tuple[str, int]]: run-attack: type: objective objective: red-access - on-success: run-defense + on_success: run-defense run-defense: type: objective objective: blue-defend - on-success: done + on_success: done done: type: end """ diff --git a/implementations/python/packages/aces_mcp/tools/completeness.py b/implementations/python/packages/aces_mcp/tools/completeness.py new file mode 100644 index 000000000..1273a1538 --- /dev/null +++ b/implementations/python/packages/aces_mcp/tools/completeness.py @@ -0,0 +1,138 @@ +"""MCP access to scientific-scenario intended-use profiles.""" + +from __future__ import annotations + +from collections import Counter +from typing import TYPE_CHECKING, Any + +from mcp.server.fastmcp import FastMCP + +from aces_mcp.tools.operation_support import json_response + +if TYPE_CHECKING: + from aces_contracts.scientific_completeness import CompletenessProfileModel, ProfileCompletenessResult + + +def _authoring_tools(profile_id: str) -> list[str]: + tools = [ + "sdl_overview", + "sdl_section_reference", + "sdl_scaffold", + "sdl_diagnostics", + "sdl_validate", + "sdl_design_assessment", + "sdl_plan", + "sdl_claims_assessment", + ] + if profile_id in {"controlled-experiment-scenario", "reproducible-benchmark-study-input"}: + tools.extend(["experiment_scaffold", "experiment_get_example", "experiment_validate"]) + return tools + + +def _profile_summary( + profile: CompletenessProfileModel, + outcome: ProfileCompletenessResult, +) -> dict[str, Any]: + disposition_counts = Counter(disposition.value for disposition in profile.dispositions.values()) + return { + "profile_id": profile.profile_id, + "title": profile.title, + "intended_claim": profile.intended_claim, + "disposition_counts": dict(sorted(disposition_counts.items())), + "aces_delivery": { + "complete": outcome.complete, + "blocking_concern_count": len(outcome.blocking_concerns), + "blocking_concern_ids": list(outcome.blocking_concerns), + }, + "explicit_non_claims": list(profile.explicit_non_claims), + "example_refs": list(profile.example_refs), + } + + +def intended_use_profiles(profile_id: str = "") -> dict[str, Any]: + """Return the canonical profile catalog or one selected profile.""" + + from aces_contracts.scientific_completeness import ( + evaluate_profile_completeness, + load_scientific_completeness_assessment, + load_scientific_completeness_taxonomy, + ) + + taxonomy = load_scientific_completeness_taxonomy() + assessment = load_scientific_completeness_assessment() + outcomes = {item.profile_id: item for item in evaluate_profile_completeness(taxonomy, assessment)} + profiles = {item.profile_id: item for item in taxonomy.profiles} + selected_id = profile_id.strip() + + base: dict[str, Any] = { + "status": "ok", + "scope": "aces-delivery-capability", + "profile_family": taxonomy.profile_family, + "taxonomy_revision": taxonomy.revision, + "assessment_revision": assessment.assessment_revision, + "assessed_on": assessment.assessed_on, + "scenario_assessment": { + "performed": False, + "status": "not-assessed", + "reason": ( + "This tool reports whether ACES delivers the concerns required by an intended-use profile. " + "It does not inspect or certify an individual scenario, experiment, backend, or run." + ), + }, + } + if not selected_id: + base["profiles"] = [_profile_summary(profile, outcomes[profile.profile_id]) for profile in taxonomy.profiles] + base["next_action"] = "Select one profile_id before authoring or making readiness claims." + return base + + if selected_id not in profiles: + return { + **base, + "status": "invalid", + "diagnostics": [ + { + "code": "aces.intended_use_profile.unknown", + "message": f"Unknown intended-use profile {selected_id!r}.", + "available_profile_ids": list(profiles), + } + ], + } + + profile = profiles[selected_id] + outcome = outcomes[selected_id] + concerns = {item.concern_id: item for item in taxonomy.concerns} + delivery = {item.concern_id: item for item in assessment.concerns} + required_ids = sorted( + concern_id for concern_id, disposition in profile.dispositions.items() if disposition.value == "required" + ) + selected = _profile_summary(profile, outcome) + selected["required_concerns"] = [ + { + **concerns[concern_id].model_dump(mode="json"), + **delivery[concern_id].model_dump(mode="json", exclude={"concern_id"}), + } + for concern_id in required_ids + ] + selected["next_tools"] = _authoring_tools(selected_id) + base["profile"] = selected + return base + + +def register(mcp: FastMCP) -> None: + """Register intended-use profile discovery on the MCP server.""" + + @mcp.tool( + name="aces_intended_use_profiles", + description=( + "List ACES scientific-scenario intended-use profiles or inspect one profile's " + "required concerns, current ACES delivery blockers, evidence, limitations, " + "nonclaims, and recommended authoring tools. Call without profile_id to discover " + "the catalog, then select a profile before authoring or making readiness claims. " + "This assesses ACES delivery capability, not an individual scenario or run." + ), + ) + def aces_intended_use_profiles(profile_id: str = "") -> str: + return json_response(intended_use_profiles(profile_id)) + + +__all__ = ["intended_use_profiles", "register"] diff --git a/implementations/python/packages/aces_mcp/tools/experiment_authoring.py b/implementations/python/packages/aces_mcp/tools/experiment_authoring.py new file mode 100644 index 000000000..34aff7523 --- /dev/null +++ b/implementations/python/packages/aces_mcp/tools/experiment_authoring.py @@ -0,0 +1,236 @@ +"""Experiment authoring tools — validate, scaffold, and retrieve experiment specs. + +These tools let agents author an experiment *specification* — the pre-run +authoring/input counterpart to the archival experiment-core outputs +(run/study/apparatus-context). An experiment spec binds a task to a run plan +(replication, seeds, episode controls, red-variant selection, condition +assignments) so an experiment can be specified and validated before execution. +See ADR-074 and specs/formal/experiment-core/. +""" + +from __future__ import annotations + +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + +# Maximum input size to prevent resource exhaustion via YAML bombs or +# extremely large payloads. 64 KiB matches the SDL authoring tools. +_MAX_INPUT_BYTES = 64 * 1024 + + +def _find_repo_root(start: Path) -> Path: + for candidate in (start, *start.parents): + if (candidate / ".git").exists() or (candidate / ".ground-control.yaml").exists(): + return candidate + raise RuntimeError(f"could not locate aces-sdl repo root from {start}") + + +_REPO_ROOT = _find_repo_root(Path(__file__).resolve().parent) +_EXAMPLES_DIR = _REPO_ROOT / "examples" / "experiments" + +_ALLOWED_EXAMPLES = { + "sweep": "techvault-red-tactic-sweep.exp.yaml", + "smoke": "techvault-smoke-run.exp.yaml", +} + + +def _run_experiment_validate(spec_content: str) -> str: + """Validate experiment-spec YAML and render a human-readable result.""" + if len(spec_content.encode("utf-8", errors="replace")) > _MAX_INPUT_BYTES: + return f"INPUT TOO LARGE — limit is {_MAX_INPUT_BYTES} bytes." + + from aces_contracts.experiment_spec import ( + ExperimentSpecValidationError, + parse_experiment_spec, + ) + + try: + spec = parse_experiment_spec(spec_content) + except ExperimentSpecValidationError as exc: + return f"VALIDATION ERROR — the experiment spec is invalid.\n\nDetails:\n{exc.details}" + + run_plan = spec.run_plan + allocation = run_plan.allocation + run_count = ( + f" compared conditions: {len(allocation.compared_conditions)}" + if allocation is not None + else f" target run count: {run_plan.target_run_count}" + ) + parts = [ + f"VALID — experiment spec '{spec.spec_id}' (v{spec.spec_version}) parsed successfully.", + f" task_ref: {spec.task_ref.ref_id}", + f" run-count source: {'allocation' if allocation is not None else 'target_run_count'}", + f" stochastic controls: {len(run_plan.stochastic_controls)}", + f" red-variant selections: {len(run_plan.red_variant_selections)}", + f" factors: {len(spec.factors)}", + f" capture-spec refs: {len(spec.capture_spec_refs)}", + run_count, + ] + return "\n".join(parts) + + +def _run_experiment_scaffold(complexity: str, spec_id: str, task_ref_id: str) -> str: + """Render a starter experiment-spec skeleton for the requested complexity.""" + key = complexity.lower().strip() + if key not in ("minimal", "sweep"): + return "Invalid complexity. Choose: 'minimal' or 'sweep'." + template = _SCAFFOLD_MINIMAL if key == "minimal" else _SCAFFOLD_SWEEP + return template.replace("{spec_id}", spec_id).replace("{task_ref_id}", task_ref_id) + + +def _run_experiment_get_example(name: str) -> str: + """Return an allowlisted worked experiment-spec example.""" + filename = _ALLOWED_EXAMPLES.get(name.lower().strip()) + if filename is None: + return f"Unknown example '{name}'. Available: {', '.join(sorted(_ALLOWED_EXAMPLES))}" + path = _EXAMPLES_DIR / filename + if not path.exists(): + return f"Example file not found: {filename}" + return path.read_text(encoding="utf-8") + + +def register(mcp: FastMCP) -> None: + """Register experiment authoring tools on the MCP server.""" + + @mcp.tool( + name="experiment_validate", + description=( + "Parse and validate an authored experiment specification (YAML for the " + "experiment-authoring-input contract). Returns either a success " + "confirmation with a short summary, or the structured validation " + "errors found.\n\n" + "An experiment spec is the pre-run design: it references an " + "experiment task and declares the run plan (stochastic controls/seeds, " + "episode controls such as turn order / step count / termination, " + "red-variant selections, and either a condition allocation or a simple " + "target run count). Pass the full YAML as `spec_content`." + ), + ) + def experiment_validate(spec_content: str) -> str: + return _run_experiment_validate(spec_content) + + @mcp.tool( + name="experiment_scaffold", + description=( + "Generate a starter experiment-specification skeleton (valid YAML you " + "can edit). Choose a complexity level: 'minimal' (a single-condition " + "design using target_run_count) or 'sweep' (a two-condition red-variant " + "comparison using an allocation plan). Optionally provide a spec id and " + "the task id the design targets." + ), + ) + def experiment_scaffold( + complexity: str = "minimal", + spec_id: str = "my-experiment-spec", + task_ref_id: str = "task-example-v1", + ) -> str: + return _run_experiment_scaffold(complexity, spec_id, task_ref_id) + + @mcp.tool( + name="experiment_get_example", + description=( + "Get a complete, annotated experiment-specification example. Available " + "examples: 'sweep' (a two-condition red-tactic comparison with an " + "allocation plan) and 'smoke' (a minimal single-condition design using " + "target_run_count). Use 'sweep' to see the full authoring surface." + ), + ) + def experiment_get_example(name: str) -> str: + return _run_experiment_get_example(name) + + +# --------------------------------------------------------------------------- +# Scaffold templates +# --------------------------------------------------------------------------- + +_SCAFFOLD_MINIMAL = """\ +schema_version: experiment-authoring-input/v1 +spec_id: {spec_id} +spec_version: 1.0.0 +title: My experiment +description: A minimal single-condition experiment design. + +# Reference the separately authored experiment task (experiment-task-v1). +task_ref: + ref_kind: task + ref_id: {task_ref_id} + ref_version: 1.0.0 + +run_plan: + stochastic_controls: + - control_id: episode-seed + role: seed + value: 1 + description: Base RNG seed. + episode_control: + turn_order: sequential + max_steps: 100 + termination_rule: Terminate each episode after 100 logical steps. + # Exactly one of allocation / target_run_count. + target_run_count: 10 +""" + +_SCAFFOLD_SWEEP = """\ +schema_version: experiment-authoring-input/v1 +spec_id: {spec_id} +spec_version: 1.0.0 +title: My red-variant sweep +description: A two-condition comparison of red variants with seeded replication. + +task_ref: + ref_kind: task + ref_id: {task_ref_id} + ref_version: 1.0.0 + +run_plan: + stochastic_controls: + - control_id: episode-seed + role: seed + value: 1 + description: Base RNG seed; per-run seeds derived deterministically. + episode_control: + turn_order: sequential + max_steps: 100 + termination_rule: Terminate each episode after 100 logical steps. + allocation: + allocation_unit: run + allocation_method: balanced + compared_conditions: + - cond-a + - cond-b + condition_assignments: + cond-a: + condition_id: cond-a + factor_levels: + red-variant: variant-a + required_parameters: + - name: red_variant + value: variant-a + value_kind: protocol + cond-b: + condition_id: cond-b + factor_levels: + red-variant: variant-b + required_parameters: + - name: red_variant + value: variant-b + value_kind: protocol + target_runs_per_condition: 50 + replication_policy: 50 independent seeded runs per condition. + red_variant_selections: + variant-a: + variant_id: variant-a + agent_ref: red-agent + variant-b: + variant_id: variant-b + agent_ref: red-agent + +factors: + red-variant: + name: Red variant + factor_kind: treatment + levels: + - variant-a + - variant-b +""" diff --git a/implementations/python/packages/aces_mcp/tools/inspection.py b/implementations/python/packages/aces_mcp/tools/inspection.py index be1e103f6..2f9b6b0a4 100644 --- a/implementations/python/packages/aces_mcp/tools/inspection.py +++ b/implementations/python/packages/aces_mcp/tools/inspection.py @@ -440,16 +440,22 @@ def _build_reference_map(scenario) -> dict[tuple[str, str], list[str]]: if feat.dependencies: refs[("features", name)] = list(feat.dependencies) - # Events -> conditions, injects + # Events -> precondition assertions, injects for name, event in scenario.events.items(): targets = [] - if event.conditions: - targets.extend(event.conditions) + if event.assertions: + targets.extend(event.assertions) if event.injects: targets.extend(event.injects) if targets: refs[("events", name)] = targets + for name, proposition in scenario.propositions.items(): + refs[("propositions", name)] = [*proposition.subjects, *proposition.evidence_requirements] + + for name, assertion in scenario.assertions.items(): + refs[("assertions", name)] = [assertion.proposition] + # Scripts -> events for name, script in scenario.scripts.items(): if script.events: @@ -502,7 +508,7 @@ def _build_reference_map(scenario) -> dict[tuple[str, str], list[str]]: if obj.depends_on: targets.extend(obj.depends_on) if obj.success: - targets.extend(obj.success.conditions) + targets.extend(obj.success.assertions) if targets: refs[("objectives", name)] = targets diff --git a/implementations/python/packages/aces_mcp/tools/language_service.py b/implementations/python/packages/aces_mcp/tools/language_service.py index cd31c88f3..eeddca661 100644 --- a/implementations/python/packages/aces_mcp/tools/language_service.py +++ b/implementations/python/packages/aces_mcp/tools/language_service.py @@ -57,9 +57,9 @@ def sdl_references( @mcp.tool( name="sdl_format", description=( - "Format SDL YAML into the repository's normalized authoring shape. " - "Returns the formatted content and any diagnostics produced after " - "formatting." + "Migrate recognized legacy SDL field spellings and format the result " + "as canonical sdl-yaml/v1. Returns source-ranged migration advisories " + "and any diagnostics produced after formatting." ), ) def sdl_format(sdl_content: str) -> str: diff --git a/implementations/python/packages/aces_mcp/tools/operation_support.py b/implementations/python/packages/aces_mcp/tools/operation_support.py index 9355db196..0cb7ddebb 100644 --- a/implementations/python/packages/aces_mcp/tools/operation_support.py +++ b/implementations/python/packages/aces_mcp/tools/operation_support.py @@ -28,11 +28,15 @@ "outcome_interpretation_rules", "objectives", "workflows", - "variables", ] -def compile_pipeline(sdl_content: str, parameters_json: str) -> dict[str, Any]: +def compile_pipeline( + sdl_content: str, + parameters_json: str, + *, + accept_migration_syntax: bool = False, +) -> dict[str, Any]: size_error = size_error_payload(sdl_content, parameters_json) if size_error is not None: return {"error": json.loads(size_error), "stages": [], "scenario": None, "model": None} @@ -40,6 +44,7 @@ def compile_pipeline(sdl_content: str, parameters_json: str) -> dict[str, Any]: from aces_processor.compiler import compile_runtime_model from aces_sdl import ( SDLInstantiationError, + SDLMigrationPolicy, SDLParseError, SDLValidationError, instantiate_scenario, @@ -52,9 +57,12 @@ def compile_pipeline(sdl_content: str, parameters_json: str) -> dict[str, Any]: stages: list[dict[str, str]] = [] try: - scenario = parse_sdl(sdl_content) + scenario = parse_sdl( + sdl_content, + migration_policy=(SDLMigrationPolicy.ACCEPT if accept_migration_syntax else SDLMigrationPolicy.REJECT), + ) except SDLParseError as exc: - return {"error": stage_error("parse", exc.details), "stages": stages, "scenario": None, "model": None} + return {"error": stage_error("parse", exc), "stages": stages, "scenario": None, "model": None} except SDLValidationError as exc: return { "error": { @@ -90,7 +98,13 @@ def compile_pipeline(sdl_content: str, parameters_json: str) -> dict[str, Any]: "stages": stages, "scenario": concrete, "model": model, - "instantiation_parameters": concrete.instantiation_parameters, + "source_diagnostics": [item.as_dict() for item in scenario.source_diagnostics], + "instantiation": { + "binding_count": len(concrete.instantiation_provenance.bindings) + + sum(len(item.bindings) for item in concrete.instantiation_provenance.imports), + "import_count": len(concrete.instantiation_provenance.imports), + "profile": concrete.instantiation_provenance.selected_profile, + }, } @@ -139,7 +153,15 @@ def stage_ok(stage: str, *, detail: str = "ok") -> dict[str, str]: return {"stage": stage, "status": "ok", "detail": detail} -def stage_error(stage: str, message: str) -> dict[str, Any]: +def stage_error(stage: str, error: object) -> dict[str, Any]: + structured = getattr(error, "diagnostics", ()) + if structured: + return { + "status": "invalid", + "stage": stage, + "diagnostics": [item.as_dict() for item in structured], + } + message = getattr(error, "details", str(error)) return { "status": "invalid", "stage": stage, @@ -196,7 +218,7 @@ def runtime_model_summary(model: Any) -> dict[str, Any]: "entities": len(model.entity_specs), "agents": len(model.agent_specs), "relationships": len(model.relationship_specs), - "variables": len(model.variable_specs), + "capability_constraints": len(model.capability_constraints), }, "domains": { "provisioning": { @@ -278,13 +300,12 @@ def design_notes(scenario: Any, model: Any, execution_plan: Any) -> list[dict[st "No objectives are authored; range intent and success criteria may be hard to assess.", ) ) - if scenario.objectives and not any(objective.success.conditions for objective in scenario.objectives.values()): + if scenario.objectives and not any(objective.success.assertions for objective in scenario.objectives.values()): notes.append( note( "assessment", "warning", - "Objectives exist without any observable-state (conditions) success criteria; " - "objective success may be under-specified.", + "Objectives exist without any backend-neutral success assertions; objective truth is under-specified.", ) ) if scenario.agents and not scenario.action_contracts: diff --git a/implementations/python/packages/aces_mcp/tools/operations.py b/implementations/python/packages/aces_mcp/tools/operations.py index 0db621b74..343bc9b01 100644 --- a/implementations/python/packages/aces_mcp/tools/operations.py +++ b/implementations/python/packages/aces_mcp/tools/operations.py @@ -45,8 +45,9 @@ def aces_tool_surface() -> str: "ACES scenarios for researchers and range designers." ), "recommended_workflow": [ - "sdl_overview", "aces_agent_guidance", + "aces_intended_use_profiles", + "sdl_overview", "sdl_section_reference", "sdl_scaffold or user-authored SDL", "sdl_completions / sdl_diagnostics / sdl_apply_edit while authoring", @@ -71,6 +72,11 @@ def aces_tool_surface() -> str: "sdl_validate_section", "sdl_instantiate", ], + "experiment_authoring": [ + "experiment_scaffold", + "experiment_validate", + "experiment_get_example", + ], "language_service": [ "sdl_completions", "sdl_references", @@ -97,6 +103,7 @@ def aces_tool_surface() -> str: ], "guidance": [ "aces_agent_guidance", + "aces_intended_use_profiles", ], }, "boundaries": [ @@ -138,26 +145,29 @@ def aces_agent_guidance(audience: str = "all") -> str: "Parse SDL YAML and return a machine-readable JSON summary of the " "normalized scenario shape, populated sections, advisories, and " "optional semantic-validation status. This is useful before editing " - "or deeper validation." + "or deeper validation. Canonical syntax is required unless " + "`accept_migration_syntax=true` is explicitly selected." ), ) def sdl_parse( sdl_content: str, semantic_validation: bool = False, + accept_migration_syntax: bool = False, ) -> str: size_error = size_error_payload(sdl_content) if size_error is not None: return size_error - from aces_sdl import SDLParseError, SDLValidationError, parse_sdl + from aces_sdl import SDLMigrationPolicy, SDLParseError, SDLValidationError, parse_sdl try: scenario = parse_sdl( sdl_content, skip_semantic_validation=not semantic_validation, + migration_policy=(SDLMigrationPolicy.ACCEPT if accept_migration_syntax else SDLMigrationPolicy.REJECT), ) except SDLParseError as exc: - return json_response(stage_error("parse", exc.details)) + return json_response(stage_error("parse", exc)) except SDLValidationError as exc: return json_response( { @@ -181,6 +191,7 @@ def sdl_parse( "version": scenario.version, "populated_sections": section_counts(scenario), "advisories": list(scenario.advisories), + "source_diagnostics": [item.as_dict() for item in scenario.source_diagnostics], }, } ) @@ -190,14 +201,20 @@ def sdl_parse( description=( "Parse, semantically validate, instantiate, and compile SDL YAML " "into the ACES runtime model. Returns JSON with domain counts, " - "participant-contract counts, and structured compiler diagnostics." + "participant-contract counts, source migration advisories, and " + "structured compiler diagnostics." ), ) def sdl_compile( sdl_content: str, parameters_json: str = "{}", + accept_migration_syntax: bool = False, ) -> str: - pipeline = compile_pipeline(sdl_content, parameters_json) + pipeline = compile_pipeline( + sdl_content, + parameters_json, + accept_migration_syntax=accept_migration_syntax, + ) if pipeline["error"] is not None: return json_response(pipeline["error"]) @@ -208,10 +225,11 @@ def sdl_compile( "stages": pipeline["stages"], "scenario": { "name": model.scenario_name, - "instantiation_parameters": pipeline["instantiation_parameters"], + "instantiation": pipeline["instantiation"], }, "runtime_model": runtime_model_summary(model), "diagnostics": diagnostics(model.diagnostics, stage="compilation"), + "source_diagnostics": pipeline["source_diagnostics"], } ) @@ -244,7 +262,7 @@ def sdl_plan( "stages": [*pipeline["stages"], stage_ok("planning")], "scenario": { "name": execution_plan.scenario_name, - "instantiation_parameters": pipeline["instantiation_parameters"], + "instantiation": pipeline["instantiation"], }, "manifest": { "backend": manifest.identity.name, @@ -314,7 +332,7 @@ def sdl_design_assessment( "scenario": { "name": scenario.name, "populated_sections": section_counts(scenario), - "instantiation_parameters": pipeline["instantiation_parameters"], + "instantiation": pipeline["instantiation"], }, "runtime_model": runtime_model_summary(model), "plan": execution_plan_summary(execution_plan), diff --git a/implementations/python/packages/aces_mcp/tools/reference.py b/implementations/python/packages/aces_mcp/tools/reference.py index 50affff74..7cce46815 100644 --- a/implementations/python/packages/aces_mcp/tools/reference.py +++ b/implementations/python/packages/aces_mcp/tools/reference.py @@ -71,6 +71,8 @@ def _read_example(name: str) -> str: "infrastructure": "Infrastructure", "features": "Features", "conditions": "Conditions", + "propositions": "Propositions", + "assertions": "Assertions", "vulnerabilities": "Vulnerabilities", "entities": "Entities", "injects": "Orchestration: Injects, Events, Scripts, Stories", @@ -81,7 +83,13 @@ def _read_example(name: str) -> str: "content": "Content", "accounts": "Accounts", "relationships": "Relationships", + "forwarding_agents": "Forwarding Agents", "agents": "Agents", + "action_contracts": "Action Contracts", + "observation_boundaries": "Observation Boundaries", + "outcome_interpretation_rules": "Outcome Interpretation Rules", + "behavior_specifications": "Behavior Specifications", + "evidence_requirements": "Evidence Requirements", "objectives": "Objectives", "workflows": "Workflows", "variables": "Variables", @@ -113,7 +121,7 @@ def register(mcp: FastMCP) -> None: name="sdl_overview", description=( "Get a comprehensive overview of the ACES Scenario Description " - "Language (SDL). Returns what the SDL is, its 17 sections, how " + "Language (SDL). Returns what the SDL is, its authoring sections, how " "parsing/validation works, the variable system, and a complete " "minimal example. Start here if you have never seen the SDL before." ), @@ -129,7 +137,10 @@ def sdl_overview() -> str: "rules. Valid section names: nodes, infrastructure, features, " "conditions, vulnerabilities, entities, orchestration " "(injects+events+scripts+stories), content, accounts, " - "relationships, agents, objectives, workflows, variables. You can " + "relationships, forwarding_agents, agents, action_contracts, " + "observation_boundaries, outcome_interpretation_rules, " + "behavior_specifications, evidence_requirements, objectives, " + "workflows, variables. You can " "also pass the individual section name like 'conditions' or " "'events'." ), @@ -161,7 +172,7 @@ def sdl_section_reference(section: str) -> str: description=( "Get a complete, real-world annotated SDL scenario example. " "Available examples: 'hospital' (hospital ransomware exercise, " - "~750 lines, uses all 17 sections), 'satcom' (satellite supply-chain " + "~750 lines, broad language coverage), 'satcom' (satellite supply-chain " "exercise, ~750 lines), 'port' (port authority OT exercise, ~680 lines), " "'minimal' (a small annotated pentest-lab example to learn the basics). " "Use 'hospital' for a comprehensive reference of all SDL features." @@ -196,7 +207,7 @@ def sdl_parser_reference() -> str: @mcp.tool( name="sdl_validation_reference", description=( - "Get documentation about SDL semantic validation: all 22 named " + "Get documentation about SDL semantic validation: the aggregated " "validation passes, cross-reference resolution rules, error " "reporting, advisories, and how ambiguous references are handled." ), @@ -284,9 +295,26 @@ def sdl_validation_reference() -> str: # --- Health checks --- conditions: web-alive: + proposition: web-available command: "curl -sf http://localhost:8080/ || exit 1" interval: 15 +propositions: + web-available: + description: The web service reports its availability as healthy. + subjects: [nodes.web.services.http] + basis: declared_state + predicate: + kind: presence + property: service + semantic_ref: urn:aces:declared-property:service + operator: exists + +assertions: + web-alive: + proposition: web-available + role: postcondition + # --- Teams / people --- entities: blue-team: @@ -296,14 +324,14 @@ def sdl_validation_reference() -> str: name: Red Team role: Red -# Objective success references observable state (conditions) per ADR-073. +# Objective success references assertions over typed propositions per ADR-079. # Graded scoring/reward lives in the experiment/evaluator plane, not the SDL. objectives: keep-web-alive: description: Keep the web application available entity: blue-team success: - conditions: [web-alive] + assertions: [web-alive] # --- Parameterization (${var} syntax, resolved at instantiation, not parse time) --- variables: @@ -323,15 +351,17 @@ def sdl_validation_reference() -> str: means* — not how to deploy it. Backend implementations realize SDL \ specifications through runtime contracts. -It descends from the Open Cyber Range (OCR) SDL and extends it with 7 \ -additional sections for richer experiment semantics. +Its topology and exercise-narrative core descends from the Open Cyber Range +(OCR) SDL. ACES adds composition, participant, evidence, objective, workflow, +and runtime-inventory semantics; the exact live surface is governed by the +normative section catalog rather than a historical count. -## The 17 Sections +## Authoring Sections -A scenario is a YAML document with a required `name` and up to 17 optional \ -sections, organized into four concerns: +A scenario is a YAML document with a required `name`, optional metadata and +composition fields, and optional authoring sections organized by concern. -### Topology & Software (5 sections) +### Topology and Software | Section | Purpose | |---------|---------| | `nodes` | VMs and network switches — the compute/network topology | @@ -345,7 +375,7 @@ def sdl_validation_reference() -> str: success references observable state (`conditions`); graded scoring, reward, and \ evaluation outputs live in the experiment/evaluator plane (ADR-055/064/069). -### Exercise Orchestration (4 sections) +### Exercise Orchestration | Section | Purpose | |---------|---------| | `entities` | Teams, organizations, people (recursive hierarchy) | @@ -354,13 +384,19 @@ def sdl_validation_reference() -> str: | `scripts` | Timed event sequences with human-readable durations | | `stories` | Top-level orchestration grouping scripts | -### Extended Experiment Semantics (7 sections) +### Extended Scenario and Experiment Semantics | Section | Purpose | |---------|---------| | `content` | Data placed into systems (files, datasets, emails) | | `accounts` | User accounts on nodes | | `relationships` | Typed directed edges (auth, trust, federation, etc.) | +| `forwarding_agents` | Scenario-level logical forwarding and shipping agents | | `agents` | Autonomous participants with actions/knowledge/scope | +| `action_contracts` | Participant action applicability, effects, failures, and interactions | +| `observation_boundaries` | Participant information projections and visibility changes | +| `outcome_interpretation_rules` | Interpretation of action observations and local outcomes | +| `behavior_specifications` | Versioned bindings across participant behavior surfaces | +| `evidence_requirements` | Authored capture obligations, distinct from captured evidence | | `objectives` | Declarative tasks: actor + targets + success + window | | `workflows` | Branching/parallel control graphs over objectives | | `variables` | Parameterization via `${var_name}` syntax | @@ -383,11 +419,11 @@ def sdl_validation_reference() -> str: - `source: "pkg"` → `{name: "pkg", version: "*"}` - `infrastructure: {node: 3}` → `{node: {count: 3}}` - `roles: {admin: "user"}` → `{admin: {username: "user"}}` -- `min-score: 50` → `{percentage: 50}` - `features: [nginx, php]` → `{nginx: "", php: ""}` ### Key Normalization -- Field keys are case-insensitive: `Name` → `name`, `Min-Score` → `min_score` +- Recognized legacy field spellings are migrated to canonical lowercase + `snake_case` with source diagnostics; canonical authoring uses those names - User-defined names (node names, feature names, etc.) are preserved as-is ### Cross-Reference System @@ -398,11 +434,11 @@ def sdl_validation_reference() -> str: - Named ACLs: `infrastructure..acls.` ### Validation Pipeline -1. YAML parsing (`yaml.safe_load()`) -2. Key normalization +1. Bounded YAML parsing with duplicate-key checks +2. Explicit migration to canonical field spelling 3. Shorthand expansion 4. Pydantic structural validation -5. 22-pass semantic validation (cross-references, cycles, IP/CIDR, etc.) +5. Aggregated semantic validation (cross-references, cycles, IP/CIDR, etc.) All errors collected before reporting — authors see every issue at once. ### Duration Grammar diff --git a/implementations/python/packages/aces_operations/_evidence_run_artifact.py b/implementations/python/packages/aces_operations/_evidence_run_artifact.py index c93e18985..68045d005 100644 --- a/implementations/python/packages/aces_operations/_evidence_run_artifact.py +++ b/implementations/python/packages/aces_operations/_evidence_run_artifact.py @@ -20,7 +20,7 @@ from pathlib import Path from typing import Any -from aces_backend_libvirt.techvault_native import NativeLibvirtProbe, expected_surface, native_soc_readback +from aces_backend_libvirt.techvault_native import expected_surface from aces_backend_protocols.capabilities import ( observation_capability_contract_gaps, participant_runtime_capability_contract_gaps, @@ -40,6 +40,7 @@ RealizedNetwork, TerminalSnapshot, ) +from aces_operations.run_artifacts import portable_artifact_ref EVIDENCE_RUN_SCHEMA = "aces.libvirt.scenario-evidence-run/v1" _LIBVIRT_BACKEND_NAME = "libvirt-qemu" @@ -68,7 +69,7 @@ def assemble_artifact(inputs: EvidenceArtifactInputs) -> dict[str, Any]: manifest = inputs.manifest proof = inputs.proof native_snapshot = inputs.native_snapshot - probe = inputs.probe + native_cleanup_verified = inputs.native_cleanup_verified unrealized_capabilities = inputs.unrealized_capabilities substrate_realized = native_snapshot is not None @@ -82,12 +83,15 @@ def assemble_artifact(inputs: EvidenceArtifactInputs) -> dict[str, Any]: "evidence_source_mode": mode, "scenario": scenario_section, "compiled_artifact": _compiled_artifact_section(model), - "backend": _backend_section(manifest, mode, substrate_realized), + "backend": _backend_section(manifest, mode, substrate_realized, native_cleanup_verified), + "realization_facts": _realization_facts_section( + model, native_snapshot, native_cleanup_verified, inputs.guest_observed + ), "realized_topology": _topology_section(model, native_snapshot, unrealized_capabilities), "participant_action_proof": _participant_proof_section(proof), "terminal_observation": _terminal_observation_section(proof["snapshot"]), "defensive_evidence": _defensive_evidence_section(native_snapshot, model, recorded_at), - "negative_boundary_checks": _negative_boundary_section(boundary_refs, native_snapshot, probe), + "negative_boundary_checks": _negative_boundary_section(boundary_refs), "evaluator_outcome": _evaluator_outcome_section(proof["lifecycle_clean"], recorded_at), "realized_form_disclosures": _realized_form_disclosures(manifest, substrate_realized), "limitations": _limitations(mode, unrealized_capabilities), @@ -111,7 +115,12 @@ def _manifest_version(manifest: BackendManifest) -> str: return str(getattr(manifest, "version", "0.0.0+unknown")) -def _backend_section(manifest: BackendManifest, mode: str, substrate_realized: bool) -> dict[str, Any]: +def _backend_section( + manifest: BackendManifest, + mode: str, + substrate_realized: bool, + cleanup_verified: bool | None, +) -> dict[str, Any]: """Embed the canonical BackendManifestV2 payload + capability-gap report. The manifest is rendered through ``backend_manifest_payload`` — the same @@ -132,7 +141,8 @@ def _backend_section(manifest: BackendManifest, mode: str, substrate_realized: b "backend": _manifest_name(manifest), "evidence_source_mode": mode, "substrate_realized": substrate_realized, - "basis": "native-realized" if substrate_realized else "planned-not-realized", + "basis": "daemon-observed-substrate" if substrate_realized else "planned-not-realized", + "cleanup_verified": cleanup_verified, }, } @@ -149,20 +159,11 @@ def _scenario_section(scenario_path: Path, model: CompiledModel) -> dict[str, An return { "name": model.scenario_name, "version": version, - "relative_path": _portable_scenario_ref(scenario_path), + "relative_path": portable_artifact_ref(scenario_path), "content_sha256": "sha256:" + hashlib.sha256(content).hexdigest(), } -def _portable_scenario_ref(scenario_path: Path) -> str: - """Return a repo-portable scenario reference, never the absolute host path.""" - parts = scenario_path.parts - for anchor in ("examples", "scenarios"): - if anchor in parts: - return "/".join(parts[parts.index(anchor) :]) - return scenario_path.name - - def _compiled_artifact_section(model: CompiledModel) -> dict[str, Any]: addresses = { "participant_behaviors": sorted(model.participant_behaviors), @@ -207,6 +208,62 @@ def _network_properties(network: RealizedNetwork) -> dict[str, Any]: return {"cidr": props.get("cidr"), "gateway": props.get("gateway"), "internal": props.get("internal")} +def _realization_facts_section( + model: CompiledModel, + native_snapshot: Mapping[str, Any] | None, + cleanup_verified: bool | None, + guest_observed: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + observed = native_snapshot if isinstance(native_snapshot, Mapping) else {} + daemon_domains = observed.get("domains", ()) + daemon_networks = observed.get("networks", ()) + realized_addresses = observed.get("realized_addresses", ()) + return { + "authored": { + "source": "authored", + "scenario_name": model.scenario_name, + }, + "planned": { + "source": "planned", + "node_addresses": sorted(model.node_deployments), + "network_addresses": sorted(model.networks), + }, + "driver_reported": { + "source": "driver-reported", + "realized_addresses": list(realized_addresses) if isinstance(realized_addresses, list | tuple) else [], + }, + "daemon_observed": { + "source": "daemon-observed", + "domains": list(daemon_domains) if isinstance(daemon_domains, list | tuple) else [], + "networks": list(daemon_networks) if isinstance(daemon_networks, list | tuple) else [], + }, + "guest_observed": _guest_observed_section(guest_observed), + "cleanup": { + "source": "driver-reported", + "status": _cleanup_status(cleanup_verified), + }, + "binding": observed.get("binding"), + } + + +def _guest_observed_section(guest_observed: Mapping[str, Any] | None) -> dict[str, Any]: + """Return the guest-observed fact section: the bound report, or a not-observed stub. + + A daemon-only run discloses ``not-observed`` honestly; a guest-certified run + embeds the operation-joined, challenge-bound, per-domain guest report. + """ + + if not isinstance(guest_observed, Mapping): + return {"source": "guest-observed", "status": "not-observed"} + return {**guest_observed, "source": "guest-observed"} + + +def _cleanup_status(cleanup_verified: bool | None) -> str: + if cleanup_verified is None: + return "not-required" + return "verified" if cleanup_verified else "failed" + + def _topology_section( model: CompiledModel, native_snapshot: Mapping[str, Any] | None, @@ -215,6 +272,7 @@ def _topology_section( substrate_realized = native_snapshot is not None nodes = [ { + "source": "planned", "address": node.address, "name": node.name, "node_type": getattr(node, "node_type", None), @@ -225,12 +283,14 @@ def _topology_section( for node in model.node_deployments.values() ] networks = [ - {"address": net.address, "name": net.name, **_network_properties(net)} for net in model.networks.values() + {"source": "planned", "address": net.address, "name": net.name, **_network_properties(net)} + for net in model.networks.values() ] section: dict[str, Any] = { - "basis": "native-realized" if substrate_realized else "planned-not-realized", + "basis": "mixed-source" if substrate_realized else "planned", "disclosure": ( - "Topology realized through the native libvirt driver." + "Compiled topology remains planned; the native surface contains only independently daemon-observed " + "substrate fields." if substrate_realized else "Compiled/planned topology from the authored scenario; no live substrate realized. Network CIDRs and " "gateways are authored values, not host-private libvirt addresses." @@ -327,21 +387,11 @@ def _defensive_evidence_section( # assembly, not a freshly synthesized one, so every section shares one # consistent run timestamp and the artifact stays reproducible. evidence_channels = _boundary_evidence_refs(model) - if native_snapshot is not None: - return { - "evidence_kind": "telemetry", - "evidence_source": "native-translated-readback", - "visibility": "evaluator-only", - "sensitivity": "restricted", - "redaction_state": "redacted", - "loss_disclosure": ( - "Native libvirt SOC readback is a translated native readback of generated appliance state, not full " - "upstream Wazuh internals; no detection-quality claim is made." - ), - "evaluator_evidence_channels": evidence_channels, - "soc_readback": native_soc_readback(native_snapshot), - "captured_at": recorded_at, - } + substrate_note = ( + " Daemon-observed libvirt substrate state is present, but it is not guest SOC observation." + if native_snapshot is not None + else "" + ) return { "evidence_kind": "telemetry", "evidence_source": "structural-evaluator-channel", @@ -351,25 +401,21 @@ def _defensive_evidence_section( "loss_disclosure": ( "Deterministic mode: no live SOC substrate is booted. Wazuh/SOC defensive evidence is reported as the " "evaluator-only evidence channels declared by the scenario observation boundary, not upstream Wazuh " - "detection output; no detection-quality claim is made." + f"detection output; no detection-quality claim is made.{substrate_note}" ), "evaluator_evidence_channels": evidence_channels, "payload_summary": ( "Evaluator-only Wazuh/SOC and policy-decision evidence channels are declared and kept off the participant " - "view; live SOC readback is available only under native-live mode." + "view; neither evidence mode claims guest SOC readback." ), "captured_at": recorded_at, } -def _negative_boundary_section( - boundary_refs: Sequence[str], - native_snapshot: Mapping[str, Any] | None, - probe: NativeLibvirtProbe | None, -) -> dict[str, Any]: +def _negative_boundary_section(boundary_refs: Sequence[str]) -> dict[str, Any]: internal_refs = [ref for ref in boundary_refs if any(kw in ref for kw in _INTERNAL_SURFACE_KEYWORDS)] checks = [{"ref": ref, "exposed_to_participant": False} for ref in internal_refs] - section: dict[str, Any] = { + return { "method": ( "Structural boundary analysis over the compiled observation boundary (hidden_refs) and the participant " "exposure policy (empty visible/disclosed refs). The participant action surface does not expose the " @@ -380,36 +426,6 @@ def _negative_boundary_section( "checks": checks, "disclosure": "Negative boundary checks are evaluator-side derived analysis, not participant observations.", } - if native_snapshot is not None and probe is not None: - section["native_reachability"] = _native_reachability_summary(native_snapshot, probe) - return section - - -def _native_reachability_summary(native_snapshot: Mapping[str, Any], probe: NativeLibvirtProbe) -> dict[str, Any]: - summary: dict[str, Any] = {"reachable_surface_domains": []} - raw_domains = native_snapshot.get("domains", ()) - if not isinstance(raw_domains, list | tuple): - return summary - for domain in raw_domains: - if not isinstance(domain, Mapping): - continue - name = str(domain.get("name", "")) - if any(kw in name for kw in _INTERNAL_SURFACE_KEYWORDS): - continue - ip = _first_ip(domain) - if ip and probe.ping(ip).ok: - summary["reachable_surface_domains"].append(name) - return summary - - -def _first_ip(domain: Mapping[str, Any]) -> str | None: - interfaces = domain.get("interfaces", ()) - if not isinstance(interfaces, list | tuple): - return None - for interface in interfaces: - if isinstance(interface, Mapping) and isinstance(interface.get("ip"), str) and interface.get("ip"): - return str(interface["ip"]) - return None def _evaluator_outcome_section(lifecycle_clean: bool, recorded_at: str) -> dict[str, Any]: @@ -459,9 +475,12 @@ def _realized_form_disclosures(manifest: BackendManifest, substrate_realized: bo "realized_by_ref": backend_ref, "realized_value_summary": ( f"{backend_name} backend ({backend_version}); substrate " - f"{'realized natively' if substrate_realized else 'planned, not realized'}." + f"{'daemon-observed at bounded fields' if substrate_realized else 'planned, not realized'}." + ), + "disclosure": ( + "The libvirt-qemu backend supplied the run; live claims are limited to independently " + "daemon-observed substrate fields." ), - "disclosure": "The libvirt-qemu backend realized this scenario-evidence run.", } ), ExperimentRealizedFormDisclosureModel.model_validate( @@ -487,13 +506,13 @@ def _limitations(mode: str, unrealized_capabilities: tuple[str, ...] = ()) -> li limitations = [ "The libvirt participant runtime uses the deterministic domain adapter; no live participant domain is " "executed (issue #614).", - "Wazuh/SOC evidence is evaluator-only and, in native-live mode, is a translated native readback of generated " - "appliance state rather than full upstream Wazuh internals.", + "Wazuh/SOC evidence is evaluator-only structural evidence; daemon substrate state is not promoted to guest " + "or application observation.", ] if mode != "native-live": limitations.append( - "Deterministic mode does not realize a live libvirt substrate; topology and SOC readback are " - "compiled/structural, explicitly disclosed as not-live." + "Deterministic mode does not realize a live libvirt substrate; topology and defensive evidence channels " + "are compiled/structural, explicitly disclosed as not-live observations." ) if unrealized_capabilities: limitations.append( diff --git a/implementations/python/packages/aces_operations/_evidence_run_native.py b/implementations/python/packages/aces_operations/_evidence_run_native.py new file mode 100644 index 000000000..01fa2d4e8 --- /dev/null +++ b/implementations/python/packages/aces_operations/_evidence_run_native.py @@ -0,0 +1,244 @@ +"""Native/guest realization subsystem for the libvirt scenario-evidence producer. + +Only the ``native-live`` / ``guest-certified`` evidence-source modes exercise this +module: it realizes the bounded VM/network substrate through the native libvirt +driver, captures the challenge-bound guest report (guest-certified only), and +verifies teardown/residue on every path. Split from ``libvirt_evidence_run`` to keep +each module under the ADR-015 source-size cap; the producer calls +``_default_native_driver_factory`` and ``_run_native_mode`` from here. +""" + +from __future__ import annotations + +import hashlib +from collections.abc import Callable, Iterable, Mapping +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from aces_backend_libvirt.techvault_native import TechVaultNativeLibvirtDriver +from aces_runtime.control_plane import RuntimeControlPlane + +from aces_operations._evidence_run_types import EvidenceCheck, ExecutionPlan, LibvirtEvidenceRunConfig +from aces_operations._techvault_cleanup import cleanup_native_snapshot + + +def _run_native_mode( + mode: str, + execution_plan: ExecutionPlan, + control_plane: RuntimeControlPlane, + native_driver: TechVaultNativeLibvirtDriver, + driver_factory: Callable[[], TechVaultNativeLibvirtDriver] | None, + checks: list[EvidenceCheck], +) -> tuple[Mapping[str, Any] | None, bool | None, tuple[str, ...], Mapping[str, Any] | None]: + """Realize the native substrate, capture any guest report, and clean up in a finally-path. + + Native-proof boundary: only the default production driver/transport (no injected + factory) yields a certifying guest artifact; injected fakes are marked + non-certifying so their evidence can never be published as a real certification. + """ + native_snapshot: Mapping[str, Any] | None = None + guest_observed: Mapping[str, Any] | None = None + unrealized: tuple[str, ...] = () + try: + native_snapshot, realize_check, unrealized, operation_id = _realize_native_substrate( + execution_plan, control_plane, native_driver + ) + checks.append(realize_check) + if native_snapshot is not None and mode == "guest-certified": + guest_observed = _guest_observed_report(native_driver, operation_id, certifying=driver_factory is None) + finally: + native_cleanup_verified = _append_cleanup_check(native_driver, native_snapshot, checks) + return native_snapshot, native_cleanup_verified, unrealized, guest_observed + + +def _append_cleanup_check( + native_driver: TechVaultNativeLibvirtDriver, native_snapshot: Mapping[str, Any] | None, checks: list[EvidenceCheck] +) -> bool | None: + """Cleanup runs after every attempt; residue on a failed/unrealized run is reported.""" + if native_snapshot is not None: + verified, diagnostics = _verify_native_cleanup(native_driver, native_snapshot) + checks.append(EvidenceCheck("native_substrate_cleanup", verified, diagnostics)) + return verified + residue_ok, residue_diagnostics = _sweep_residue(native_driver) + if not residue_ok: + checks.append(EvidenceCheck("native_substrate_residue", False, residue_diagnostics)) + return None + + +def _default_native_driver_factory( + project_dir: Path, run_id: str, settings: LibvirtEvidenceRunConfig, mode: str +) -> Callable[[], TechVaultNativeLibvirtDriver]: + """Build the default native libvirt driver factory for an operator-run native mode. + + The driver connects to a real libvirt daemon at realize time; ``guest-certified`` + selects the guest-observing driver. In CI/tests a fake driver_factory is injected + instead, so this is never exercised without a daemon. + """ + state_dir = project_dir / "runs" / run_id / "scenario-evidence" / "libvirt" + + def factory() -> TechVaultNativeLibvirtDriver: + if mode == "guest-certified": + from aces_backend_libvirt.guest_certified_driver import GuestCertifiedLibvirtDriver + + return GuestCertifiedLibvirtDriver( + state_dir=state_dir, + connection_uri=settings.connection_uri, + name_prefix="aces-evidence", + ) + return TechVaultNativeLibvirtDriver( + state_dir=state_dir, + connection_uri=settings.connection_uri, + name_prefix="aces-evidence", + ) + + return factory + + +def _guest_observed_report( + native_driver: TechVaultNativeLibvirtDriver, operation_id: str | None, *, certifying: bool +) -> Mapping[str, Any] | None: + """Assemble the operation-joined, challenge-bound guest report from the driver. + + The control-plane operation id and observation timestamp are joined here, at the + operations boundary, rather than inside the backend driver. ``certifying`` records + whether the governed production driver was used; an injected fake driver yields a + non-certifying report that is externally distinguishable from a real proof. + """ + observations = getattr(native_driver, "last_guest_observations", ()) + if not observations: + return None + facts = getattr(native_driver, "last_guest_facts", {}) + binding = getattr(native_driver, "last_guest_binding", {}) + correlations = binding.get("correlations", {}) if isinstance(binding, Mapping) else {} + domains = [ + { + "address": address, + "correlation": correlations.get(address), + "architecture": fact.get("architecture"), + "vcpus": fact.get("vcpus"), + "memory_mib": fact.get("memory_mib"), + "network": list(fact.get("interfaces", ())), + "content": list(fact.get("content", ())), + "accounts": list(fact.get("accounts", ())), + "services": list(fact.get("services", ())), + } + for address, fact in sorted(facts.items()) + if isinstance(fact, Mapping) + ] + return { + # The raw control-plane operation id is a UUID and never portable identity; + # bind a redacted digest instead so the guest report joins the operation + # without leaking the UUID (the redaction gate forbids raw UUIDs). + "operation_ref": _operation_ref(operation_id), + "observed_at": datetime.now(UTC).isoformat(), + "certifying": certifying, + "probe_policy": binding.get("probe_policy") if isinstance(binding, Mapping) else None, + "challenge": binding.get("challenge") if isinstance(binding, Mapping) else None, + "domains": domains, + } + + +def _operation_ref(operation_id: str | None) -> str | None: + if not operation_id: + return None + return "sha256:" + hashlib.sha256(operation_id.encode("utf-8")).hexdigest() + + +def _verify_native_cleanup( + native_driver: TechVaultNativeLibvirtDriver, native_snapshot: Mapping[str, Any] +) -> tuple[bool, tuple[str, ...]]: + """Tear down a realized substrate and verify native + guest-probe cleanup.""" + + verified, diagnostics = cleanup_native_snapshot(native_driver, native_snapshot) + if verified and getattr(native_driver, "last_guest_binding", {}): + return False, (*diagnostics, "guest probe artifacts were not fully cleaned") + return verified, diagnostics + + +def _sweep_residue(native_driver: TechVaultNativeLibvirtDriver) -> tuple[bool, tuple[str, ...]]: + """Best-effort finally-path sweep after a failed/unrealized attempt. + + The driver rolls back on failure, so the common case leaves no residue. Any + remaining realized address, non-empty snapshot, or residual guest binding is a + leak and is reported so the run cannot pass. + """ + clean = ( + not native_driver.realized_addresses() + and native_driver.last_snapshot == {} + and not getattr(native_driver, "last_guest_binding", {}) + ) + if clean: + return True, () + residual = tuple(sorted(native_driver.realized_addresses())) + result = native_driver.destroy(networks=residual, domains=residual) + ok = ( + not result.diagnostics + and not native_driver.realized_addresses() + and native_driver.last_snapshot == {} + and not getattr(native_driver, "last_guest_binding", {}) + ) + if ok: + return True, () + diagnostics = tuple(f"{item.code} at {item.address}" for item in result.diagnostics) + return False, diagnostics or ("residual native or guest state remains after a failed attempt",) + + +def _realize_native_substrate( + execution_plan: ExecutionPlan, + control_plane: RuntimeControlPlane, + native_driver: TechVaultNativeLibvirtDriver | None, +) -> tuple[Mapping[str, Any] | None, EvidenceCheck, tuple[str, ...], str | None]: + """Realize the libvirt provisioning substrate (VMs + networks) for the scenario. + + Native modes pass only when the runtime operation succeeds and the fresh driver + report contains independently daemon-observed domains bound to the selected + realization-envelope/configuration identity. A domain handle or planned matrix + alone is never sufficient. Returns the control-plane operation id so the evidence + producer can join it at this boundary. + """ + if native_driver is None: + return None, EvidenceCheck("native_substrate_realization", False, ("no native driver",)), (), None + try: + receipt = control_plane.submit_provisioning(execution_plan.provisioning) + operation_id = str(receipt.operation_id) + status = control_plane.get_operation(receipt.operation_id) + except Exception: + return None, EvidenceCheck("native_substrate_realization", False, ("native realization failed",)), (), None + unrealized = _dedupe( + f"{d.code}: {d.message}" + for source in (execution_plan.diagnostics, () if status is None else status.diagnostics) + for d in source + if d.is_error + ) + snapshot = native_driver.last_snapshot + operation_succeeded = status is not None and status.state.value == "succeeded" + realized = operation_succeeded and _snapshot_has_daemon_observations(snapshot) + check = EvidenceCheck( + "native_substrate_realization", + realized, + () + if realized + else ("libvirt backend realized no native substrate for this scenario; capabilities disclosed as unrealized",), + ) + return (snapshot if realized else None), check, unrealized, operation_id + + +def _dedupe(items: Iterable[str]) -> tuple[str, ...]: + seen: dict[str, None] = {} + for item in items: + seen.setdefault(item, None) + return tuple(seen) + + +def _snapshot_has_daemon_observations(snapshot: Mapping[str, Any] | None) -> bool: + if not isinstance(snapshot, Mapping): + return False + domains = snapshot.get("domains", ()) + binding = snapshot.get("binding") + return ( + snapshot.get("source") == "daemon-observed" + and isinstance(domains, list | tuple) + and len(domains) > 0 + and isinstance(binding, Mapping) + ) diff --git a/implementations/python/packages/aces_operations/_evidence_run_realization.py b/implementations/python/packages/aces_operations/_evidence_run_realization.py new file mode 100644 index 000000000..2958d60a8 --- /dev/null +++ b/implementations/python/packages/aces_operations/_evidence_run_realization.py @@ -0,0 +1,395 @@ +"""Realization-source validation for the libvirt scenario-evidence artifact. + +Verifies that the artifact's realization facts, topology, daemon/guest +observations, and realization binding declare only admitted observation bases and +stay mutually consistent. Split from ``_evidence_run_validation`` to keep each +module under the ADR-015 source-size cap; the top-level validator imports +``_validate_realization_sources`` from here. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from collections.abc import Mapping +from typing import Any + +_SHA256_RE = re.compile(r"sha256:[a-f0-9]{64}") +_DAEMON_REQUIRED_FIELDS = { + "domains": { + "address", + "name", + "architecture", + "image_policy", + "memory_mib", + "vcpus", + "network_attachments", + "observation_source", + }, + "networks": { + "address", + "name", + "cidr", + "gateway", + "internal", + "forward_mode", + "observation_source", + }, +} + + +def _validate_realization_sources(payload: Mapping[str, Any]) -> list[str]: + problems: list[str] = [] + if "native-realized" in json.dumps(payload, sort_keys=True, default=str): + problems.append("realization source violation: native-realized is not an admitted observation basis") + + facts = payload.get("realization_facts", {}) + if not isinstance(facts, Mapping): + return [*problems, "realization_facts must be a mapping"] + problems.extend(_validate_fact_sources(facts)) + topology = payload.get("realized_topology", {}) + problems.extend(_validate_topology_sources(topology)) + backend = payload.get("backend", {}) + provenance = backend.get("realization_provenance", {}) if isinstance(backend, Mapping) else {} + substrate_realized = isinstance(provenance, Mapping) and provenance.get("substrate_realized") is True + cleanup = facts.get("cleanup") + problems.extend(_validate_cleanup_source(cleanup)) + if substrate_realized: + problems.extend(_validate_realized_substrate(backend, facts, topology, provenance, cleanup)) + elif isinstance(provenance, Mapping) and provenance.get("basis") != "planned-not-realized": + problems.append("unrealized substrate basis must be planned-not-realized") + else: + problems.extend(_validate_unrealized_substrate(facts, provenance, cleanup)) + problems.extend(_validate_guest_observation_boundary(payload)) + problems.extend(_validate_guest_observations(facts)) + return problems + + +def _validate_guest_observations(facts: Mapping[str, Any]) -> list[str]: + """Validate the guest-observed fact section when a guest report is present. + + A daemon-only run carries ``{"source": "guest-observed", "status": "not-observed"}`` + and is skipped here. A guest-certified run must bind every observed domain to the + control-plane operation, a fresh challenge, a canonical native correlation, and a + daemon-observed domain (rejecting unjoined or cross-operation evidence). + """ + + guest = facts.get("guest_observed") + if not isinstance(guest, Mapping) or guest.get("status") == "not-observed": + return [] + problems = _validate_guest_metadata(guest) + domains = guest.get("domains") + if not isinstance(domains, list | tuple) or not domains: + return [*problems, "guest observation requires at least one observed domain"] + daemon_addresses = _daemon_domain_addresses(facts) + for item in domains: + problems.extend(_validate_guest_domain(item, daemon_addresses)) + return problems + + +_GUEST_METADATA_FIELDS = ( + ("observation timestamp", "observed_at"), + ("probe policy", "probe_policy"), + ("fresh challenge", "challenge"), +) + + +def _validate_guest_metadata(guest: Mapping[str, Any]) -> list[str]: + problems: list[str] = [] + if not _is_canonical_sha256(guest.get("operation_ref")): + problems.append("guest observation requires a canonical operation reference") + if not isinstance(guest.get("certifying"), bool): + problems.append("guest observation requires an explicit certifying flag") + problems.extend( + f"guest observation requires a {label}" + for label, field_name in _GUEST_METADATA_FIELDS + if not _nonempty_string(guest.get(field_name)) + ) + return problems + + +def _daemon_domain_addresses(facts: Mapping[str, Any]) -> set[object]: + daemon = facts.get("daemon_observed", {}) + domains = daemon.get("domains", ()) if isinstance(daemon, Mapping) else () + return {item.get("address") for item in domains if isinstance(item, Mapping)} + + +_GUEST_DOMAIN_FIELDS = ("architecture", "vcpus", "memory_mib", "network", "content", "accounts", "services") + + +def _validate_guest_domain(item: object, daemon_addresses: set[object]) -> list[str]: + if not isinstance(item, Mapping): + return ["guest observation domain must be a mapping"] + problems: list[str] = [] + if not _is_canonical_sha256(item.get("correlation")): + problems.append("guest observation requires a canonical native correlation") + if item.get("address") not in daemon_addresses: + problems.append("guest observation is not joined to a daemon-observed domain") + problems.extend( + f"guest observation domain missing {field_name}" + for field_name in _GUEST_DOMAIN_FIELDS + if field_name not in item + ) + return problems + + +def _validate_fact_sources(facts: Mapping[str, Any]) -> list[str]: + expected_sources = { + "authored": "authored", + "planned": "planned", + "driver_reported": "driver-reported", + "daemon_observed": "daemon-observed", + "guest_observed": "guest-observed", + } + return [ + f"realization source violation: {key}.source must be {source!r}" + for key, source in expected_sources.items() + if not isinstance(facts.get(key), Mapping) or facts[key].get("source") != source + ] + + +def _validate_topology_sources(topology: object) -> list[str]: + if not isinstance(topology, Mapping): + return [] + problems: list[str] = [] + if topology.get("basis") not in {"planned", "mixed-source"}: + problems.append("realized_topology.basis must be planned or mixed-source") + for collection in ("nodes", "networks"): + for item in topology.get(collection, ()) or (): + if isinstance(item, Mapping) and item.get("source") != "planned": + problems.append(f"realization source violation: realized_topology.{collection} is planned") + native_surface = topology.get("native_surface") + if isinstance(native_surface, Mapping) and native_surface.get("source") != "daemon-observed": + problems.append("realization source violation: native_surface must be daemon-observed") + return problems + + +def _validate_cleanup_source(cleanup: object) -> list[str]: + if isinstance(cleanup, Mapping) and cleanup.get("source") == "driver-reported": + return [] + return ["realization cleanup must be driver-reported"] + + +def _validate_realized_substrate( + backend: object, + facts: Mapping[str, Any], + topology: object, + provenance: Mapping[str, Any], + cleanup: object, +) -> list[str]: + daemon = facts.get("daemon_observed", {}) + daemon_items = _daemon_items(daemon) + problems = _validate_realized_provenance(provenance, cleanup) + problems.extend(_validate_daemon_observations(daemon)) + problems.extend(_validate_reported_addresses(facts, daemon_items)) + problems.extend(_validate_native_surface(topology, daemon)) + if isinstance(backend, Mapping): + problems.extend(_validate_realization_binding(backend, facts)) + else: + problems.append("daemon-observed substrate requires a realization binding") + return problems + + +def _validate_realized_provenance(provenance: Mapping[str, Any], cleanup: object) -> list[str]: + problems: list[str] = [] + if provenance.get("basis") != "daemon-observed-substrate": + problems.append("realization provenance basis must be daemon-observed-substrate") + cleanup_verified = provenance.get("cleanup_verified") + expected_cleanup_status = "verified" if cleanup_verified is True else "failed" + cleanup_consistent = ( + isinstance(cleanup_verified, bool) + and isinstance(cleanup, Mapping) + and cleanup.get("status") == expected_cleanup_status + ) + if not cleanup_consistent: + problems.append("daemon-observed substrate requires a consistent cleanup outcome") + return problems + + +def _validate_daemon_observations(daemon: object) -> list[str]: + problems: list[str] = [] + domains = daemon.get("domains", ()) if isinstance(daemon, Mapping) else () + if not isinstance(domains, list | tuple) or not domains: + problems.append("daemon-observed substrate requires at least one observed domain") + for collection in ("domains", "networks"): + values = daemon.get(collection, ()) if isinstance(daemon, Mapping) else () + for item in values: + if isinstance(item, Mapping): + if item.get("observation_source") != "daemon-observed": + problems.append(f"realization source violation: daemon_observed.{collection} item source") + problems.extend(_validate_daemon_observation_item(collection, item)) + return problems + + +def _daemon_items(daemon: object) -> list[Mapping[str, Any]]: + if not isinstance(daemon, Mapping): + return [] + return [ + item + for collection in ("domains", "networks") + for item in daemon.get(collection, ()) + if isinstance(item, Mapping) + ] + + +def _validate_reported_addresses( + facts: Mapping[str, Any], + daemon_items: list[Mapping[str, Any]], +) -> list[str]: + observed_addresses = {item.get("address") for item in daemon_items} + driver_reported = facts.get("driver_reported", {}) + reported_addresses = driver_reported.get("realized_addresses", ()) if isinstance(driver_reported, Mapping) else () + valid = ( + isinstance(reported_addresses, list | tuple) + and all(isinstance(item, str) for item in reported_addresses) + and set(reported_addresses) == observed_addresses + ) + return [] if valid else ["driver-reported addresses do not match daemon observations"] + + +def _validate_native_surface(topology: object, daemon: object) -> list[str]: + native_surface = topology.get("native_surface") if isinstance(topology, Mapping) else None + if not isinstance(native_surface, Mapping): + return ["daemon-observed substrate requires a native surface"] + problems: list[str] = [] + for collection in ("domains", "networks"): + observed_names = sorted( + str(item.get("name")) + for item in (daemon.get(collection, ()) if isinstance(daemon, Mapping) else ()) + if isinstance(item, Mapping) + ) + surface_names = native_surface.get(collection, ()) + if not isinstance(surface_names, list | tuple) or sorted(str(item) for item in surface_names) != observed_names: + problems.append(f"native surface {collection} do not match daemon observations") + return problems + + +def _validate_unrealized_substrate( + facts: Mapping[str, Any], + provenance: Mapping[str, Any], + cleanup: object, +) -> list[str]: + problems = _unrealized_daemon_leak(facts) + guest = facts.get("guest_observed") + if isinstance(guest, Mapping) and guest.get("status") != "not-observed": + problems.append("unrealized substrate cannot publish guest observations") + if provenance.get("cleanup_verified") is not None: + problems.append("unrealized substrate cleanup must be not-applicable") + if isinstance(cleanup, Mapping) and cleanup.get("status") != "not-required": + problems.append("unrealized substrate cleanup status must be not-required") + return problems + + +def _unrealized_daemon_leak(facts: Mapping[str, Any]) -> list[str]: + daemon = facts.get("daemon_observed", {}) + daemon_domains = daemon.get("domains", ()) if isinstance(daemon, Mapping) else () + daemon_networks = daemon.get("networks", ()) if isinstance(daemon, Mapping) else () + if daemon_domains or daemon_networks or facts.get("binding") is not None: + return ["unrealized substrate cannot publish daemon observations or realization binding"] + return [] + + +def _validate_guest_observation_boundary(payload: Mapping[str, Any]) -> list[str]: + defensive = payload.get("defensive_evidence", {}) + if isinstance(defensive, Mapping) and "soc_readback" in defensive: + return ["guest observation violation: daemon substrate cannot supply SOC readback"] + return [] + + +def _validate_realization_binding(backend: Mapping[str, Any], facts: Mapping[str, Any]) -> list[str]: + binding = facts.get("binding") + manifest = backend.get("manifest", {}) + envelope = manifest.get("realization_envelope", {}) if isinstance(manifest, Mapping) else {} + if not isinstance(binding, Mapping) or not isinstance(envelope, Mapping): + return ["daemon-observed substrate requires a realization binding"] + problems = _validate_binding_identity(binding, envelope) + problems.extend(_validate_boot_artifact_binding(binding)) + expected_driver_digest = _driver_configuration_digest(binding) + if binding.get("driver_configuration_digest") != expected_driver_digest: + problems.append("realization binding driver configuration digest does not match its material") + return problems + + +def _validate_binding_identity(binding: Mapping[str, Any], envelope: Mapping[str, Any]) -> list[str]: + problems: list[str] = [] + if binding.get("realization_envelope_digest") != envelope.get("digest"): + problems.append("realization binding envelope digest does not match backend manifest") + if binding.get("configuration_digest") != envelope.get("configuration_digest"): + problems.append("realization binding configuration digest does not match backend manifest") + driver_digest = binding.get("driver_configuration_digest") + if not _is_canonical_sha256(driver_digest): + problems.append("realization binding requires a canonical driver configuration digest") + if binding.get("driver") not in {"techvault-appliance", "guest-certified-appliance"}: + problems.append("realization binding driver does not match a governed appliance mode") + for field_name in ("connection_uri_digest", "name_prefix_digest"): + if not _is_canonical_sha256(binding.get(field_name)): + problems.append(f"realization binding requires canonical {field_name}") + return problems + + +def _validate_boot_artifact_binding(binding: Mapping[str, Any]) -> list[str]: + problems: list[str] = [] + boot_artifacts = binding.get("boot_artifact_digests") + if not isinstance(boot_artifacts, Mapping) or set(boot_artifacts) != {"kernel", "initramfs"}: + problems.append("realization binding requires kernel and initramfs artifact digests") + elif not all(_is_canonical_sha256(value) for value in boot_artifacts.values()): + problems.append("realization binding boot artifact digests must be canonical sha256 values") + return problems + + +def _driver_configuration_digest(binding: Mapping[str, Any]) -> str: + material = { + "driver": binding.get("driver"), + "configuration_digest": binding.get("configuration_digest"), + "boot_artifact_digests": binding.get("boot_artifact_digests"), + "connection_uri_digest": binding.get("connection_uri_digest"), + "name_prefix_digest": binding.get("name_prefix_digest"), + } + encoded = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + return "sha256:" + hashlib.sha256(encoded).hexdigest() + + +def _is_canonical_sha256(value: object) -> bool: + return isinstance(value, str) and _SHA256_RE.fullmatch(value) is not None + + +def _validate_daemon_observation_item(collection: str, item: Mapping[str, Any]) -> list[str]: + noun = "domain" if collection == "domains" else "network" + problems: list[str] = [] + if set(item) != _DAEMON_REQUIRED_FIELDS[collection] or not _valid_observation_identity(item): + problems.append(f"incomplete daemon {noun} observation") + elif collection == "domains" and not _valid_domain_observation(item): + problems.append("incomplete daemon domain observation") + elif collection == "networks" and not _valid_network_observation(item): + problems.append("incomplete daemon network observation") + return problems + + +def _valid_observation_identity(item: Mapping[str, Any]) -> bool: + return all(_nonempty_string(item.get(key)) for key in ("address", "name")) + + +def _valid_domain_observation(item: Mapping[str, Any]) -> bool: + checks = ( + _nonempty_string(item.get("architecture")), + _nonempty_string(item.get("image_policy")), + isinstance(item.get("memory_mib"), int) and item["memory_mib"] > 0, + isinstance(item.get("vcpus"), int) and item["vcpus"] > 0, + isinstance(item.get("network_attachments"), list | tuple), + ) + return all(checks) + + +def _valid_network_observation(item: Mapping[str, Any]) -> bool: + checks = ( + _nonempty_string(item.get("cidr")), + _nonempty_string(item.get("gateway")), + isinstance(item.get("internal"), bool), + item.get("forward_mode") in {"none", "nat"}, + ) + return all(checks) + + +def _nonempty_string(value: object) -> bool: + return isinstance(value, str) and bool(value) diff --git a/implementations/python/packages/aces_operations/_evidence_run_types.py b/implementations/python/packages/aces_operations/_evidence_run_types.py index 58fd8e3bc..d8bc8a0c5 100644 --- a/implementations/python/packages/aces_operations/_evidence_run_types.py +++ b/implementations/python/packages/aces_operations/_evidence_run_types.py @@ -17,9 +17,8 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass from pathlib import Path -from typing import Any, Protocol +from typing import Any, Literal, Protocol -from aces_backend_libvirt.techvault_native import NativeLibvirtProbe from aces_backend_protocols.capabilities import BackendManifest __all__ = [ @@ -27,8 +26,10 @@ "BackendManifest", "CompiledModel", "EvidenceArtifactInputs", + "EvidenceCheck", + "EvidenceSourceMode", "ExecutionPlan", - "NativeLibvirtProbe", + "LibvirtEvidenceRunConfig", "NodeDeployment", "ObservationBoundary", "ParticipantBehavior", @@ -36,6 +37,31 @@ "TerminalSnapshot", ] +EvidenceSourceMode = Literal["deterministic", "native-live", "guest-certified"] + + +@dataclass(frozen=True) +class EvidenceCheck: + """One named check over the scenario-evidence production run. + + Every check is gating: it contributes to ``LibvirtEvidenceRunReport.passed``. + There is deliberately no non-gating escape hatch — in particular, a native-live + run that fails to realize the libvirt substrate must report ``passed=False`` so + the mode can never claim success without actually realizing. + """ + + name: str + passed: bool + diagnostics: tuple[str, ...] = () + + +@dataclass(frozen=True) +class LibvirtEvidenceRunConfig: + """Runtime controls for the libvirt scenario-evidence producer.""" + + evidence_source_mode: EvidenceSourceMode = "deterministic" + connection_uri: str = "qemu:///system" + class ActionContract(Protocol): """Compiled action contract surface read by the proof builder.""" @@ -116,5 +142,6 @@ class EvidenceArtifactInputs: manifest: BackendManifest proof: Mapping[str, Any] native_snapshot: Mapping[str, Any] | None - probe: NativeLibvirtProbe | None + native_cleanup_verified: bool | None unrealized_capabilities: tuple[str, ...] = () + guest_observed: Mapping[str, Any] | None = None diff --git a/implementations/python/packages/aces_operations/_evidence_run_validation.py b/implementations/python/packages/aces_operations/_evidence_run_validation.py index bfafa6194..8951f2d0e 100644 --- a/implementations/python/packages/aces_operations/_evidence_run_validation.py +++ b/implementations/python/packages/aces_operations/_evidence_run_validation.py @@ -23,6 +23,7 @@ from pydantic import BaseModel from aces_operations._evidence_run_artifact import EVIDENCE_RUN_SCHEMA +from aces_operations._evidence_run_realization import _validate_realization_sources # Redaction gate: substrings/patterns that must never appear in the artifact. _FORBIDDEN_REDACTION_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = ( @@ -47,6 +48,7 @@ "scenario", "compiled_artifact", "backend", + "realization_facts", "realized_topology", "participant_action_proof", "terminal_observation", @@ -77,6 +79,7 @@ def validate_libvirt_evidence_run_artifact(payload: Mapping[str, Any]) -> list[s problems.extend(_validate_embedded_contracts(payload)) problems.extend(_validate_redaction(payload)) problems.extend(_validate_boundary(payload)) + problems.extend(_validate_realization_sources(payload)) return problems diff --git a/implementations/python/packages/aces_operations/_techvault_cleanup.py b/implementations/python/packages/aces_operations/_techvault_cleanup.py new file mode 100644 index 000000000..16c9e8c8d --- /dev/null +++ b/implementations/python/packages/aces_operations/_techvault_cleanup.py @@ -0,0 +1,57 @@ +"""Verified cleanup for captured TechVault native-substrate reports.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +from aces_backend_libvirt.techvault_native import TechVaultNativeLibvirtDriver + + +def cleanup_native_snapshot( + driver: TechVaultNativeLibvirtDriver, + snapshot: Mapping[str, object], +) -> tuple[bool, tuple[str, ...]]: + """Destroy every captured native address and require explicit absence handles.""" + + domains = _addresses(snapshot.get("domains")) + networks = _addresses(snapshot.get("networks")) + if not domains: + return False, ("native cleanup requires a captured domain inventory",) + try: + result = driver.destroy(networks=networks, domains=domains) + except Exception: + return False, ("native cleanup failed",) + diagnostics = tuple(f"{item.code} at {item.address}" for item in result.diagnostics) + verified = ( + not diagnostics + and _absence_handles(result.domains, domains) + and _absence_handles(result.networks, networks) + and driver.last_snapshot == {} + ) + if not verified and not diagnostics: + diagnostics = ("native cleanup did not return a complete verified-absence inventory",) + return verified, diagnostics + + +def _addresses(raw: object) -> tuple[str, ...]: + if not isinstance(raw, list | tuple): + return () + return tuple( + str(item["address"]) + for item in raw + if isinstance(item, Mapping) and isinstance(item.get("address"), str) and item.get("address") + ) + + +def _absence_handles( + handles: Sequence[object], + requested: tuple[str, ...], +) -> bool: + return ( + len(handles) == len(requested) + and {getattr(handle, "address", None) for handle in handles} == set(requested) + and all(getattr(handle, "realized", None) is False for handle in handles) + ) + + +__all__ = ["cleanup_native_snapshot"] diff --git a/implementations/python/packages/aces_operations/libvirt_evidence_run.py b/implementations/python/packages/aces_operations/libvirt_evidence_run.py index c6a8e893e..941b9ac09 100644 --- a/implementations/python/packages/aces_operations/libvirt_evidence_run.py +++ b/implementations/python/packages/aces_operations/libvirt_evidence_run.py @@ -21,14 +21,13 @@ * ``deterministic`` (default, no libvirt daemon — used by tests / CI): participant lifecycle proof + compiled topology + structural negative-boundary evidence + an - evaluator-only translated SOC-readback record explicitly marked as not upstream - Wazuh. -* ``native-live`` (operator-run; injected/default native driver + probe): - additionally realizes the libvirt VM/network substrate (including the scenario's - content and account placements, via cloud-init) and records the native topology - and native SOC readback. Any provisioning capability the backend genuinely cannot - realize is disclosed as ``unrealized_capabilities`` (and fails native-live), not - faked; orchestration/evaluation planes are outside a provisioning-only target. + evaluator-only declaration of defensive evidence channels; no SOC state is + observed. +* ``native-live`` (operator-run; injected/default native driver): + additionally realizes only the bounded VM/network substrate that passes the + TechVault concern-admission gate and records independently daemon-observed fields. + Guest content, accounts, features, ACLs, services, and SOC state are rejected or + disclosed as not observed; orchestration/evaluation remain separate planes. Artifact assembly lives in ``_evidence_run_artifact`` and validation in ``_evidence_run_validation`` (kept separate for the ADR-015 source-size cap). @@ -36,23 +35,25 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path -from typing import Any, Literal +from typing import Any from aces_backend_libvirt.target import create_libvirt_target -from aces_backend_libvirt.techvault_native import NativeLibvirtProbe, TechVaultNativeLibvirtDriver +from aces_backend_libvirt.techvault_native import TechVaultNativeLibvirtDriver from aces_runtime.control_plane import RuntimeControlPlane from aces_runtime.manager import RuntimeManager from aces_sdl.parser import parse_sdl_file from aces_operations._evidence_run_artifact import EVIDENCE_RUN_SCHEMA, assemble_artifact +from aces_operations._evidence_run_native import _default_native_driver_factory, _run_native_mode from aces_operations._evidence_run_types import ( CompiledModel, EvidenceArtifactInputs, - ExecutionPlan, + EvidenceCheck, + LibvirtEvidenceRunConfig, ParticipantBehavior, ) from aces_operations._evidence_run_validation import validate_libvirt_evidence_run_artifact @@ -73,33 +74,7 @@ _PROOF_EPISODE_ID = "proof-ep-1" -EvidenceSourceMode = Literal["deterministic", "native-live"] - - -@dataclass(frozen=True) -class EvidenceCheck: - """One named check over the scenario-evidence production run. - - Every check is gating: it contributes to ``LibvirtEvidenceRunReport.passed``. - There is deliberately no non-gating escape hatch — in particular, a native-live - run that fails to realize the libvirt substrate must report ``passed=False`` so - the mode can never claim success without actually realizing. - """ - - name: str - passed: bool - diagnostics: tuple[str, ...] = () - - -@dataclass(frozen=True) -class LibvirtEvidenceRunConfig: - """Runtime controls for the libvirt scenario-evidence producer.""" - - evidence_source_mode: EvidenceSourceMode = "deterministic" - connection_uri: str = "qemu:///system" - boot_timeout_seconds: int = 180 - appliance_memory_mib: int = 128 - clean_boot: bool = True +_NATIVE_MODES = frozenset({"native-live", "guest-certified"}) @dataclass(frozen=True) @@ -141,7 +116,6 @@ def run_libvirt_evidence_run( run_id: str, config: LibvirtEvidenceRunConfig | None = None, driver_factory: Callable[[], TechVaultNativeLibvirtDriver] | None = None, - probe: NativeLibvirtProbe | None = None, ) -> LibvirtEvidenceRunReport: """Produce the libvirt scenario evaluator-evidence artifact for ``scenario_path``.""" settings = config or LibvirtEvidenceRunConfig() @@ -156,15 +130,15 @@ def run_libvirt_evidence_run( return LibvirtEvidenceRunReport(scenario_path.name, run_id, str(project_dir), mode, tuple(checks)) native_driver: TechVaultNativeLibvirtDriver | None = None - if mode == "native-live": - native_driver = (driver_factory or _default_native_driver_factory(project_dir, run_id, settings))() + if mode in _NATIVE_MODES: + native_driver = (driver_factory or _default_native_driver_factory(project_dir, run_id, settings, mode))() try: target = create_libvirt_target(participant_runtime=True, driver=native_driver) execution_plan = RuntimeManager(target).plan(parse_sdl_file(scenario_path)) control_plane = RuntimeControlPlane(target) - except Exception as exc: - checks.append(EvidenceCheck("scenario_plan", False, (f"failed to plan scenario: {exc}",))) + except Exception: + checks.append(EvidenceCheck("scenario_plan", False, ("failed to plan scenario",))) return LibvirtEvidenceRunReport(scenario_path.name, run_id, str(project_dir), mode, tuple(checks)) model = execution_plan.model @@ -172,12 +146,13 @@ def run_libvirt_evidence_run( checks.append(EvidenceCheck("participant_action_proof", proof["lifecycle_clean"], tuple(proof["diagnostics"]))) native_snapshot: Mapping[str, Any] | None = None + native_cleanup_verified: bool | None = None unrealized_capabilities: tuple[str, ...] = () - if mode == "native-live": - native_snapshot, realize_check, unrealized_capabilities = _realize_native_substrate( - execution_plan, control_plane, native_driver + guest_observed: Mapping[str, Any] | None = None + if mode in _NATIVE_MODES and native_driver is not None: + native_snapshot, native_cleanup_verified, unrealized_capabilities, guest_observed = _run_native_mode( + mode, execution_plan, control_plane, native_driver, driver_factory, checks ) - checks.append(realize_check) inputs = EvidenceArtifactInputs( scenario_path=scenario_path, @@ -188,8 +163,9 @@ def run_libvirt_evidence_run( manifest=execution_plan.manifest, proof=proof, native_snapshot=native_snapshot, - probe=probe if mode == "native-live" else None, + native_cleanup_verified=native_cleanup_verified, unrealized_capabilities=unrealized_capabilities, + guest_observed=guest_observed, ) artifact, artifact_path = _finalize_artifact(inputs, project_dir, checks) return LibvirtEvidenceRunReport( @@ -222,8 +198,8 @@ def _persist_artifact( try: target_path = run_artifact_path(project_dir, run_id, "scenario-evidence", "libvirt-scenario-evidence-run.json") atomic_write_json_artifact(target_path, artifact) - except OSError as exc: - return None, EvidenceCheck("artifact_write", False, (f"artifact write failed: {exc}",)) + except OSError: + return None, EvidenceCheck("artifact_write", False, ("artifact write failed",)) return str(target_path), EvidenceCheck("artifact_write", True) @@ -314,82 +290,3 @@ def _admit_one_action( action_address if accepted else None, None if accepted else f"admit rejected for {behavior_address}/{action_address}", ) - - -def _default_native_driver_factory( - project_dir: Path, run_id: str, settings: LibvirtEvidenceRunConfig -) -> Callable[[], TechVaultNativeLibvirtDriver]: - """Build the default native libvirt driver factory for operator-run native-live mode. - - Mirrors the TechVault live gate: the driver connects to a real libvirt daemon at - realize time. In CI/tests a fake driver_factory is injected instead, so this is - never exercised without a daemon. - """ - state_dir = project_dir / "runs" / run_id / "scenario-evidence" / "libvirt" - - def factory() -> TechVaultNativeLibvirtDriver: - return TechVaultNativeLibvirtDriver( - state_dir=state_dir, - connection_uri=settings.connection_uri, - name_prefix="aces-evidence", - appliance_memory_mib=settings.appliance_memory_mib, - clean_existing=settings.clean_boot, - ) - - return factory - - -def _realize_native_substrate( - execution_plan: ExecutionPlan, - control_plane: RuntimeControlPlane, - native_driver: TechVaultNativeLibvirtDriver | None, -) -> tuple[Mapping[str, Any] | None, EvidenceCheck, tuple[str, ...]]: - """Realize the libvirt provisioning substrate (VMs + networks) for the scenario. - - The libvirt backend realizes the full provisioning plane — nodes, networks, and - content/account/feature placements (via cloud-init) — so a governed provisioning - capability is realized rather than disclosed. Any capability the backend still - cannot realize (an ungoverned provisioning term, or orchestration/evaluation - outside a provisioning-only target) is returned as ``unrealized_capabilities`` - and disclosed in the artifact. The check is gating and passes only when the - native driver realized at least one domain — native-live must never report - success without realizing — so a scenario the backend cannot provision fails - native-live and surfaces its unrealized capabilities rather than silently passing. - """ - if native_driver is None: - return None, EvidenceCheck("native_substrate_realization", False, ("no native driver",)), () - try: - receipt = control_plane.submit_provisioning(execution_plan.provisioning) - status = control_plane.get_operation(receipt.operation_id) - except Exception as exc: - return None, EvidenceCheck("native_substrate_realization", False, (f"native realization raised: {exc}",)), () - unrealized = _dedupe( - f"{d.code}: {d.message}" - for source in (execution_plan.diagnostics, () if status is None else status.diagnostics) - for d in source - if d.is_error - ) - snapshot = native_driver.last_snapshot - realized = _snapshot_has_domains(snapshot) - check = EvidenceCheck( - "native_substrate_realization", - realized, - () - if realized - else ("libvirt backend realized no native substrate for this scenario; capabilities disclosed as unrealized",), - ) - return (snapshot if realized else None), check, unrealized - - -def _dedupe(items: Iterable[str]) -> tuple[str, ...]: - seen: dict[str, None] = {} - for item in items: - seen.setdefault(item, None) - return tuple(seen) - - -def _snapshot_has_domains(snapshot: Mapping[str, Any] | None) -> bool: - if not isinstance(snapshot, Mapping): - return False - domains = snapshot.get("domains", ()) - return isinstance(domains, list | tuple) and len(domains) > 0 diff --git a/implementations/python/packages/aces_operations/run_artifacts.py b/implementations/python/packages/aces_operations/run_artifacts.py index 8a5563e2c..8f83f6428 100644 --- a/implementations/python/packages/aces_operations/run_artifacts.py +++ b/implementations/python/packages/aces_operations/run_artifacts.py @@ -31,6 +31,15 @@ def is_valid_run_id_label(run_id: str) -> bool: return bool(RUN_ID_LABEL_PATTERN.match(run_id)) +def portable_artifact_ref(path: Path) -> str: + """Return a repository-portable reference without exposing a host path.""" + + for anchor in ("examples", "contracts", "specs", "docs"): + if anchor in path.parts: + return "/".join(path.parts[path.parts.index(anchor) :]) + return path.name + + def run_artifact_path(output_dir: Path, run_id: str, subdir: str, filename: str) -> Path: """Return the archive path ``/runs///``. diff --git a/implementations/python/packages/aces_operations/techvault_live.py b/implementations/python/packages/aces_operations/techvault_live.py index e4fc67917..e6dd34da8 100644 --- a/implementations/python/packages/aces_operations/techvault_live.py +++ b/implementations/python/packages/aces_operations/techvault_live.py @@ -2,6 +2,9 @@ from __future__ import annotations +import hashlib +import json +import re from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass from datetime import UTC, datetime @@ -9,38 +12,22 @@ from typing import Any from aces_backend_libvirt.target import create_libvirt_target -from aces_backend_libvirt.techvault_native import ( - NativeLibvirtProbe, - TechVaultNativeLibvirtDriver, - check_native_readiness, - expected_surface, - native_soc_readback, -) +from aces_backend_libvirt.techvault_native import TechVaultNativeLibvirtDriver, expected_surface from aces_runtime.control_plane import RuntimeControlPlane from aces_runtime.manager import RuntimeManager from aces_sdl.parser import parse_sdl_file +from aces_operations._evidence_run_validation import redaction_violations +from aces_operations._techvault_cleanup import cleanup_native_snapshot from aces_operations.run_artifacts import ( atomic_write_json_artifact, is_valid_run_id_label, + portable_artifact_ref, run_artifact_path, ) -DEFAULT_EVENT_WINDOW_SECONDS = 180 -DEFAULT_BOOT_TIMEOUT_SECONDS = 180 -_FULL_SOC_NODES = frozenset( - { - "wazuh-manager", - "wazuh-indexer", - "wazuh-dashboard", - "suricata", - "misp", - "thehive", - "cortex", - "shuffle-backend", - "shuffle-frontend", - } -) +_LIVE_SCHEMA = "aces.libvirt.techvault-native-live-gate/v1" +_SHA256_RE = re.compile(r"sha256:[a-f0-9]{64}") @dataclass(frozen=True) @@ -83,11 +70,7 @@ def render(self) -> str: class TechVaultLiveConfig: """Runtime controls for the native ACES/libvirt TechVault live gate.""" - clean_boot: bool = True - event_window_seconds: int = DEFAULT_EVENT_WINDOW_SECONDS - boot_timeout_seconds: int = DEFAULT_BOOT_TIMEOUT_SECONDS connection_uri: str = "qemu:///system" - appliance_memory_mib: int = 128 def validate_techvault_live( @@ -97,7 +80,6 @@ def validate_techvault_live( run_id: str, config: TechVaultLiveConfig | None = None, driver_factory: Callable[[], TechVaultNativeLibvirtDriver] | None = None, - probe: NativeLibvirtProbe | None = None, ) -> TechVaultLiveReport: """Boot and validate a TechVault SDL through native ACES/libvirt.""" @@ -116,8 +98,6 @@ def validate_techvault_live( state_dir=run_dir / "libvirt", connection_uri=settings.connection_uri, name_prefix="aces-techvault", - appliance_memory_mib=settings.appliance_memory_mib, - clean_existing=settings.clean_boot, ) ) target = create_libvirt_target(driver=driver, name_prefix="aces-techvault") @@ -128,27 +108,17 @@ def validate_techvault_live( boot_check = _apply_plan(target, scenario_path, driver) checks.append(boot_check) snapshot = driver.last_snapshot - evidence: dict[str, object] = {} if boot_check.passed: checks.append(_substrate_independence_check(snapshot)) checks.append(_surface_check(snapshot)) - readiness_check = _readiness_check( - snapshot, probe or NativeLibvirtProbe(), settings.boot_timeout_seconds - ) - checks.append(readiness_check) - checks.append(_kali_reachability_check(snapshot, probe or NativeLibvirtProbe())) - soc_check, soc_evidence = _soc_stack_readback_check(snapshot) - checks.append(soc_check) - evidence.update(soc_evidence) - checks.append(_variation_check(snapshot)) + cleanup_ok, cleanup_diagnostics = cleanup_native_snapshot(driver, snapshot) + checks.append(LiveCheck("verified_native_cleanup", cleanup_ok, cleanup_diagnostics)) manifest_path = _write_manifest( output_dir, run_id, scenario_path, - driver, + snapshot, checks, - evidence, - clean_boot=settings.clean_boot, ) checks.append( LiveCheck( @@ -170,8 +140,8 @@ def _plan_scenario(target: object, scenario_path: Path) -> tuple[object | None, try: scenario = parse_sdl_file(scenario_path) execution_plan = RuntimeManager(target).plan(scenario) - except Exception as exc: - return None, LiveCheck("planning", False, (f"scenario planning failed: {exc}",)) + except Exception: + return None, LiveCheck("planning", False, ("scenario planning failed",)) diagnostics = tuple(f"{diag.code}: {diag.message}" for diag in execution_plan.diagnostics if diag.is_error) if diagnostics: return scenario, LiveCheck("planning", False, diagnostics) @@ -187,8 +157,8 @@ def _apply_plan(target: object, scenario_path: Path, driver: TechVaultNativeLibv control_plane = RuntimeControlPlane(target, initial_snapshot=execution_plan.base_snapshot) receipt = control_plane.submit_provisioning(execution_plan.provisioning) status = control_plane.get_operation(receipt.operation_id) - except Exception as exc: - diagnostics = (f"provisioning raised: {exc}",) + except Exception: + diagnostics = ("provisioning failed",) else: if status is None: diagnostics = ("control plane did not record provisioning status",) @@ -226,96 +196,54 @@ def _surface_check(snapshot: Mapping[str, Any]) -> LiveCheck: return LiveCheck("model_derived_native_surface", not diagnostics, tuple(diagnostics)) -def _readiness_check(snapshot: Mapping[str, Any], probe: NativeLibvirtProbe, timeout_seconds: int) -> LiveCheck: - ok, diagnostics = check_native_readiness(snapshot, probe=probe, timeout_seconds=timeout_seconds) - return LiveCheck("native_domain_service_readiness", ok, tuple(diagnostics)) - - -def _kali_reachability_check(snapshot: Mapping[str, Any], probe: NativeLibvirtProbe) -> LiveCheck: - kali = _domain_by_name(snapshot, "kali") - if kali is None: - return LiveCheck("kali_target_network_reachability", True, ("scenario does not include kali",)) - targets = _targets_sharing_network(kali, snapshot) - diagnostics: list[str] = [] - for target in targets: - ip = _first_ip(target) - if ip and not probe.ping(ip).ok: - diagnostics.append(f"kali-shared target {target.get('name')} is not reachable at {ip}") - return LiveCheck("kali_target_network_reachability", not diagnostics, tuple(diagnostics)) - - -def _soc_stack_readback_check(snapshot: Mapping[str, Any]) -> tuple[LiveCheck, dict[str, object]]: - names = {str(domain.get("name", "")) for domain in _domains(snapshot)} - evidence = {"soc_readback": native_soc_readback(snapshot)} - diagnostics: list[str] = [] - if _FULL_SOC_NODES.issubset(names): - diagnostics.extend(_full_soc_diagnostics(evidence["soc_readback"])) - return LiveCheck("native_soc_stack_readback", not diagnostics, tuple(diagnostics)), evidence - - -def _full_soc_diagnostics(readback: object) -> tuple[str, ...]: - if not isinstance(readback, Mapping): - return ("native SOC readback is not structured",) - diagnostics: list[str] = [] - suricata = readback.get("suricata", {}) - case_mgmt = readback.get("case_management", {}) - agents = readback.get("wazuh_active_agents", ()) - if not agents: - diagnostics.append("native Wazuh readback reported no active agents") - diagnostics.extend(_suricata_diagnostics(suricata)) - if not _has_case_management(case_mgmt): - diagnostics.append("native case-management readback is missing TheHive, MISP, Cortex, or Shuffle") - return tuple(diagnostics) - - -def _suricata_diagnostics(suricata: object) -> tuple[str, ...]: - if not isinstance(suricata, Mapping): - return ("native Suricata readback is not structured",) - diagnostics: list[str] = [] - if suricata.get("rules_loaded", 0) <= 0: - diagnostics.append("native Suricata readback reported no loaded rules") - if suricata.get("rules_failed", 0) != 0: - diagnostics.append("native Suricata readback reported failed rules") - if suricata.get("kernel_drops", 0) != 0: - diagnostics.append("native Suricata readback reported kernel drops") - return tuple(diagnostics) - - -def _has_case_management(case_mgmt: object) -> bool: - return isinstance(case_mgmt, Mapping) and all( - case_mgmt.get(name) for name in ("thehive", "misp", "cortex", "shuffle") - ) - - -def _variation_check(snapshot: Mapping[str, Any]) -> LiveCheck: - roles = {str(domain.get("role", "")) for domain in _domains(snapshot) if domain.get("role")} - if len(roles) >= 1 and len(_domains(snapshot)) != 30: - return LiveCheck("scenario_variant_composability", True) - if len(roles) >= 4: - return LiveCheck("scenario_variant_composability", True) - return LiveCheck("scenario_variant_composability", False, ("native surface collapsed to too few role families",)) - - def _write_manifest( output_dir: Path, run_id: str, scenario_path: Path, - driver: TechVaultNativeLibvirtDriver, + snapshot: Mapping[str, object], checks: Sequence[LiveCheck], - evidence: Mapping[str, object], - *, - clean_boot: bool, ) -> 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) + 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) + cleanup_status = _live_cleanup_status(native_succeeded, cleanup_check) payload = { - "schema": "aces.libvirt.techvault-native-live-gate/v1", - "scenario": {"path": str(scenario_path), "name": scenario_path.name.split(".")[0]}, + "schema": _LIVE_SCHEMA, + "scenario": { + "path": portable_artifact_ref(scenario_path), + "name": scenario_path.name.split(".")[0], + }, "run_id": run_id, "recorded_at": datetime.now(UTC).isoformat(), - "clean_boot": clean_boot, - "aces_libvirt": { - "substrate": "libvirt-qemu-initramfs", - "surface": expected_surface(driver.last_snapshot), + "cleanup_policy": "current-operation-owned-resources-only", + "cleanup": {"source": "driver-reported", "status": cleanup_status}, + "realization_binding": observed_snapshot.get("binding"), + "realization_facts": { + "authored": { + "source": "authored", + "scenario_ref": portable_artifact_ref(scenario_path), + }, + "planned": { + "source": "planned", + "status": "accepted" + if any(check.name == "planning" and check.passed for check in checks) + else "failed", + }, + "driver_reported": { + "source": "driver-reported", + "status": "succeeded" if native_succeeded else "failed", + }, + "daemon_observed": { + "source": "daemon-observed", + "domains": list(native_surface["domains"]), + "networks": list(native_surface["networks"]), + }, + "guest_observed": { + "source": "guest-observed", + "status": "not-observed", + }, }, "validation": { "ok": all(check.passed for check in checks), @@ -323,9 +251,9 @@ def _write_manifest( {"name": check.name, "ok": check.passed, "diagnostics": list(check.diagnostics)} for check in checks ], }, - "snapshot": driver.last_snapshot, - "evidence": dict(evidence), } + if validate_techvault_live_manifest(payload): + return None try: atomic_write_json_artifact(target, payload) except OSError: @@ -333,41 +261,185 @@ def _write_manifest( return str(target) -def _domains(snapshot: Mapping[str, Any]) -> tuple[Mapping[str, Any], ...]: - raw = snapshot.get("domains", ()) - return tuple(item for item in raw if isinstance(item, Mapping)) if isinstance(raw, list | tuple) else () +def _live_cleanup_status(native_succeeded: bool, cleanup_check: LiveCheck | None) -> str: + if not native_succeeded: + return "not-required" + return "verified" if cleanup_check is not None and cleanup_check.passed else "failed" + + +def validate_techvault_live_manifest(payload: Mapping[str, object]) -> list[str]: + """Validate source separation and redaction before a live manifest is written.""" + + violations = _validate_live_manifest_metadata(payload) + violations.extend(_validate_live_scenario(payload)) + violations.extend(_validate_live_realization_facts(payload)) + violations.extend(_validate_live_forbidden_terms(payload)) + violations.extend(redaction_violations(payload)) + return violations + + +def _validate_live_manifest_metadata(payload: Mapping[str, object]) -> list[str]: + violations: list[str] = [] + if payload.get("schema") != _LIVE_SCHEMA: + violations.append("invalid TechVault live manifest schema") + if payload.get("cleanup_policy") != "current-operation-owned-resources-only": + violations.append("invalid TechVault cleanup policy") + cleanup = payload.get("cleanup") + if not isinstance(cleanup, Mapping) or cleanup.get("source") != "driver-reported": + violations.append("cleanup must be a driver-reported section") + return violations + + +def _validate_live_scenario(payload: Mapping[str, object]) -> list[str]: + scenario = payload.get("scenario") + path = scenario.get("path") if isinstance(scenario, Mapping) else None + if not isinstance(path, str) or not path or path.startswith("/") or ".." in Path(path).parts: + return ["scenario.path must be a portable reference"] + return [] + + +def _validate_live_realization_facts(payload: Mapping[str, object]) -> list[str]: + facts = payload.get("realization_facts") + if not isinstance(facts, Mapping): + return ["realization_facts must be a mapping"] + violations = _validate_live_fact_sources(facts) + daemon = facts.get("daemon_observed") + domains = daemon.get("domains", ()) if isinstance(daemon, Mapping) else () + networks = daemon.get("networks", ()) if isinstance(daemon, Mapping) else () + violations.extend(_validate_live_daemon_names(domains, networks)) + driver_reported = facts.get("driver_reported") + succeeded = isinstance(driver_reported, Mapping) and driver_reported.get("status") == "succeeded" + binding = payload.get("realization_binding") + cleanup = payload.get("cleanup") + cleanup_status = cleanup.get("status") if isinstance(cleanup, Mapping) else None + violations.extend(_validate_live_outcome(succeeded, cleanup_status, domains, networks, binding)) + if isinstance(binding, Mapping): + violations.extend(_validate_live_binding(binding)) + return violations + + +def _validate_live_fact_sources(facts: Mapping[str, object]) -> list[str]: + expected_sources = { + "authored": "authored", + "planned": "planned", + "driver_reported": "driver-reported", + "daemon_observed": "daemon-observed", + "guest_observed": "guest-observed", + } + return [ + f"{key}.source must be {source}" + for key, source in expected_sources.items() + if not isinstance(facts.get(key), Mapping) or facts[key].get("source") != source + ] + + +def _validate_live_daemon_names(domains: object, networks: object) -> list[str]: + violations: list[str] = [] + for collection_name, values in (("domains", domains), ("networks", networks)): + if not _bounded_native_names(values): + violations.append(f"daemon_observed.{collection_name} must contain bounded native names") + return violations + + +def _bounded_native_names(values: object) -> bool: + return isinstance(values, list | tuple) and all(isinstance(value, str) and value for value in values) + + +def _validate_live_outcome( + succeeded: bool, + cleanup_status: object, + domains: object, + networks: object, + binding: object, +) -> list[str]: + if succeeded: + return _validate_successful_live_outcome(cleanup_status, domains, binding) + return _validate_failed_live_outcome(cleanup_status, domains, networks, binding) + + +def _validate_successful_live_outcome(cleanup_status: object, domains: object, binding: object) -> list[str]: + violations: list[str] = [] + if cleanup_status not in {"verified", "failed"}: + violations.append("successful native realization requires an explicit cleanup outcome") + if not domains or not isinstance(binding, Mapping): + violations.append("successful native run requires daemon observations and realization binding") + return violations + + +def _validate_failed_live_outcome( + cleanup_status: object, + domains: object, + networks: object, + binding: object, +) -> list[str]: + violations: list[str] = [] + if cleanup_status != "not-required": + violations.append("failed native realization must mark cleanup not-required") + if domains or networks or binding is not None: + violations.append("failed native run cannot publish stale daemon observations or binding") + return violations + + +def _validate_live_binding(binding: Mapping[str, object]) -> list[str]: + violations = _validate_live_binding_identity(binding) + violations.extend(_validate_live_boot_digests(binding)) + expected_digest = _live_binding_digest(binding) + if binding.get("driver_configuration_digest") != expected_digest: + violations.append("realization binding driver configuration digest does not match its material") + return violations + + +def _validate_live_binding_identity(binding: Mapping[str, object]) -> list[str]: + violations: list[str] = [] + if binding.get("driver") != "techvault-appliance": + violations.append("realization binding driver must be techvault-appliance") + for field_name in ( + "realization_envelope_digest", + "configuration_digest", + "driver_configuration_digest", + "connection_uri_digest", + "name_prefix_digest", + ): + if not _canonical_digest(binding.get(field_name)): + violations.append(f"realization binding requires canonical {field_name}") + return violations + + +def _validate_live_boot_digests(binding: Mapping[str, object]) -> list[str]: + boot_artifacts = binding.get("boot_artifact_digests") + if not isinstance(boot_artifacts, Mapping) or set(boot_artifacts) != {"kernel", "initramfs"}: + return ["realization binding requires kernel and initramfs digests"] + if not all(_canonical_digest(value) for value in boot_artifacts.values()): + return ["realization binding boot digests must be canonical sha256 values"] + return [] + + +def _live_binding_digest(binding: Mapping[str, object]) -> str: + material = { + "driver": binding.get("driver"), + "configuration_digest": binding.get("configuration_digest"), + "boot_artifact_digests": binding.get("boot_artifact_digests"), + "connection_uri_digest": binding.get("connection_uri_digest"), + "name_prefix_digest": binding.get("name_prefix_digest"), + } + encoded = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + return "sha256:" + hashlib.sha256(encoded).hexdigest() -def _domain_by_name(snapshot: Mapping[str, Any], name: str) -> Mapping[str, Any] | None: - return next((domain for domain in _domains(snapshot) if domain.get("name") == name), None) +def _canonical_digest(value: object) -> bool: + return isinstance(value, str) and _SHA256_RE.fullmatch(value) is not None -def _targets_sharing_network(kali: Mapping[str, Any], snapshot: Mapping[str, Any]) -> list[Mapping[str, Any]]: - kali_networks = { - str(interface.get("network_address", "")) for interface in _interfaces(kali) if interface.get("network_address") - } - targets: list[Mapping[str, Any]] = [] - for domain in _domains(snapshot): - if domain.get("name") == "kali": - continue - domain_networks = { - str(interface.get("network_address", "")) - for interface in _interfaces(domain) - if interface.get("network_address") - } - if kali_networks & domain_networks: - targets.append(domain) - return targets - - -def _interfaces(domain: Mapping[str, Any]) -> tuple[Mapping[str, Any], ...]: - raw = domain.get("interfaces", ()) - return tuple(item for item in raw if isinstance(item, Mapping)) if isinstance(raw, list | tuple) else () +def _validate_live_forbidden_terms(payload: Mapping[str, object]) -> list[str]: + violations: list[str] = [] + rendered = json.dumps(payload, sort_keys=True, default=str) + if "native-realized" in rendered: + violations.append("native-realized is not an admitted observation basis") + if "soc_readback" in rendered: + violations.append("daemon substrate cannot supply SOC readback") + return violations -def _first_ip(domain: Mapping[str, Any]) -> str | None: - for interface in _interfaces(domain): - ip = interface.get("ip") - if isinstance(ip, str) and ip: - return ip - return None +def _domains(snapshot: Mapping[str, Any]) -> tuple[Mapping[str, Any], ...]: + raw = snapshot.get("domains", ()) + return tuple(item for item in raw if isinstance(item, Mapping)) if isinstance(raw, list | tuple) else () diff --git a/implementations/python/packages/aces_processor/compiler.py b/implementations/python/packages/aces_processor/compiler.py index af36b1f71..eceec8aa3 100644 --- a/implementations/python/packages/aces_processor/compiler.py +++ b/implementations/python/packages/aces_processor/compiler.py @@ -8,17 +8,19 @@ WorkflowFeature, WorkflowStatePredicateFeature, ) +from aces_contracts.addressing import render_compiled_address from aces_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION +from aces_sdl import build_declaration_index from aces_sdl.entities import flatten_entities from aces_sdl.explicitness import ExplicitnessClass -from aces_sdl.instantiate import instantiate_scenario +from aces_sdl.instantiate import admit_instantiated_scenario, instantiate_scenario from aces_sdl.nodes import NodeType from aces_sdl.orchestration import WorkflowStepType from aces_sdl.participant_outcome_semantics import ( OutcomeInterpretationSourceLayer, OutcomeInterpretationTargetLayer, ) -from aces_sdl.scenario import InstantiatedScenario, Scenario +from aces_sdl.scenario import ExpandedScenario, InstantiatedScenario, Scenario from aces_sdl.semantics.objective_semantics import ( OBJECTIVE_WINDOW_DEPENDENCY_ROLES, partition_objective_dependencies, @@ -30,6 +32,8 @@ from .models import ( AccountPlacement, + AssertionRuntime, + CompiledCapabilityConstraint, ConditionBinding, ContentPlacement, Diagnostic, @@ -48,6 +52,7 @@ ParticipantBehaviorSpecificationRuntime, ParticipantObservationBoundaryRuntime, ParticipantOutcomeInterpretationRuleRuntime, + PropositionRuntime, RuntimeModel, RuntimeTemplate, ScriptRuntime, @@ -77,7 +82,7 @@ def _dump(model: Any) -> dict[str, Any]: def _address(*parts: str) -> str: - return ".".join(part for part in parts if part) + return render_compiled_address(*parts) def _dedupe(items: list[str]) -> tuple[str, ...]: @@ -224,13 +229,17 @@ def _service_address(node_name: str, service_name: str) -> str: return _address("provision", "node", node_name, "service", service_name) -def _split_node_service_ref(ref: object) -> tuple[str, str] | None: - if not isinstance(ref, str) or not ref.startswith("nodes."): - return None - node_name, sep, service_name = ref[len("nodes.") :].partition(".services.") - if not sep or not node_name or not service_name: +def _resolve_node_service_ref( + scenario: InstantiatedScenario, + ref: object, +) -> tuple[str, str] | None: + if not isinstance(ref, str): return None - return node_name, service_name + for node_name, node in scenario.nodes.items(): + for service in node.services: + if service.name and ref == f"nodes.{node_name}.services.{service.name}": + return node_name, service.name + return None def _action_contract_address(name: str) -> str: @@ -257,6 +266,14 @@ def _condition_binding_address(node_name: str, condition_name: str) -> str: return _address("evaluation", "condition", node_name, condition_name) +def _proposition_address(name: str) -> str: + return _address("evaluation", "proposition", name) + + +def _assertion_address(name: str) -> str: + return _address("evaluation", "assertion", name) + + def _inject_address(name: str) -> str: return _address("orchestration", "inject", name) @@ -453,7 +470,7 @@ def _condition_addresses_for_refs(scenario: InstantiatedScenario, refs: list[str def _service_addresses_for_refs(scenario: InstantiatedScenario, refs: list[str]) -> tuple[str, ...]: addresses: list[str] = [] for ref in dict.fromkeys(refs): - split = _split_node_service_ref(ref) + split = _resolve_node_service_ref(scenario, ref) if split is not None: node_name, service_name = split node = scenario.nodes.get(node_name) @@ -624,11 +641,8 @@ def _resolve_resource_refs( resolved: list[str] = [] diagnostics: list[Diagnostic] = [] for ref_name in dict.fromkeys(ref_names): - matched_address = next( - (address for address, resource in resources.items() if resource.name == ref_name), - None, - ) - if matched_address is None: + matched_addresses = sorted(address for address, resource in resources.items() if resource.name == ref_name) + if not matched_addresses: diagnostics.append( Diagnostic( code=f"{code_prefix}-unbound", @@ -638,7 +652,20 @@ def _resolve_resource_refs( ) ) continue - resolved.append(matched_address) + if len(matched_addresses) > 1: + diagnostics.append( + Diagnostic( + code=f"{code_prefix}-ambiguous", + domain=domain, + address=owner_address, + message=( + f"Reference '{ref_name}' resolves to multiple {resource_label}s: " + f"{', '.join(matched_addresses)}." + ), + ) + ) + continue + resolved.append(matched_addresses[0]) return _dedupe(resolved), diagnostics @@ -728,7 +755,7 @@ class _ObjectiveWindowCompilation: @dataclass(frozen=True) class _WorkflowPredicateCompilation: predicate: WorkflowPredicateRuntime - condition_addresses: tuple[str, ...] + assertion_addresses: tuple[str, ...] predicate_addresses: tuple[str, ...] objective_addresses: tuple[str, ...] step_state_predicates: tuple[WorkflowStepStatePredicateRuntime, ...] @@ -740,7 +767,7 @@ class _WorkflowCompilationState: control_steps: dict[str, WorkflowStepRuntime] = field(default_factory=dict) control_edges: dict[str, tuple[str, ...]] = field(default_factory=dict) referenced_objectives: list[str] = field(default_factory=list) - step_condition_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) + step_assertion_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) step_predicate_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) required_features: list[WorkflowFeature] = field(default_factory=list) required_state_predicate_features: list[WorkflowStatePredicateFeature] = field(default_factory=list) @@ -823,22 +850,32 @@ def _compile_templates( def _metadata_specs( scenario: InstantiatedScenario, -) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]: +) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: entity_specs = {name: _dump(entity) for name, entity in flatten_entities(scenario.entities).items()} agent_specs = {name: _dump(agent) for name, agent in scenario.agents.items()} relationship_specs = {name: _dump(relationship) for name, relationship in scenario.relationships.items()} - variable_specs = {name: _dump(variable) for name, variable in scenario.variables.items()} - for prefixed_name, spec in scenario.module_variable_specs.items(): - if prefixed_name in variable_specs: - raise ValueError( - "Outer scenario variable " - f"{prefixed_name!r} collides with a module-import " - "provenance entry generated under the reserved private " - "namespace; rename the outer variable so it does not " - "shadow the imported domain." + return entity_specs, agent_specs, relationship_specs + + +def _compile_capability_constraints( + scenario: InstantiatedScenario, +) -> tuple[CompiledCapabilityConstraint, ...]: + compiled: list[CompiledCapabilityConstraint] = [] + for constraint in scenario.instantiation_provenance.capability_constraints: + parts = constraint.field_pointer.split("/") + section_name, encoded_name, field_name = parts[1:] + node_name = encoded_name.replace("~1", "/").replace("~0", "~") + node = scenario.nodes[node_name] + address = _network_address(node_name) if node.type == NodeType.SWITCH else _node_address(node_name) + compiled.append( + CompiledCapabilityConstraint( + address=address, + concern=f"{section_name}.{field_name}", + parameter=constraint.parameter, + allowed_values=constraint.allowed_values, ) - variable_specs[prefixed_name] = spec - return entity_specs, agent_specs, relationship_specs, variable_specs + ) + return tuple(compiled) def _node_dependency_addresses( @@ -1025,9 +1062,49 @@ def _compile_feature_bindings( return feature_bindings +def _compile_propositions( + scenario: InstantiatedScenario, +) -> dict[str, PropositionRuntime]: + address_index = _runtime_addressable_ref_index(scenario) + return { + _proposition_address(name): PropositionRuntime( + address=_proposition_address(name), + name=name, + spec=_dump(proposition), + subject_addresses=_runtime_addresses_for_refs( + list(proposition.subjects), + addressable_ref_index=address_index, + ), + predicate_kind=proposition.predicate.kind, + evaluation_basis=proposition.basis.value, + evidence_requirement_refs=tuple(proposition.evidence_requirements), + ) + for name, proposition in scenario.propositions.items() + } + + +def _compile_assertions( + scenario: InstantiatedScenario, +) -> dict[str, AssertionRuntime]: + return { + _assertion_address(name): AssertionRuntime( + address=_assertion_address(name), + name=name, + spec=_dump(assertion), + proposition_address=_proposition_address(assertion.proposition), + role=assertion.role.value, + polarity=assertion.polarity.value, + ordering_dependencies=(_proposition_address(assertion.proposition),), + refresh_dependencies=(_proposition_address(assertion.proposition),), + ) + for name, assertion in scenario.assertions.items() + } + + def _compile_condition_bindings( scenario: InstantiatedScenario, condition_templates: dict[str, RuntimeTemplate], + propositions: dict[str, PropositionRuntime], diagnostics: list[Diagnostic], ) -> dict[str, ConditionBinding]: condition_bindings: dict[str, ConditionBinding] = {} @@ -1051,6 +1128,10 @@ def _compile_condition_bindings( ) continue address = _condition_binding_address(node_name, condition_name) + proposition_address = ( + _proposition_address(template.spec["proposition"]) if template.spec.get("proposition") else "" + ) + proposition_dependencies = (proposition_address,) if proposition_address in propositions else () result_contract, execution_contract = _evaluation_contracts("condition-binding") condition_bindings[address] = ConditionBinding( address=address, @@ -1060,7 +1141,9 @@ def _compile_condition_bindings( condition_name=condition_name, template_address=template.address, role_name=role_name, - refresh_dependencies=(node_addr,), + proposition_address=proposition_address, + ordering_dependencies=proposition_dependencies, + refresh_dependencies=_dedupe([node_addr, *proposition_dependencies]), spec={"binding": {"node": node_name, "role": role_name}, "template": template.spec}, result_contract=result_contract, execution_contract=execution_contract, @@ -1468,8 +1551,8 @@ def _compile_participant_behaviors( scenario, agent.initial_knowledge, ) - starting_condition_refs = tuple(agent.starting_conditions) - starting_condition_addresses = _condition_addresses_for_refs(scenario, list(agent.starting_conditions)) + starting_assertion_refs = tuple(agent.starting_assertions) + starting_assertion_addresses = tuple(_assertion_address(ref) for ref in agent.starting_assertions) authority_anchor_refs = tuple(agent.authority_anchors) authority_anchor_addresses = _runtime_addresses_for_refs( list(agent.authority_anchors), @@ -1486,7 +1569,7 @@ def _compile_participant_behaviors( *observation_addresses, *starting_account_addresses, *initial_knowledge_addresses, - *starting_condition_addresses, + *starting_assertion_addresses, *authority_anchor_addresses, *operating_scope_addresses, ] @@ -1499,8 +1582,8 @@ def _compile_participant_behaviors( starting_account_refs=starting_account_refs, starting_account_addresses=starting_account_addresses, initial_knowledge_addresses=initial_knowledge_addresses, - starting_condition_refs=starting_condition_refs, - starting_condition_addresses=starting_condition_addresses, + starting_assertion_refs=starting_assertion_refs, + starting_assertion_addresses=starting_assertion_addresses, authority_anchor_refs=authority_anchor_refs, authority_anchor_addresses=authority_anchor_addresses, operating_scope_refs=operating_scope_refs, @@ -1627,7 +1710,7 @@ def _compile_behavior_specifications( def _compile_events( scenario: InstantiatedScenario, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], injects: dict[str, InjectRuntime], inject_bindings: dict[str, InjectBinding], diagnostics: list[Diagnostic], @@ -1635,16 +1718,16 @@ def _compile_events( events: dict[str, EventRuntime] = {} for name, event in scenario.events.items(): event_address = _event_address(name) - condition_names = list(event.conditions) + assertion_names = list(event.assertions) inject_names = list(event.injects) - condition_addresses, condition_diagnostics = _resolve_binding_refs( - condition_bindings, - ref_names=condition_names, + assertion_addresses, assertion_diagnostics = _resolve_named_refs( + ref_names=assertion_names, + available_names={assertion.name for assertion in assertions.values()}, + address_builder=_assertion_address, owner_address=event_address, domain="orchestration", - code_prefix="orchestration.condition-ref", - binding_attr="condition_name", - binding_label="condition", + code_prefix="orchestration.assertion-ref", + resource_label="assertion", ) inject_addresses, inject_diagnostics = _resolve_resource_refs( injects, @@ -1654,7 +1737,7 @@ def _compile_events( code_prefix="orchestration.inject-ref", resource_label="inject", ) - diagnostics.extend(condition_diagnostics) + diagnostics.extend(assertion_diagnostics) diagnostics.extend(inject_diagnostics) inject_binding_ordering_dependencies = [ address for address, binding in inject_bindings.items() if binding.inject_name in inject_names @@ -1662,13 +1745,13 @@ def _compile_events( events[event_address] = EventRuntime( address=event_address, name=name, - condition_names=tuple(condition_names), - condition_addresses=condition_addresses, + assertion_names=tuple(assertion_names), + assertion_addresses=assertion_addresses, inject_names=tuple(inject_names), inject_addresses=inject_addresses, ordering_dependencies=_dedupe([*inject_addresses, *inject_binding_ordering_dependencies]), refresh_dependencies=_dedupe( - [*condition_addresses, *inject_addresses, *inject_binding_ordering_dependencies] + [*assertion_addresses, *inject_addresses, *inject_binding_ordering_dependencies] ), spec=_dump(event), ) @@ -1732,23 +1815,22 @@ def _compile_stories( def _objective_success_addresses( - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], objective: Any, objective_address: str, diagnostics: list[Diagnostic], ) -> list[str]: - # Per ADR-073 objective success references observable ``conditions`` only. - condition_addresses, condition_diagnostics = _resolve_binding_refs( - condition_bindings, - ref_names=list(objective.success.conditions), + assertion_addresses, assertion_diagnostics = _resolve_named_refs( + ref_names=list(objective.success.assertions), + available_names={assertion.name for assertion in assertions.values()}, + address_builder=_assertion_address, owner_address=objective_address, domain="evaluation", - code_prefix="evaluation.condition-ref", - binding_attr="condition_name", - binding_label="condition", + code_prefix="evaluation.assertion-ref", + resource_label="assertion", ) - diagnostics.extend(condition_diagnostics) - return list(condition_addresses) + diagnostics.extend(assertion_diagnostics) + return list(assertion_addresses) def _objective_dependency_addresses( @@ -1833,14 +1915,14 @@ def _compile_objective_window( def _compile_objectives( scenario: InstantiatedScenario, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], diagnostics: list[Diagnostic], ) -> dict[str, ObjectiveRuntime]: objectives: dict[str, ObjectiveRuntime] = {} for name, objective in scenario.objectives.items(): objective_address = _objective_address(name) success_addresses = _objective_success_addresses( - condition_bindings, + assertions, objective, objective_address, diagnostics, @@ -1886,18 +1968,18 @@ def _compile_workflow_predicate( predicate_source: Any, *, scenario: InstantiatedScenario, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], predicate_address: str, diagnostics: list[Diagnostic], ) -> _WorkflowPredicateCompilation: - condition_addresses, workflow_diagnostics = _resolve_binding_refs( - condition_bindings, - ref_names=list(predicate_source.conditions), + assertion_addresses, workflow_diagnostics = _resolve_named_refs( + ref_names=list(predicate_source.assertions), + available_names={assertion.name for assertion in assertions.values()}, + address_builder=_assertion_address, owner_address=predicate_address, domain="orchestration", - code_prefix="orchestration.condition-ref", - binding_attr="condition_name", - binding_label="condition", + code_prefix="orchestration.assertion-ref", + resource_label="assertion", ) objective_addresses, objective_diagnostics = _resolve_named_refs( ref_names=list(predicate_source.objectives), @@ -1925,17 +2007,17 @@ def _compile_workflow_predicate( ) predicate_addresses = _dedupe( [ - *condition_addresses, + *assertion_addresses, *objective_addresses, ] ) return _WorkflowPredicateCompilation( predicate=WorkflowPredicateRuntime( - condition_addresses=condition_addresses, + assertion_addresses=assertion_addresses, objective_addresses=tuple(objective_addresses), step_state_predicates=step_state_predicates, ), - condition_addresses=condition_addresses, + assertion_addresses=assertion_addresses, predicate_addresses=predicate_addresses, objective_addresses=tuple(objective_addresses), step_state_predicates=step_state_predicates, @@ -2039,7 +2121,7 @@ def _apply_workflow_predicate_compilation( compilation: _WorkflowPredicateCompilation, ) -> None: state.referenced_objectives.extend(compilation.objective_addresses) - state.step_condition_addresses[step_name] = compilation.condition_addresses + state.step_assertion_addresses[step_name] = compilation.assertion_addresses state.step_predicate_addresses[step_name] = compilation.predicate_addresses _apply_step_state_predicate_features(state, compilation.step_state_predicates) @@ -2061,29 +2143,29 @@ def _compile_switch_cases( step_name: str, step: Any, state: _WorkflowCompilationState, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], diagnostics: list[Diagnostic], ) -> tuple[WorkflowSwitchCaseRuntime, ...]: compiled_cases: list[WorkflowSwitchCaseRuntime] = [] - switch_condition_addresses: list[str] = [] + switch_assertion_addresses: list[str] = [] switch_predicate_addresses: list[str] = [] for case_index, case in enumerate(step.cases): compilation = _compile_workflow_predicate( case.when, scenario=scenario, - condition_bindings=condition_bindings, + assertions=assertions, predicate_address=_address(workflow_address, "step", step_name, "case", str(case_index)), diagnostics=diagnostics, ) state.referenced_objectives.extend(compilation.objective_addresses) - switch_condition_addresses.extend(compilation.condition_addresses) + switch_assertion_addresses.extend(compilation.assertion_addresses) switch_predicate_addresses.extend(compilation.predicate_addresses) _apply_step_state_predicate_features(state, compilation.step_state_predicates) compiled_cases.append( WorkflowSwitchCaseRuntime(case_index=case_index, predicate=compilation.predicate, next_step=case.next_step) ) - if switch_condition_addresses: - state.step_condition_addresses[step_name] = _dedupe(switch_condition_addresses) + if switch_assertion_addresses: + state.step_assertion_addresses[step_name] = _dedupe(switch_assertion_addresses) if switch_predicate_addresses: state.step_predicate_addresses[step_name] = _dedupe(switch_predicate_addresses) return tuple(compiled_cases) @@ -2096,14 +2178,14 @@ def _compile_workflow_step_predicates( step_name: str, step: Any, state: _WorkflowCompilationState, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], diagnostics: list[Diagnostic], ) -> tuple[WorkflowPredicateRuntime | None, tuple[WorkflowSwitchCaseRuntime, ...]]: if step.when is not None: compilation = _compile_workflow_predicate( step.when, scenario=scenario, - condition_bindings=condition_bindings, + assertions=assertions, predicate_address=_address(workflow_address, "step", step_name), diagnostics=diagnostics, ) @@ -2117,7 +2199,7 @@ def _compile_workflow_step_predicates( step_name=step_name, step=step, state=state, - condition_bindings=condition_bindings, + assertions=assertions, diagnostics=diagnostics, ) @@ -2130,7 +2212,7 @@ def _compile_workflow_step( step_name: str, step: Any, state: _WorkflowCompilationState, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], diagnostics: list[Diagnostic], ) -> None: edges, type_features = _workflow_step_edges_and_features(step) @@ -2157,7 +2239,7 @@ def _compile_workflow_step( step_name=step_name, step=step, state=state, - condition_bindings=condition_bindings, + assertions=assertions, diagnostics=diagnostics, ) state.required_features.extend(_workflow_cross_cutting_features(step, workflow)) @@ -2262,7 +2344,7 @@ def _compile_workflow_runtime( *, name: str, workflow: Any, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], diagnostics: list[Diagnostic], ) -> WorkflowRuntime: workflow_address = _workflow_address(name) @@ -2275,7 +2357,7 @@ def _compile_workflow_runtime( step_name=step_name, step=step, state=state, - condition_bindings=condition_bindings, + assertions=assertions, diagnostics=diagnostics, ) objective_addresses = _dedupe(state.referenced_objectives) @@ -2289,7 +2371,7 @@ def _compile_workflow_runtime( control_steps=state.control_steps, control_edges=state.control_edges, join_owners=state.join_owners, - step_condition_addresses=state.step_condition_addresses, + step_assertion_addresses=state.step_assertion_addresses, step_predicate_addresses=state.step_predicate_addresses, required_features=_dedupe_by_value(state.required_features), required_state_predicate_features=_dedupe_by_value(state.required_state_predicate_features), @@ -2305,7 +2387,7 @@ def _compile_workflow_runtime( def _compile_workflows( scenario: InstantiatedScenario, - condition_bindings: dict[str, ConditionBinding], + assertions: dict[str, AssertionRuntime], diagnostics: list[Diagnostic], ) -> dict[str, WorkflowRuntime]: return { @@ -2313,43 +2395,23 @@ def _compile_workflows( scenario, name=name, workflow=workflow, - condition_bindings=condition_bindings, + assertions=assertions, diagnostics=diagnostics, ) for name, workflow in scenario.workflows.items() } -def _node_variable_refs_by_address( - scenario: InstantiatedScenario, - node_variable_refs: dict[str, dict[str, str | None]], -) -> dict[str, dict[str, str | None]]: - refs_by_address: dict[str, dict[str, str | None]] = {} - for node_name, refs in node_variable_refs.items(): - if not refs.get("os") and not refs.get("count"): - continue - scenario_node = scenario.nodes.get(node_name) - if scenario_node is None: - continue - address = _network_address(node_name) if scenario_node.type == NodeType.SWITCH else _node_address(node_name) - refs_by_address[address] = refs - return refs_by_address - - def _realization_requirement_address(scenario: InstantiatedScenario, field_path: str) -> str: - """Resolve the compiled resource address for a realization-concern path. - - Falls back to the field path (an equivalent field identifier) when the - concern is not tied to a single compiled provisioning resource. - """ + """Resolve the compiled resource address for a realization-concern path.""" - parts = field_path.split(".") - head, name = parts[0], parts[1] - if head == "nodes": - node = scenario.nodes.get(name) - is_switch = node is not None and node.type == NodeType.SWITCH - return _network_address(name) if is_switch else _node_address(name) - return _content_address(name) if head == "content" else field_path + for name, node in scenario.nodes.items(): + if field_path in {f"nodes.{name}.os", f"nodes.{name}.type"}: + return _network_address(name) if node.type == NodeType.SWITCH else _node_address(name) + for name in scenario.content: + if field_path == f"content.{name}.type": + return _content_address(name) + raise ValueError("realization concern must resolve to one compiled resource address") def _compile_realization_requirements( @@ -2366,7 +2428,10 @@ def _compile_realization_requirements( for field_path, record in scenario.explicitness.items(): if record.classification is ExplicitnessClass.OPEN: continue - concern_kind = resolve_realization_concern(field_path) + concern_kind = resolve_realization_concern( + field_path, + declaration_names={"nodes": scenario.nodes, "content": scenario.content}, + ) if concern_kind is None: continue requirements.append( @@ -2382,7 +2447,7 @@ def _compile_realization_requirements( def compile_scenario_runtime_model( - scenario: Scenario | InstantiatedScenario, + scenario: Scenario | ExpandedScenario | InstantiatedScenario, *, parameters: Mapping[str, object] | None = None, profile: str | None = None, @@ -2397,12 +2462,15 @@ def compile_scenario_runtime_model( return compile_runtime_model(concrete_scenario) -def compile_runtime_model(scenario: Scenario | InstantiatedScenario) -> RuntimeModel: +def compile_runtime_model(scenario: Scenario | ExpandedScenario | InstantiatedScenario) -> RuntimeModel: """Compile an SDL scenario into bound runtime objects.""" - if not isinstance(scenario, InstantiatedScenario): - scenario = instantiate_scenario(scenario, validate_semantics=False) - node_variable_refs = dict(scenario.node_variable_refs) + scenario = ( + admit_instantiated_scenario(scenario) + if isinstance(scenario, InstantiatedScenario) + else instantiate_scenario(scenario) + ) + build_declaration_index(scenario) diagnostics: list[Diagnostic] = [] ( @@ -2411,11 +2479,18 @@ def compile_runtime_model(scenario: Scenario | InstantiatedScenario) -> RuntimeM inject_templates, vulnerability_templates, ) = _compile_templates(scenario) - entity_specs, agent_specs, relationship_specs, variable_specs = _metadata_specs(scenario) + entity_specs, agent_specs, relationship_specs = _metadata_specs(scenario) networks, node_deployments = _compile_node_runtimes(scenario, diagnostics) feature_bindings = _compile_feature_bindings(scenario, feature_templates, diagnostics) - condition_bindings = _compile_condition_bindings(scenario, condition_templates, diagnostics) + propositions = _compile_propositions(scenario) + assertions = _compile_assertions(scenario) + condition_bindings = _compile_condition_bindings( + scenario, + condition_templates, + propositions, + diagnostics, + ) injects = _compile_inject_runtimes(inject_templates) inject_bindings = _compile_inject_bindings(scenario, inject_templates, diagnostics) content_placements = _compile_content_placements(scenario, diagnostics) @@ -2425,11 +2500,11 @@ def compile_runtime_model(scenario: Scenario | InstantiatedScenario) -> RuntimeM outcome_interpretation_rules = _compile_outcome_interpretation_rules(scenario) participant_behaviors = _compile_participant_behaviors(scenario, diagnostics) behavior_specifications = _compile_behavior_specifications(scenario, diagnostics) - events = _compile_events(scenario, condition_bindings, injects, inject_bindings, diagnostics) + events = _compile_events(scenario, assertions, injects, inject_bindings, diagnostics) scripts = _compile_scripts(scenario, diagnostics) stories = _compile_stories(scenario, diagnostics) - objectives = _compile_objectives(scenario, condition_bindings, diagnostics) - workflows = _compile_workflows(scenario, condition_bindings, diagnostics) + objectives = _compile_objectives(scenario, assertions, diagnostics) + workflows = _compile_workflows(scenario, assertions, diagnostics) return RuntimeModel( scenario_name=scenario.name, @@ -2440,11 +2515,12 @@ def compile_runtime_model(scenario: Scenario | InstantiatedScenario) -> RuntimeM entity_specs=entity_specs, agent_specs=agent_specs, relationship_specs=relationship_specs, - variable_specs=variable_specs, - node_variable_refs=_node_variable_refs_by_address(scenario, node_variable_refs), + capability_constraints=_compile_capability_constraints(scenario), networks=networks, node_deployments=node_deployments, feature_bindings=feature_bindings, + propositions=propositions, + assertions=assertions, condition_bindings=condition_bindings, injects=injects, inject_bindings=inject_bindings, @@ -2462,4 +2538,5 @@ def compile_runtime_model(scenario: Scenario | InstantiatedScenario) -> RuntimeM objectives=objectives, diagnostics=diagnostics, realization_requirements=_compile_realization_requirements(scenario), + realization_instance=scenario, ) diff --git a/implementations/python/packages/aces_processor/models.py b/implementations/python/packages/aces_processor/models.py index cff1962f0..8ec313e2e 100644 --- a/implementations/python/packages/aces_processor/models.py +++ b/implementations/python/packages/aces_processor/models.py @@ -20,6 +20,7 @@ WorkflowFeature, WorkflowStatePredicateFeature, ) +from aces_contracts.addressing import require_compiled_address from aces_contracts.diagnostics import Diagnostic as Diagnostic from aces_contracts.diagnostics import Severity as Severity from aces_contracts.evaluation import ( @@ -235,6 +236,7 @@ ParticipantTemporalState, ParticipantTimeDomain, ) +from aces_sdl.scenario import InstantiatedScenario from aces_sdl.semantics.workflow import ( WorkflowStepSemanticContract, ) @@ -299,6 +301,25 @@ class FeatureBinding(ResolvedResource): role_name: str = "" +@dataclass(frozen=True) +class PropositionRuntime(ResolvedResource): + """Compiled backend-neutral proposition with resolved finite subjects.""" + + subject_addresses: tuple[str, ...] = () + predicate_kind: str = "" + evaluation_basis: str = "" + evidence_requirement_refs: tuple[str, ...] = () + + +@dataclass(frozen=True) +class AssertionRuntime(ResolvedResource): + """Compiled assertion use over one proposition.""" + + proposition_address: str = "" + role: str = "" + polarity: str = "" + + @dataclass(frozen=True) class ConditionBinding(ResolvedResource): """Condition template bound to a specific node role.""" @@ -308,6 +329,7 @@ class ConditionBinding(ResolvedResource): condition_name: str = "" template_address: str = "" role_name: str = "" + proposition_address: str = "" result_contract: "EvaluationResultContract" = field( default_factory=lambda: EvaluationResultContract(resource_type="condition-binding") ) @@ -579,8 +601,8 @@ class ParticipantBehaviorRuntime(ResolvedResource): starting_account_refs: tuple[str, ...] = () starting_account_addresses: tuple[str, ...] = () initial_knowledge_addresses: tuple[str, ...] = () - starting_condition_refs: tuple[str, ...] = () - starting_condition_addresses: tuple[str, ...] = () + starting_assertion_refs: tuple[str, ...] = () + starting_assertion_addresses: tuple[str, ...] = () authority_anchor_refs: tuple[str, ...] = () authority_anchor_addresses: tuple[str, ...] = () operating_scope_refs: tuple[str, ...] = () @@ -618,8 +640,8 @@ class ParticipantBehaviorSpecificationRuntime(ResolvedResource): class EventRuntime(ResolvedResource): """Resolved orchestration event.""" - condition_names: tuple[str, ...] = () - condition_addresses: tuple[str, ...] = () + assertion_names: tuple[str, ...] = () + assertion_addresses: tuple[str, ...] = () inject_names: tuple[str, ...] = () inject_addresses: tuple[str, ...] = () @@ -664,7 +686,7 @@ class WorkflowStepStatePredicateRuntime: class WorkflowPredicateRuntime: """Resolved workflow predicate semantics.""" - condition_addresses: tuple[str, ...] = () + assertion_addresses: tuple[str, ...] = () objective_addresses: tuple[str, ...] = () step_state_predicates: tuple[WorkflowStepStatePredicateRuntime, ...] = () @@ -673,7 +695,7 @@ def external_addresses(self) -> tuple[str, ...]: seen: set[str] = set() ordered: list[str] = [] for address in ( - *self.condition_addresses, + *self.assertion_addresses, *self.objective_addresses, ): if address in seen: @@ -728,7 +750,7 @@ class WorkflowRuntime(ResolvedResource): control_steps: dict[str, WorkflowStepRuntime] = field(default_factory=dict) control_edges: dict[str, tuple[str, ...]] = field(default_factory=dict) join_owners: dict[str, str] = field(default_factory=dict) - step_condition_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) + step_assertion_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) step_predicate_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) required_features: tuple[WorkflowFeature, ...] = () required_state_predicate_features: tuple[WorkflowStatePredicateFeature, ...] = () @@ -4181,6 +4203,25 @@ class ObjectiveRuntime(ResolvedResource): ) +@dataclass(frozen=True) +class CompiledCapabilityConstraint: + """One finite SDL capability domain lowered onto a compiled resource.""" + + address: str + concern: str + parameter: tuple[str, ...] + allowed_values: tuple[str | int | float | bool, ...] + + def __post_init__(self) -> None: + require_compiled_address(self.address, field_name="capability constraint address") + if self.concern not in {"nodes.os", "infrastructure.count"}: + raise ValueError("compiled capability constraint has an unsupported concern") + if not self.parameter or any(not segment for segment in self.parameter): + raise ValueError("compiled capability constraint requires a parameter identity") + if not self.allowed_values: + raise ValueError("compiled capability constraint requires a non-empty domain") + + @dataclass(frozen=True) class RuntimeModel: """Compiled SDL runtime model. @@ -4197,19 +4238,15 @@ class RuntimeModel: entity_specs: dict[str, dict[str, Any]] = field(default_factory=dict) agent_specs: dict[str, dict[str, Any]] = field(default_factory=dict) relationship_specs: dict[str, dict[str, Any]] = field(default_factory=dict) - variable_specs: dict[str, dict[str, Any]] = field(default_factory=dict) - # Pre-instantiation `${name}` refs on `nodes.os` and `infrastructure.count`, - # keyed by the network/node resource address. Lets the planner reach a - # variable's `allowed_values` even after `compile_runtime_model` substitutes - # the resolved values onto the corresponding runtime resources. Kept on the - # model rather than on the resources themselves so the provenance does not - # leak into the backend-facing `resource_payload()` envelope. Inner dict - # carries `"os"` and `"count"` keys; missing or `None` values mean the - # field was authored as a concrete literal rather than a variable ref. - node_variable_refs: dict[str, dict[str, str | None]] = field(default_factory=dict) + # Typed compiler metadata for finite pre-instantiation domains. It is + # consumed by planner capability checks and never enters backend resource + # payloads. + capability_constraints: tuple[CompiledCapabilityConstraint, ...] = () networks: dict[str, NetworkRuntime] = field(default_factory=dict) node_deployments: dict[str, NodeRuntime] = field(default_factory=dict) feature_bindings: dict[str, FeatureBinding] = field(default_factory=dict) + propositions: dict[str, PropositionRuntime] = field(default_factory=dict) + assertions: dict[str, AssertionRuntime] = field(default_factory=dict) condition_bindings: dict[str, ConditionBinding] = field(default_factory=dict) injects: dict[str, InjectRuntime] = field(default_factory=dict) inject_bindings: dict[str, InjectBinding] = field(default_factory=dict) @@ -4227,10 +4264,54 @@ class RuntimeModel: objectives: dict[str, ObjectiveRuntime] = field(default_factory=dict) diagnostics: list[Diagnostic] = field(default_factory=list) # SEM-218 typed compiler emission: each authored realization concern with - # its preserved explicitness class. Model-side metadata (like - # `node_variable_refs`); it never enters the backend-facing + # its preserved explicitness class. Model-side metadata; it never enters the backend-facing # `resource_payload()` envelope. Consumed by the planner realization gate. realization_requirements: tuple[CompiledRealizationRequirement, ...] = () + realization_instance: InstantiatedScenario | None = None + + def __post_init__(self) -> None: + owners: dict[str, str] = {} + address_map_fields = ( + "networks", + "node_deployments", + "feature_bindings", + "propositions", + "assertions", + "condition_bindings", + "injects", + "inject_bindings", + "content_placements", + "account_placements", + "action_contracts", + "observation_boundaries", + "outcome_interpretation_rules", + "participant_behaviors", + "behavior_specifications", + "events", + "scripts", + "stories", + "workflows", + "objectives", + ) + for field_name in address_map_fields: + value = getattr(self, field_name) + for map_key, item in value.items(): + address = getattr(item, "address", None) + if not isinstance(address, str): + raise TypeError(f"RuntimeModel {field_name} entries must carry an address") + require_compiled_address(address) + require_compiled_address(map_key, field_name="runtime model map key") + if map_key != address: + raise ValueError(f"RuntimeModel {field_name} map key must equal embedded address") + previous_owner = owners.get(address) + if previous_owner is not None and previous_owner != field_name: + raise ValueError( + f"RuntimeModel duplicate compiled address across {previous_owner} and {field_name}" + ) + owners[address] = field_name + capability_keys = [(constraint.address, constraint.concern) for constraint in self.capability_constraints] + if len(capability_keys) != len(set(capability_keys)): + raise ValueError("RuntimeModel capability constraints must address unique fields") @dataclass(frozen=True) diff --git a/implementations/python/packages/aces_processor/planner.py b/implementations/python/packages/aces_processor/planner.py index 11b3f7f7d..1e0e99726 100644 --- a/implementations/python/packages/aces_processor/planner.py +++ b/implementations/python/packages/aces_processor/planner.py @@ -4,10 +4,12 @@ from aces_backend_protocols.capabilities import BackendManifest from aces_sdl.infrastructure import MINIMUM_NODE_COUNT from aces_sdl.nodes import OSFamily +from aces_sdl.realization_envelope import member from aces_sdl.value_parsing import extract_variable_name, parse_enum_or_var, parse_int_or_var from .models import ( ChangeAction, + CompiledCapabilityConstraint, Diagnostic, EvaluationOp, EvaluationPlan, @@ -20,7 +22,6 @@ RuntimeDomain, RuntimeModel, RuntimeSnapshot, - Severity, SnapshotEntry, resource_payload, ) @@ -36,6 +37,9 @@ __all__ = ["plan", "realization_disclosure", "snapshot_delete_order"] +_COUNT_DOMAIN_INVALID = "provisioner.count-variable-domain-invalid" +_OS_FAMILY_DOMAIN_INVALID = "provisioner.os-family-variable-domain-invalid" + def _planned_resource(address: str, domain: RuntimeDomain, resource_type: str, resource) -> PlannedResource: return PlannedResource( @@ -135,6 +139,20 @@ def _collect_resources(model: RuntimeModel) -> dict[str, PlannedResource]: "condition-binding", resource, ) + for address, resource in model.propositions.items(): + resources[address] = _planned_resource( + address, + RuntimeDomain.EVALUATION, + "proposition", + resource, + ) + for address, resource in model.assertions.items(): + resources[address] = _planned_resource( + address, + RuntimeDomain.EVALUATION, + "assertion", + resource, + ) for address, resource in model.objectives.items(): resources[address] = _planned_resource( address, @@ -192,94 +210,58 @@ def _entry_matches_resource(entry: SnapshotEntry, resource: PlannedResource) -> ) -def _variable_ref( +def _capability_constraint( model: RuntimeModel, - value: object, -) -> tuple[str | None, dict[str, object] | None, bool]: - if not isinstance(value, str): - return None, None, False - variable_name = extract_variable_name(value) - if variable_name is None: - return None, None, False - return _resolve_variable_ref(model, variable_name) - - -def _resolve_variable_ref( - model: RuntimeModel, - variable_name: str, -) -> tuple[str | None, dict[str, object] | None, bool]: - """Look up a variable's spec by name and report whether it is declared.""" - - spec = model.variable_specs.get(variable_name) - if isinstance(spec, dict): - return variable_name, spec, True - return variable_name, None, False - - -def _has_allowed_values(variable_spec: dict[str, object] | None) -> bool: - """Return whether `variable_spec` declares a non-empty `allowed_values`.""" - - if not isinstance(variable_spec, dict): - return False - allowed_values = variable_spec.get("allowed_values") - return isinstance(allowed_values, list) and bool(allowed_values) - - -def _error_diagnostic(code: str, address: str, message: str) -> Diagnostic: - return Diagnostic( - code=code, - domain="provisioning", - address=address, - message=message, - ) - - -def _variable_default_suffix(variable_name: str, variable_spec: dict[str, object] | None) -> str: - if variable_spec is None or variable_spec.get("default") is None: - return f" Variable '{variable_name}' has no finite pre-instantiation domain." - return ( - f" Variable '{variable_name}' has default {variable_spec['default']!r}, " - "but defaults are informative only before instantiation." + *, + address: str, + concern: str, +) -> CompiledCapabilityConstraint | None: + return next( + ( + constraint + for constraint in model.capability_constraints + if constraint.address == address and constraint.concern == concern + ), + None, ) -def _warning_diagnostic(code: str, address: str, message: str) -> Diagnostic: +def _error_diagnostic(code: str, address: str, message: str) -> Diagnostic: return Diagnostic( code=code, domain="provisioning", address=address, message=message, - severity=Severity.WARNING, ) def _validate_os_allowed_values( variable_name: str, - variable_spec: dict[str, object], + allowed_values: tuple[str | int | float | bool, ...], *, address: str, ) -> tuple[tuple[str, ...] | None, Diagnostic | None]: - allowed_values = variable_spec.get("allowed_values") - if not isinstance(allowed_values, list) or not allowed_values: - return None, None - validated_values: list[str] = [] for raw_value in allowed_values: try: parsed = parse_enum_or_var(raw_value, OSFamily, field_name="os") except ValueError as exc: return None, _error_diagnostic( - "provisioner.os-family-variable-domain-invalid", + _OS_FAMILY_DOMAIN_INVALID, address, (f"Variable '{variable_name}' allowed_values contain value {raw_value!r} invalid for nodes.os: {exc}."), ) if extract_variable_name(parsed) is not None: - return None, None + return None, _error_diagnostic( + _OS_FAMILY_DOMAIN_INVALID, + address, + f"Variable '{variable_name}' has a non-concrete nodes.os domain.", + ) if isinstance(parsed, OSFamily): validated_values.append(parsed.value) continue return None, _error_diagnostic( - "provisioner.os-family-variable-domain-invalid", + _OS_FAMILY_DOMAIN_INVALID, address, ( "Variable " @@ -299,22 +281,20 @@ def _validate_node_os_family( if not node.os_family: return [] - # Prefer the pre-instantiation variable ref captured by the compiler over - # any `${...}` placeholder still on the resolved value, but only when the - # variable carries an explicit `allowed_values` domain. Without - # allowed_values the documented planner contract is "trust the - # instantiated default"; widening the captured-ref path to those variables - # would emit a spurious validation-deferred warning even though the - # default is a concrete supported value. - variable_name, variable_spec, is_declared = None, None, False - captured_ref = model.node_variable_refs.get(node.address, {}).get("os") - if captured_ref: - ref_name, ref_spec, ref_declared = _resolve_variable_ref(model, captured_ref) - if _has_allowed_values(ref_spec): - variable_name, variable_spec, is_declared = ref_name, ref_spec, ref_declared - if variable_name is None: - variable_name, variable_spec, is_declared = _variable_ref(model, node.os_family) - if variable_name is None: + constraint = _capability_constraint(model, address=node.address, concern="nodes.os") + if constraint is None: + unresolved = extract_variable_name(node.os_family) + if unresolved is not None: + return [ + _error_diagnostic( + "provisioner.os-family-variable-ref-unbound", + node.address, + ( + "Provisioner capability validation cannot resolve undeclared " + f"variable '{unresolved}' referenced by nodes.os." + ), + ) + ] if node.os_family in supported_os_families: return [] return [ @@ -325,21 +305,10 @@ def _validate_node_os_family( message=f"Provisioner does not support OS family '{node.os_family}'.", ) ] - if not is_declared: - return [ - _error_diagnostic( - "provisioner.os-family-variable-ref-unbound", - node.address, - ( - "Provisioner capability validation cannot resolve undeclared " - f"variable '{variable_name}' referenced by nodes.os." - ), - ) - ] - + variable_name = ".".join(constraint.parameter) finite_domain, domain_error = _validate_os_allowed_values( variable_name, - variable_spec or {}, + constraint.allowed_values, address=node.address, ) if domain_error is not None: @@ -361,29 +330,15 @@ def _validate_node_os_family( ] return [] - return [ - _warning_diagnostic( - "provisioner.os-family-validation-deferred", - node.address, - ( - "Provisioner OS-family validation is deferred until instantiation " - f"for {node.os_family!r}." - f"{_variable_default_suffix(variable_name, variable_spec)}" - ), - ) - ] + return [] def _validate_count_allowed_values( variable_name: str, - variable_spec: dict[str, object], + allowed_values: tuple[str | int | float | bool, ...], *, address: str, ) -> tuple[int | None, Diagnostic | None]: - allowed_values = variable_spec.get("allowed_values") - if not isinstance(allowed_values, list) or not allowed_values: - return None, None - validated_values: list[int] = [] for raw_value in allowed_values: try: @@ -394,7 +349,7 @@ def _validate_count_allowed_values( ) except ValueError as exc: return None, _error_diagnostic( - "provisioner.count-variable-domain-invalid", + _COUNT_DOMAIN_INVALID, address, ( "Variable " @@ -403,12 +358,16 @@ def _validate_count_allowed_values( ), ) if extract_variable_name(parsed) is not None: - return None, None + return None, _error_diagnostic( + _COUNT_DOMAIN_INVALID, + address, + f"Variable '{variable_name}' has a non-concrete infrastructure.count domain.", + ) if isinstance(parsed, int): validated_values.append(parsed) continue return None, _error_diagnostic( - "provisioner.count-variable-domain-invalid", + _COUNT_DOMAIN_INVALID, address, ( "Variable " @@ -426,42 +385,30 @@ def _resource_count_upper_bound( ) -> tuple[int | None, Diagnostic | None]: count = resource.spec.get("infrastructure", {}).get("count", 1) - # Same provenance issue as `_validate_node_os_family`: `instantiate_scenario` - # has already resolved `${var}` references on infrastructure.count by the - # time the planner sees the model. The captured ref on - # `model.node_variable_refs[resource.address]["count"]` lets the capability - # check still reach the variable's `allowed_values` so that, e.g., - # `max_total_nodes=2` rejects a variable whose domain includes `3` even - # when its default is `1`. Switch (network) and node (vm) resources share - # the same dict because the planner enforces `max_total_nodes` against - # both. Variables without `allowed_values` keep the documented "trust the - # instantiated default" contract and fall through to the integer-direct - # branch below. - variable_name, variable_spec, is_declared = None, None, False - captured_ref = model.node_variable_refs.get(resource.address, {}).get("count") - if captured_ref: - ref_name, ref_spec, ref_declared = _resolve_variable_ref(model, captured_ref) - if _has_allowed_values(ref_spec): - variable_name, variable_spec, is_declared = ref_name, ref_spec, ref_declared - if variable_name is None: + constraint = _capability_constraint( + model, + address=resource.address, + concern="infrastructure.count", + ) + if constraint is None: if isinstance(count, int): return count, None - variable_name, variable_spec, is_declared = _variable_ref(model, count) - if variable_name is None: - return None, None - if not is_declared: + unresolved = extract_variable_name(count) if isinstance(count, str) else None + if unresolved is None: + return None, None return None, _error_diagnostic( "provisioner.count-variable-ref-unbound", resource.address, ( "Provisioner capability validation cannot resolve undeclared " - f"variable '{variable_name}' referenced by infrastructure.count." + f"variable '{unresolved}' referenced by infrastructure.count." ), ) + variable_name = ".".join(constraint.parameter) finite_upper_bound, domain_error = _validate_count_allowed_values( variable_name, - variable_spec or {}, + constraint.allowed_values, address=resource.address, ) if domain_error is not None: @@ -469,18 +416,7 @@ def _resource_count_upper_bound( if finite_upper_bound is not None: return finite_upper_bound, None - return ( - None, - _warning_diagnostic( - "provisioner.max-total-nodes-validation-deferred", - resource.address, - ( - "Provisioner max-total-nodes validation is deferred until " - f"instantiation for {count!r}." - f"{_variable_default_suffix(variable_name, variable_spec)}" - ), - ), - ) + return None, None def _account_features(account_spec: dict[str, object]) -> set[str]: @@ -639,20 +575,20 @@ def _validate_manifest(model: RuntimeModel, manifest: BackendManifest) -> list[D message=(f"Orchestrator does not support workflow feature '{feature.value}'."), ) ) - orchestration_uses_condition_refs = any( - event.condition_addresses for event in model.events.values() + orchestration_uses_assertion_refs = any( + event.assertion_addresses for event in model.events.values() ) or any( addresses for workflow in model.workflows.values() - for addresses in workflow.step_condition_addresses.values() + for addresses in workflow.step_assertion_addresses.values() ) - if orchestration_uses_condition_refs and not manifest.orchestrator.supports_condition_refs: + if orchestration_uses_assertion_refs and not manifest.orchestrator.supports_assertion_refs: diagnostics.append( Diagnostic( - code="orchestrator.condition-refs-unsupported", + code="orchestrator.assertion-refs-unsupported", domain="orchestration", - address="orchestration.condition-refs", - message=("Orchestrator does not support condition-gated events or workflow predicates."), + address="orchestration.assertion-refs", + message=("Orchestrator does not support assertion-gated events or workflow predicates."), ) ) required_state_predicate_features = sorted( @@ -686,6 +622,8 @@ def _validate_manifest(model: RuntimeModel, manifest: BackendManifest) -> list[D evaluation_sections = { "conditions": bool(model.condition_bindings), + "propositions": bool(model.propositions), + "assertions": bool(model.assertions), "objectives": bool(model.objectives), } if any(evaluation_sections.values()): @@ -762,6 +700,7 @@ def _build_provisioning_plan( resources: dict[str, PlannedResource], actions: dict[str, ChangeAction], deleted_entries: dict[str, SnapshotEntry], + manifest: BackendManifest, ) -> ProvisioningPlan: provisioning_resources = { address: resource for address, resource in resources.items() if resource.domain == RuntimeDomain.PROVISIONING @@ -793,7 +732,13 @@ def _build_provisioning_plan( refresh_dependencies=entry.refresh_dependencies, ) ) - return ProvisioningPlan(resources=provisioning_resources, operations=ops) + return ProvisioningPlan( + resources=provisioning_resources, + operations=ops, + realization_envelope=( + manifest.realization_envelope.identity if manifest.realization_envelope is not None else None + ), + ) def _build_orchestration_plan( @@ -893,15 +838,21 @@ def plan( snapshot = snapshot or RuntimeSnapshot() resources = _collect_resources(model) + envelope_diagnostics = ( + list(member(model.realization_instance, manifest.realization_envelope.expression).diagnostics) + if manifest.realization_envelope is not None and model.realization_instance is not None + else [] + ) diagnostics = [ *model.diagnostics, *_validate_manifest(model, manifest), *realization_support_diagnostics(model.realization_requirements, manifest), + *envelope_diagnostics, *_ordering_cycle_diagnostics(resources), ] actions, deleted_entries = _build_operations(resources, snapshot) - provisioning = _build_provisioning_plan(resources, actions, deleted_entries) + provisioning = _build_provisioning_plan(resources, actions, deleted_entries, manifest) orchestration = _build_orchestration_plan(resources, actions, deleted_entries) evaluation = _build_evaluation_plan(resources, actions, deleted_entries) diff --git a/implementations/python/packages/aces_processor/reference.py b/implementations/python/packages/aces_processor/reference.py index c89a9edc9..6b919123f 100644 --- a/implementations/python/packages/aces_processor/reference.py +++ b/implementations/python/packages/aces_processor/reference.py @@ -24,7 +24,7 @@ from aces_backend_protocols.capabilities import BackendManifest from aces_contracts.diagnostics import Diagnostic from aces_sdl.parser import parse_sdl, parse_sdl_file -from aces_sdl.scenario import InstantiatedScenario, Scenario +from aces_sdl.scenario import ExpandedScenario, InstantiatedScenario, Scenario from aces_processor.compiler import compile_scenario_runtime_model from aces_processor.manifest import ( @@ -44,7 +44,7 @@ # SDL authoring input the reference processor accepts: raw SDL text, a path to an # SDL file, or an already-parsed scenario (parameterized or instantiated). -ScenarioInput = str | Path | Scenario | InstantiatedScenario +ScenarioInput = str | Path | Scenario | ExpandedScenario | InstantiatedScenario @dataclass(frozen=True) @@ -63,10 +63,8 @@ def is_valid(self) -> bool: return not any(diag.is_error for diag in self.diagnostics) -def _resolve_scenario(scenario: ScenarioInput) -> Scenario: - # ``InstantiatedScenario`` is a ``Scenario`` subclass, so the isinstance - # check covers both parsed and instantiated inputs without re-parsing. - if isinstance(scenario, Scenario): +def _resolve_scenario(scenario: ScenarioInput) -> Scenario | ExpandedScenario | InstantiatedScenario: + if isinstance(scenario, (Scenario, ExpandedScenario, InstantiatedScenario)): return scenario if isinstance(scenario, Path): return parse_sdl_file(scenario) @@ -74,7 +72,7 @@ def _resolve_scenario(scenario: ScenarioInput) -> Scenario: return parse_sdl(scenario) raise TypeError( "scenario must be SDL text (str), a file path (Path), or a parsed " - f"Scenario/InstantiatedScenario; got {type(scenario).__name__}" + f"Scenario/ExpandedScenario/InstantiatedScenario; got {type(scenario).__name__}" ) diff --git a/implementations/python/packages/aces_processor/semantics/planner.py b/implementations/python/packages/aces_processor/semantics/planner.py index 579a4fc29..f1e6a197a 100644 --- a/implementations/python/packages/aces_processor/semantics/planner.py +++ b/implementations/python/packages/aces_processor/semantics/planner.py @@ -8,6 +8,8 @@ from enum import Enum from typing import Protocol, TypeVar +from aces_contracts.addressing import require_compiled_address + class DependencyKind(str, Enum): """Typed runtime dependency semantics.""" @@ -42,9 +44,9 @@ class SupportsDependencySemantics(Protocol): def canonical_resource_identity(address: str) -> tuple[str, ...]: - """Return the canonical identity tuple for a compiled resource address.""" + """Return an opaque, validated ordering key for a compiled address.""" - return tuple(part for part in address.split(".") if part) + return (require_compiled_address(address),) def dependency_graph( diff --git a/implementations/python/packages/aces_processor/semantics/realization.py b/implementations/python/packages/aces_processor/semantics/realization.py index 9905c78d8..d8564ecbb 100644 --- a/implementations/python/packages/aces_processor/semantics/realization.py +++ b/implementations/python/packages/aces_processor/semantics/realization.py @@ -20,9 +20,11 @@ from __future__ import annotations +from collections.abc import Iterable, Mapping from dataclasses import dataclass from aces_backend_protocols.capabilities import BackendManifest +from aces_contracts.addressing import require_compiled_address from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp from aces_contracts.runtime_state import RealizationProvenanceEntry, RuntimeSnapshot @@ -80,8 +82,8 @@ class CompiledRealizationRequirement: """A compiled realization concern carrying its SEM-218 explicitness class. - Owned by ``RuntimeModel`` as model-side metadata — like the existing - ``node_variable_refs`` provenance it never enters the backend-facing + Owned by ``RuntimeModel`` as model-side metadata; like compiled capability + constraints, it never enters the backend-facing ``resource_payload()`` envelope — and consumed directly by the planner gate. """ @@ -91,8 +93,15 @@ class CompiledRealizationRequirement: requirement_kind: str explicitness: ExplicitnessClass + def __post_init__(self) -> None: + require_compiled_address(self.address) -def resolve_realization_concern(field_path: str) -> str | None: + +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 @@ -100,11 +109,10 @@ def resolve_realization_concern(field_path: str) -> str | None: a published kind and yields ``None``. """ - parts = field_path.split(".") - if len(parts) != 3: - return None - head, _name, leaf = parts - return _CONCERN_KIND_BY_PATH.get((head, leaf)) + for (head, leaf), concern_kind in _CONCERN_KIND_BY_PATH.items(): + if any(field_path == f"{head}.{name}.{leaf}" for name in declaration_names.get(head, ())): + return concern_kind + return None def realization_support_diagnostics( diff --git a/implementations/python/packages/aces_reference_backend/drivers/oci.py b/implementations/python/packages/aces_reference_backend/drivers/oci.py index 4ec654187..8ce119be9 100644 --- a/implementations/python/packages/aces_reference_backend/drivers/oci.py +++ b/implementations/python/packages/aces_reference_backend/drivers/oci.py @@ -26,6 +26,7 @@ from collections.abc import Callable from dataclasses import dataclass +from aces_backend_protocols.naming import provider_resource_name from aces_contracts.diagnostics import Diagnostic, Severity from aces_reference_backend.driver import ( @@ -158,16 +159,18 @@ def realize( diagnostics: list[Diagnostic] = [] network_handles: list[NetworkHandle] = [] for spec in networks: - argv = [self._runtime, "network", "create", *self._label_args(), spec.name] + runtime_name = provider_resource_name(spec.address, prefix="aces") + argv = [self._runtime, "network", "create", *self._label_args(), runtime_name] ok, kind = self._run(argv) if ok: self._realized.add(spec.address) - self._names[spec.address] = spec.name + self._names[spec.address] = runtime_name network_handles.append(NetworkHandle(address=spec.address, realized=True)) else: diagnostics.append(self._failure(spec.address, kind)) container_handles: list[ContainerHandle] = [] for spec in containers: + runtime_name = provider_resource_name(spec.address, prefix="aces") image = self._image_policy.image_for(spec.image_ref) if not self._image_policy.permits(image): diagnostics.append(self._image_rejected(spec.address)) @@ -179,7 +182,7 @@ def realize( "--rm", *self._label_args(), "--name", - spec.name, + runtime_name, # Attach the container to every requested network (created above # in this same realize() call) so planned topology is honored, # not silently left on the runtime default network. spec.networks @@ -194,7 +197,7 @@ def realize( ok, kind = self._run(argv) if ok: self._realized.add(spec.address) - self._names[spec.address] = spec.name + self._names[spec.address] = runtime_name container_handles.append(ContainerHandle(address=spec.address, realized=True)) else: diagnostics.append(self._failure(spec.address, kind)) @@ -228,9 +231,7 @@ def _rollback( self.destroy(networks=realized_networks, containers=realized_containers) def _name_for(self, address: str) -> str: - # Remove by the name realize() used; fall back to the address's last - # segment for resources this driver did not create (best effort). - return self._names.get(address, address.rsplit(".", 1)[-1]) + return self._names.get(address, provider_resource_name(address, prefix="aces")) def destroy( self, diff --git a/implementations/python/packages/aces_reference_backend/evaluator.py b/implementations/python/packages/aces_reference_backend/evaluator.py index f0342a6a4..a2b04c6e7 100644 --- a/implementations/python/packages/aces_reference_backend/evaluator.py +++ b/implementations/python/packages/aces_reference_backend/evaluator.py @@ -42,8 +42,12 @@ def start(self, plan: EvaluationPlan, snapshot: RuntimeSnapshot) -> ApplyResult: payload=op.payload, ordering_dependencies=op.ordering_dependencies, refresh_dependencies=op.refresh_dependencies, - status="evaluating", + status="admitted" if op.resource_type in {"proposition", "assertion"} else "evaluating", ) + if op.resource_type in {"proposition", "assertion"}: + if op.action != ChangeAction.UNCHANGED: + changed_addresses.append(op.address) + continue result_payload = self._result_payload(op, now) results[op.address] = result_payload history[op.address] = self._history_events(result_payload, now) diff --git a/implementations/python/packages/aces_reference_backend/manifest.py b/implementations/python/packages/aces_reference_backend/manifest.py index 2ddf3b76e..0c41a5ab9 100644 --- a/implementations/python/packages/aces_reference_backend/manifest.py +++ b/implementations/python/packages/aces_reference_backend/manifest.py @@ -34,6 +34,9 @@ from aces_contracts.vocabulary import RealizationSupportMode 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" +) _PARTICIPANT_ROLES = frozenset(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_ROLE_SCOPE]) _PARTICIPANT_BEHAVIOR_FEATURES = frozenset( @@ -129,7 +132,7 @@ def _capabilities() -> BackendCapabilitySet: name="reference-emulation-orchestrator", supported_sections=frozenset({"injects", "events", "scripts", "stories", "workflows"}), supports_workflows=True, - supports_condition_refs=True, + supports_assertion_refs=True, supports_inject_bindings=True, supported_workflow_features=frozenset( { @@ -153,9 +156,15 @@ def _capabilities() -> BackendCapabilitySet: ), evaluator=EvaluatorCapabilities( name="reference-emulation-evaluator", - supported_sections=frozenset({"conditions", "objectives"}), + supported_sections=frozenset({"conditions", "propositions", "assertions", "objectives"}), supports_scoring=True, supports_objectives=True, + supported_predicate_families=frozenset({"presence", "boolean", "string", "number"}), + supported_quantifiers=frozenset({"all", "any", "at_least"}), + supported_truth_outcomes=frozenset({"true", "false", "unknown", "unsupported"}), + supported_evidence_channels=frozenset({"log", "api_response", "file_artifact"}), + supported_time_domains=frozenset({"scenario_time"}), + preserves_binding_provenance=True, ), participant_runtime=ParticipantRuntimeCapabilities( name="reference-emulation-participant-runtime", @@ -205,7 +214,7 @@ def create_reference_backend_manifest(**config) -> BackendManifest: return BackendManifest( name=REFERENCE_BACKEND_NAME, version=_current_backend_version(), - supported_contract_versions=frozenset(BACKEND_SUPPORTED_CONTRACT_IDS), + supported_contract_versions=REFERENCE_BACKEND_SUPPORTED_CONTRACT_VERSIONS, compatible_processors=frozenset({"aces-reference-processor"}), concept_bindings=_concept_bindings(), realization_support=_realization_support(), diff --git a/implementations/python/packages/aces_reference_backend/realization.py b/implementations/python/packages/aces_reference_backend/realization.py index 3949e819e..c2c28d1ab 100644 --- a/implementations/python/packages/aces_reference_backend/realization.py +++ b/implementations/python/packages/aces_reference_backend/realization.py @@ -13,6 +13,7 @@ from collections.abc import Mapping from dataclasses import dataclass +from aces_backend_protocols.naming import provider_resource_name from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.planning import PlannedResource, ProvisioningPlan, RuntimeDomain @@ -96,7 +97,7 @@ def _network_address_lookup(networks: list[NetworkSpec]) -> dict[str, str]: lookup: dict[str, str] = {} for spec in networks: - for key in (spec.address, spec.name, spec.address.rsplit(".", 1)[-1]): + for key in (spec.address, spec.name): if key: lookup[key] = spec.address return lookup @@ -106,7 +107,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 resource.address.rsplit(".", 1)[-1] + return provider_resource_name(resource.address, prefix="aces") def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: diff --git a/implementations/python/packages/aces_runtime/backend_calls.py b/implementations/python/packages/aces_runtime/backend_calls.py index dab874216..57529546d 100644 --- a/implementations/python/packages/aces_runtime/backend_calls.py +++ b/implementations/python/packages/aces_runtime/backend_calls.py @@ -5,6 +5,7 @@ from collections.abc import Callable, Iterable from copy import deepcopy +from aces_contracts.addressing import require_compiled_address from aces_contracts.diagnostics import Diagnostic from aces_contracts.planning import ProvisioningPlan from aces_contracts.runtime_state import ApplyResult, RealizationProvenanceEntry, RuntimeSnapshot @@ -17,6 +18,7 @@ participant_runtime_history_transition_diagnostics, participant_runtime_state_contract_diagnostics, ) +from .proposition_truth_contracts import proposition_truth_contract_diagnostics from .workflow_result_contracts import workflow_result_contract_diagnostics _BACKEND_CONTRACT_INVALID = "runtime.backend-contract-invalid" @@ -56,6 +58,11 @@ def _call_backend_apply( backend_args = tuple(backend_snapshot if arg is snapshot else arg for arg in args) try: result = method(*backend_args) + except (TypeError, ValueError): + return _failed_apply_result( + baseline_snapshot, + _backend_contract_invalid(address, "Backend could not construct a valid apply result."), + ) except Exception as exc: return _failed_apply_result(baseline_snapshot, _backend_call_failed(address, exc)) return _finalize_backend_apply( @@ -87,7 +94,14 @@ def _finalize_backend_apply( if invalid_message is not None: return _failed_apply_result(baseline_snapshot, _backend_contract_invalid(address, invalid_message)) assert isinstance(result, ApplyResult) - contract_diagnostics = _snapshot_contract_diagnostics(result.snapshot) + contract_diagnostics = _snapshot_address_contract_diagnostics(result.snapshot) + if not contract_diagnostics: + contract_diagnostics = _changed_address_transition_diagnostics( + result, + baseline_snapshot, + ) + if not contract_diagnostics: + contract_diagnostics = _snapshot_contract_diagnostics(result.snapshot) if not contract_diagnostics: contract_diagnostics = _snapshot_transition_contract_diagnostics(baseline_snapshot, result.snapshot) realization_provenance: tuple[RealizationProvenanceEntry, ...] = () @@ -125,7 +139,7 @@ def _backend_call_failed(address: str, exc: Exception) -> Diagnostic: return _failure_diagnostic( "runtime.backend-call-failed", address, - f"Backend method '{address}' raised {type(exc).__name__}: {exc}.", + f"Backend method '{address}' did not complete ({type(exc).__name__}).", ) @@ -205,13 +219,74 @@ def _apply_result_details_violation(result: ApplyResult, address: str) -> str | def _snapshot_contract_diagnostics(snapshot: RuntimeSnapshot) -> list[Diagnostic]: - diagnostics = workflow_result_contract_diagnostics(snapshot) - if diagnostics: - return diagnostics - diagnostics = evaluation_result_contract_diagnostics(snapshot) - if diagnostics: - return diagnostics - return participant_runtime_state_contract_diagnostics(snapshot) + checks = ( + workflow_result_contract_diagnostics, + evaluation_result_contract_diagnostics, + proposition_truth_contract_diagnostics, + participant_runtime_state_contract_diagnostics, + ) + diagnostics: list[Diagnostic] = [] + for check in checks: + diagnostics = check(snapshot) + if diagnostics: + break + return diagnostics + + +def _snapshot_address_contract_diagnostics(snapshot: RuntimeSnapshot) -> list[Diagnostic]: + for map_key, entry in snapshot.entries.items(): + try: + require_compiled_address(map_key, field_name="snapshot map key") + require_compiled_address(entry.address) + except ValueError: + return [ + _backend_contract_invalid( + "runtime.snapshot", + "Backend snapshot contains a non-canonical resource address.", + ) + ] + if map_key != entry.address: + return [ + _backend_contract_invalid( + "runtime.snapshot", + "Backend snapshot map key does not equal its embedded address.", + ) + ] + return [] + + +def _changed_address_transition_diagnostics( + result: ApplyResult, + baseline_snapshot: RuntimeSnapshot, +) -> list[Diagnostic]: + admitted = _snapshot_carrier_addresses(baseline_snapshot) | _snapshot_carrier_addresses(result.snapshot) + if set(result.changed_addresses) - admitted: + return [ + _backend_contract_invalid( + "runtime.changed-addresses", + "Backend reported a changed address outside the snapshot transition.", + ) + ] + return [] + + +def _snapshot_carrier_addresses(snapshot: RuntimeSnapshot) -> set[str]: + carriers = ( + snapshot.entries, + snapshot.orchestration_results, + snapshot.orchestration_history, + snapshot.evaluation_results, + snapshot.evaluation_history, + snapshot.proposition_truth_results, + snapshot.participant_episode_results, + snapshot.participant_episode_history, + snapshot.participant_behavior_history, + snapshot.shared_state_records, + snapshot.shared_state_history, + snapshot.joint_action_records, + snapshot.time_management_contexts, + ) + return {str(address) for carrier in carriers for address in carrier} def _snapshot_transition_contract_diagnostics( diff --git a/implementations/python/packages/aces_runtime/control_plane.py b/implementations/python/packages/aces_runtime/control_plane.py index 794d493fd..18eb871be 100644 --- a/implementations/python/packages/aces_runtime/control_plane.py +++ b/implementations/python/packages/aces_runtime/control_plane.py @@ -12,7 +12,14 @@ from uuid import uuid4 from aces_contracts.diagnostics import Diagnostic -from aces_contracts.planning import EvaluationPlan, OrchestrationPlan, ProvisioningPlan, RuntimeDomain +from aces_contracts.planning import ( + EvaluationPlan, + OrchestrationPlan, + PlanOperation, + ProvisioningPlan, + RuntimeDomain, + require_plan_operation_identity, +) from aces_contracts.runtime_state import ( OperationReceipt, OperationState, @@ -60,6 +67,62 @@ def _utc_now() -> str: return datetime.now(UTC).isoformat().replace("+00:00", "Z") +def _submitted_plan_diagnostics( + plan: ProvisioningPlan | OrchestrationPlan | EvaluationPlan, + domain: RuntimeDomain, + snapshot: RuntimeSnapshot, +) -> list[Diagnostic]: + admitted = set(snapshot.entries) | {operation.address for operation in plan.operations} + diagnostic: Diagnostic | None = None + for operation in plan.operations: + diagnostic = _submitted_operation_diagnostic(operation, domain, snapshot, admitted) + if diagnostic is not None: + break + return [diagnostic] if diagnostic is not None else [] + + +def _submitted_operation_diagnostic( + operation: PlanOperation, + domain: RuntimeDomain, + snapshot: RuntimeSnapshot, + admitted: set[str], +) -> Diagnostic | None: + diagnostic: Diagnostic | None = None + address = f"runtime.control-plane.{domain.value}" + try: + require_plan_operation_identity(domain, operation.address, operation.resource_type) + except ValueError: + diagnostic = Diagnostic( + code="runtime.plan-resource-incoherent", + domain="runtime", + address=address, + message="Submitted plan operation disagrees with the endpoint resource identity.", + ) + + dependencies = {*operation.ordering_dependencies, *operation.refresh_dependencies} + if diagnostic is None and dependencies - admitted: + diagnostic = Diagnostic( + code="runtime.plan-dependency-unresolved", + domain="runtime", + address=address, + message="Submitted plan contains a dependency outside its operations and admitted snapshot.", + ) + + existing = snapshot.entries.get(operation.address) + if ( + diagnostic is None + and existing is not None + and (existing.domain is not domain or existing.resource_type != operation.resource_type) + ): + diagnostic = Diagnostic( + code="runtime.plan-resource-incoherent", + domain="runtime", + address=address, + message="Submitted plan disagrees with the admitted snapshot resource identity.", + ) + return diagnostic + + class RuntimeControlPlane(ParticipantControlMixin, ParticipantRetrievalMixin): """Reference control plane for async runtime submission and observation.""" @@ -106,6 +169,14 @@ def submit_provisioning( idempotency_key: str = "", request_fingerprint: str = "", ) -> OperationReceipt: + diagnostics = _submitted_plan_diagnostics(plan, RuntimeDomain.PROVISIONING, self._snapshot) + if diagnostics: + return self._reject_diagnostics( + domain=RuntimeDomain.PROVISIONING, + diagnostics=diagnostics, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) diagnostics = _call_backend_diagnostics( self._target.provisioner.validate, plan, @@ -138,6 +209,14 @@ def submit_orchestration( domain=RuntimeDomain.ORCHESTRATION, message="Target does not provide an orchestrator.", ) + diagnostics = _submitted_plan_diagnostics(plan, RuntimeDomain.ORCHESTRATION, self._snapshot) + if diagnostics: + return self._reject_diagnostics( + domain=RuntimeDomain.ORCHESTRATION, + diagnostics=diagnostics, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) return execute_operation( self, OperationExecutionRequest( @@ -165,6 +244,14 @@ def submit_evaluation( domain=RuntimeDomain.EVALUATION, message="Target does not provide an evaluator.", ) + diagnostics = _submitted_plan_diagnostics(plan, RuntimeDomain.EVALUATION, self._snapshot) + if diagnostics: + return self._reject_diagnostics( + domain=RuntimeDomain.EVALUATION, + diagnostics=diagnostics, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + ) return execute_operation( self, OperationExecutionRequest( diff --git a/implementations/python/packages/aces_runtime/control_plane_api.py b/implementations/python/packages/aces_runtime/control_plane_api.py index 96319210e..34a8abf2f 100644 --- a/implementations/python/packages/aces_runtime/control_plane_api.py +++ b/implementations/python/packages/aces_runtime/control_plane_api.py @@ -4,6 +4,8 @@ from collections.abc import Awaitable, Callable from dataclasses import asdict +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as distribution_version from typing import Annotated from aces_contracts.contracts import ( @@ -145,6 +147,22 @@ def _read_identity_dependency(request: Request) -> ControlPlaneIdentity: _ReadIdentity = Annotated[ControlPlaneIdentity, Depends(_read_identity_dependency)] +def _control_plane_api_version() -> str: + """OpenAPI description version for the control-plane adapter. + + Classified (GOV-901; specs/evolution/versioning-deprecation-and-migration.md) + as the API-description version of the same bundled ``aces-sdl`` distribution. + It derives from installed distribution metadata rather than a hard-coded + literal, with the honest PEP 440 ``0.0.0+unknown`` sentinel when the + distribution is not installed. + """ + + try: + return distribution_version("aces-sdl") + except PackageNotFoundError: + return "0.0.0+unknown" + + def create_control_plane_app( control_plane: RuntimeControlPlane, *, @@ -155,7 +173,7 @@ def create_control_plane_app( security = security or ControlPlaneSecurityConfig.strict_defaults() app = FastAPI( title="ACES Runtime Control Plane", - version="0.1.0", + version=_control_plane_api_version(), description="Reference HTTP/JSON adapter over the repo-owned runtime control plane.", ) app.state.control_plane_api_auth = _ControlPlaneApiAuth(control_plane, security) diff --git a/implementations/python/packages/aces_runtime/control_plane_api_models.py b/implementations/python/packages/aces_runtime/control_plane_api_models.py index b3a69de2d..174e15713 100644 --- a/implementations/python/packages/aces_runtime/control_plane_api_models.py +++ b/implementations/python/packages/aces_runtime/control_plane_api_models.py @@ -76,6 +76,7 @@ def _provisioning_plan(model: ProvisioningPlanModel) -> ProvisioningPlan: for op in model.operations ], diagnostics=[_diagnostic_from_mapping(payload) for payload in model.diagnostics], + realization_envelope=model.realization_envelope, ) @@ -151,6 +152,7 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "orchestration_history": dict(snapshot.orchestration_history), "evaluation_results": dict(snapshot.evaluation_results), "evaluation_history": dict(snapshot.evaluation_history), + "proposition_truth_results": dict(snapshot.proposition_truth_results), "participant_episode_results": dict(snapshot.participant_episode_results), "participant_episode_history": dict(snapshot.participant_episode_history), "participant_behavior_history": dict(snapshot.participant_behavior_history), @@ -158,6 +160,11 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "shared_state_history": dict(snapshot.shared_state_history), "joint_action_records": dict(snapshot.joint_action_records), "time_management_contexts": dict(snapshot.time_management_contexts), + "realization_envelope": ( + snapshot.realization_envelope.model_dump(mode="json") + if snapshot.realization_envelope is not None + else None + ), "metadata": dict(snapshot.metadata), } ) diff --git a/implementations/python/packages/aces_runtime/control_plane_store.py b/implementations/python/packages/aces_runtime/control_plane_store.py index bb650f513..0f7b75d34 100644 --- a/implementations/python/packages/aces_runtime/control_plane_store.py +++ b/implementations/python/packages/aces_runtime/control_plane_store.py @@ -10,6 +10,7 @@ from pathlib import Path from typing import Any, Protocol +from aces_contracts.contracts import RealizationEnvelopeIdentityModel from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.planning import RuntimeDomain from aces_contracts.runtime_state import ( @@ -89,6 +90,7 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: "orchestration_history": {address: list(events) for address, events in snapshot.orchestration_history.items()}, "evaluation_results": dict(snapshot.evaluation_results), "evaluation_history": {address: list(events) for address, events in snapshot.evaluation_history.items()}, + "proposition_truth_results": dict(snapshot.proposition_truth_results), "participant_episode_results": dict(snapshot.participant_episode_results), "participant_episode_history": { participant_address: list(events) @@ -115,6 +117,9 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: } for entry in snapshot.realization_provenance ], + "realization_envelope": ( + snapshot.realization_envelope.model_dump(mode="json") if snapshot.realization_envelope is not None else None + ), "metadata": dict(snapshot.metadata), } @@ -142,6 +147,7 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: }, evaluation_results=dict(payload.get("evaluation_results", {})), evaluation_history={address: list(events) for address, events in payload.get("evaluation_history", {}).items()}, + proposition_truth_results=dict(payload.get("proposition_truth_results", {})), participant_episode_results=dict(payload.get("participant_episode_results", {})), participant_episode_history={ participant_address: list(events) @@ -171,6 +177,11 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: for item in payload.get("realization_provenance", []) if isinstance(item, dict) ), + realization_envelope=( + RealizationEnvelopeIdentityModel.model_validate(payload["realization_envelope"]) + if payload.get("realization_envelope") is not None + else None + ), metadata=dict(payload.get("metadata", {})), ) diff --git a/implementations/python/packages/aces_runtime/manager.py b/implementations/python/packages/aces_runtime/manager.py index 010a66a26..19feae0ce 100644 --- a/implementations/python/packages/aces_runtime/manager.py +++ b/implementations/python/packages/aces_runtime/manager.py @@ -436,6 +436,7 @@ def destroy(self) -> ApplyResult: ) for address in _delete_order(provisioning_entries) ], + realization_envelope=working_snapshot.realization_envelope, ) provision_result = _call_backend_apply( self._target.provisioner.apply, diff --git a/implementations/python/packages/aces_runtime/operational_apparatus.py b/implementations/python/packages/aces_runtime/operational_apparatus.py index a52781134..182f2c972 100644 --- a/implementations/python/packages/aces_runtime/operational_apparatus.py +++ b/implementations/python/packages/aces_runtime/operational_apparatus.py @@ -63,6 +63,7 @@ def _runtime_surface_summary(snapshot: RuntimeSnapshot) -> dict[str, int]: "orchestration_results": len(snapshot.orchestration_results), "orchestration_history": _history_count(snapshot.orchestration_history), "evaluation_results": len(snapshot.evaluation_results), + "proposition_truth_results": len(snapshot.proposition_truth_results), "evaluation_history": _history_count(snapshot.evaluation_history), "participant_episode_results": len(snapshot.participant_episode_results), "participant_episode_history": _history_count(snapshot.participant_episode_history), diff --git a/implementations/python/packages/aces_runtime/proposition_truth_contracts.py b/implementations/python/packages/aces_runtime/proposition_truth_contracts.py new file mode 100644 index 000000000..834c2a607 --- /dev/null +++ b/implementations/python/packages/aces_runtime/proposition_truth_contracts.py @@ -0,0 +1,81 @@ +"""Runtime admission for portable proposition truth-result envelopes.""" + +from __future__ import annotations + +from aces_contracts.contracts import PropositionTruthResultModel +from aces_contracts.diagnostics import Diagnostic +from aces_contracts.runtime_state import RuntimeSnapshot +from pydantic import ValidationError + +from .diagnostics import _failure_diagnostic + +_INVALID_CODE = "runtime.proposition-truth-contract-invalid" +_RESULTS_ADDRESS = "runtime.apply.proposition-truth-results" + + +def _invalid(address: str, message: str) -> Diagnostic: + return _failure_diagnostic(_INVALID_CODE, address, message) + + +def _validation_message(exc: ValidationError) -> str: + error = exc.errors()[0] + location = ".".join(str(part) for part in error.get("loc", ())) + prefix = f"{location}: " if location else "" + return prefix + str(error.get("msg", "invalid proposition truth result")) + + +def _semantic_entry_diagnostics( + snapshot: RuntimeSnapshot, + result: PropositionTruthResultModel, + address: str, +) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + proposition_entry = snapshot.entries.get(result.proposition_address) + if proposition_entry is None or proposition_entry.resource_type != "proposition": + diagnostics.append(_invalid(address, "proposition_address must reference an admitted proposition entry.")) + elif not isinstance(proposition_entry.payload, dict): + diagnostics.append(_invalid(address, "admitted proposition entry payload must be a mapping.")) + elif proposition_entry.payload.get("evaluation_basis") != result.evaluation_basis.value: + diagnostics.append(_invalid(address, "proposition entry evaluation_basis must match the truth result.")) + + assertion_entry = snapshot.entries.get(result.assertion_address) + if assertion_entry is None or assertion_entry.resource_type != "assertion": + diagnostics.append(_invalid(address, "assertion_address must reference an admitted assertion entry.")) + return diagnostics + if not isinstance(assertion_entry.payload, dict): + diagnostics.append(_invalid(address, "admitted assertion entry payload must be a mapping.")) + return diagnostics + if assertion_entry.payload.get("proposition_address") != result.proposition_address: + diagnostics.append(_invalid(address, "assertion entry proposition_address must match the truth result.")) + if assertion_entry.payload.get("polarity") != result.assertion_polarity.value: + diagnostics.append(_invalid(address, "assertion entry polarity must match the truth result.")) + return diagnostics + + +def proposition_truth_contract_diagnostics(snapshot: RuntimeSnapshot) -> list[Diagnostic]: + """Return fail-closed diagnostics for all truth results in a snapshot.""" + + if not isinstance(snapshot.proposition_truth_results, dict): + return [_invalid(_RESULTS_ADDRESS, "RuntimeSnapshot.proposition_truth_results must be a dict.")] + diagnostics: list[Diagnostic] = [] + for result_key, payload in snapshot.proposition_truth_results.items(): + address = result_key if isinstance(result_key, str) and result_key else _RESULTS_ADDRESS + if not isinstance(result_key, str) or not result_key: + diagnostics.append(_invalid(_RESULTS_ADDRESS, "Proposition truth result keys must be non-empty strings.")) + continue + if not isinstance(payload, dict): + diagnostics.append(_invalid(address, "Proposition truth results must be mappings.")) + continue + try: + result = PropositionTruthResultModel.model_validate(payload) + except ValidationError as exc: + diagnostics.append(_invalid(address, _validation_message(exc))) + continue + if result_key != result.assertion_address: + diagnostics.append(_invalid(address, "Proposition truth result map key must equal assertion_address.")) + continue + diagnostics.extend(_semantic_entry_diagnostics(snapshot, result, address)) + return diagnostics + + +__all__ = ["proposition_truth_contract_diagnostics"] diff --git a/implementations/python/packages/aces_runtime/result_contracts.py b/implementations/python/packages/aces_runtime/result_contracts.py index 9dfdce172..99d5cb19d 100644 --- a/implementations/python/packages/aces_runtime/result_contracts.py +++ b/implementations/python/packages/aces_runtime/result_contracts.py @@ -6,6 +6,7 @@ participant_runtime_history_transition_diagnostics, participant_runtime_state_contract_diagnostics, ) +from .proposition_truth_contracts import proposition_truth_contract_diagnostics from .workflow_result_contracts import workflow_result_contract_diagnostics __all__ = [ @@ -13,5 +14,6 @@ "participant_episode_contract_diagnostics", "participant_runtime_history_transition_diagnostics", "participant_runtime_state_contract_diagnostics", + "proposition_truth_contract_diagnostics", "workflow_result_contract_diagnostics", ] diff --git a/implementations/python/packages/aces_sdl/__init__.py b/implementations/python/packages/aces_sdl/__init__.py index d8dd902bf..db407b9a9 100644 --- a/implementations/python/packages/aces_sdl/__init__.py +++ b/implementations/python/packages/aces_sdl/__init__.py @@ -1,21 +1,42 @@ """ACES Scenario Description Language (SDL). -A backend-agnostic scenario specification language ported from the -Open Cyber Range SDL and extended with sections for content, accounts, -relationships, agents, objectives, workflows, and variables. +A backend-agnostic scenario specification language with revision-pinned syntax +and translated-model ancestry in Open Cyber Range SDL plus ACES-native +extensions. The normative derivation boundary is recorded in +``contracts/provenance/sdl-lineage-ledger-v1.json``; this module does not claim +drop-in compatibility. """ from importlib import import_module __all__ = [ + "admit_instantiated_scenario", + "canonical_instantiated_sdl_bytes", + "canonical_instantiated_sdl_digest", + "canonical_sdl_bytes", + "canonical_sdl_digest", + "build_declaration_index", + "INSTANTIATED_SNAPSHOT_PROFILE", "instantiate_scenario", "InstantiatedScenario", + "InstantiatedScenarioSnapshot", + "SDLCanonicalDigest", + "SDL_CANONICAL_PROFILE", + "SDLFormatResult", + "format_sdl_source", + "load_sdl_fragment", "parse_sdl", "parse_sdl_file", "Scenario", "SDLError", "SDLInstantiationError", + "SDLMigrationPolicy", + "SDLParserLimits", + "SDL_SOURCE_FORMAT", + "SDLParseDiagnostic", "SDLParseError", + "SDLSourcePosition", + "SDLSourceRange", "SDLValidationError", "VARIABLE_TOKEN_PATTERN", ] @@ -25,15 +46,39 @@ def __getattr__(name: str): if name in { "SDLError", "SDLInstantiationError", + "SDLParseDiagnostic", "SDLParseError", + "SDLSourcePosition", + "SDLSourceRange", "SDLValidationError", }: module = import_module("aces_sdl._errors") + elif name in { + "canonical_instantiated_sdl_bytes", + "canonical_instantiated_sdl_digest", + "canonical_sdl_bytes", + "canonical_sdl_digest", + "INSTANTIATED_SNAPSHOT_PROFILE", + "InstantiatedScenarioSnapshot", + "SDLCanonicalDigest", + }: + module = import_module("aces_sdl.canonical") + elif name in {"format_sdl_source", "SDLFormatResult"}: + module = import_module("aces_sdl.formatting") + elif name in { + "SDL_CANONICAL_PROFILE", + "SDLMigrationPolicy", + "SDLParserLimits", + "SDL_SOURCE_FORMAT", + }: + module = import_module("aces_sdl._source_profile") elif name == "VARIABLE_TOKEN_PATTERN": module = import_module("aces_sdl._base") - elif name == "instantiate_scenario": + elif name == "build_declaration_index": + module = import_module("aces_sdl._declarations") + elif name in {"admit_instantiated_scenario", "instantiate_scenario"}: module = import_module("aces_sdl.instantiate") - elif name in {"parse_sdl", "parse_sdl_file"}: + elif name in {"load_sdl_fragment", "parse_sdl", "parse_sdl_file"}: module = import_module("aces_sdl.parser") elif name in {"InstantiatedScenario", "Scenario"}: module = import_module("aces_sdl.scenario") diff --git a/implementations/python/packages/aces_sdl/_base.py b/implementations/python/packages/aces_sdl/_base.py index 4cd08bbf4..546c1a6f5 100644 --- a/implementations/python/packages/aces_sdl/_base.py +++ b/implementations/python/packages/aces_sdl/_base.py @@ -6,6 +6,8 @@ from pydantic import BaseModel, ConfigDict +from ._identifiers import PORTABLE_IDENTIFIER_PATTERN + class SDLModel(BaseModel): """Base for all SDL Pydantic models.""" @@ -16,7 +18,7 @@ class SDLModel(BaseModel): ) -_VARIABLE_NAME_PATTERN = r"[A-Za-z_][A-Za-z0-9_-]*" +_VARIABLE_NAME_PATTERN = PORTABLE_IDENTIFIER_PATTERN VARIABLE_NAME_PATTERN = _VARIABLE_NAME_PATTERN VARIABLE_NAME_RE = re.compile(r"^" + VARIABLE_NAME_PATTERN + r"$") # Single source of truth for the ``${name}`` substitution token, shared by the diff --git a/implementations/python/packages/aces_sdl/_composition_budget.py b/implementations/python/packages/aces_sdl/_composition_budget.py new file mode 100644 index 000000000..1e2b41738 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_composition_budget.py @@ -0,0 +1,86 @@ +"""Aggregate resource bounds for one SDL module-composition request.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from ._errors import SDLParseError +from ._identifiers import QualifiedName +from ._module_symbols import FORWARDING_AGENTS_SECTION, HASHMAP_SECTIONS +from ._source_profile import SDLParserLimits + + +@dataclass +class CompositionBudget: + limits: SDLParserLimits + imports: int = 0 + nodes: int = 0 + decoded_bytes: int = 0 + + def add_document(self, value: object, *, path: Path) -> None: + pending = [value] + count = 0 + while pending: + current = pending.pop() + count += 1 + if isinstance(current, str): + self.decoded_bytes += len(current.encode("utf-8")) + if isinstance(current, Mapping): + pending.extend(current.keys()) + pending.extend(current.values()) + elif isinstance(current, list | tuple): + pending.extend(current) + self.nodes += count + if self.nodes > self.limits.max_composed_nodes: + raise SDLParseError("SDL composition node budget exceeded", path=path) + if self.decoded_bytes > self.limits.max_composed_bytes: + raise SDLParseError("SDL composition decoded-byte budget exceeded", path=path) + + def add_import(self, *, path: Path) -> None: + self.imports += 1 + if self.imports > self.limits.max_imports: + raise SDLParseError("SDL composition import budget exceeded", path=path) + + def check_depth(self, depth: int, *, path: Path) -> None: + if depth > self.limits.max_composition_depth: + raise SDLParseError("SDL composition depth budget exceeded", path=path) + + def check_namespaces(self, payload: Mapping[str, Any], *, path: Path) -> None: + identifiers: list[str] = [] + for section_name in HASHMAP_SECTIONS: + section = payload.get(section_name) + if isinstance(section, Mapping): + identifiers.extend(str(name) for name in section) + agents = payload.get(FORWARDING_AGENTS_SECTION) + if isinstance(agents, list): + identifiers.extend( + str(agent.get("forwarding_agent_id")) + for agent in agents + if isinstance(agent, Mapping) and agent.get("forwarding_agent_id") + ) + for identifier in identifiers: + try: + namespace_depth = len(QualifiedName.parse(identifier).parts) - 1 + except ValueError as exc: + raise SDLParseError("SDL composition produced an invalid qualified identifier", path=path) from exc + if namespace_depth > self.limits.max_namespace_depth: + raise SDLParseError("SDL composition namespace-depth budget exceeded", path=path) + + +@dataclass(frozen=True) +class CompositionTraversal: + """Immutable ancestry plus the request-scoped mutable resource budget.""" + + seen: frozenset[Path] + budget: CompositionBudget + depth: int + + def descend_from(self, path: Path) -> CompositionTraversal: + return CompositionTraversal( + seen=self.seen | {path}, + budget=self.budget, + depth=self.depth + 1, + ) diff --git a/implementations/python/packages/aces_sdl/_composition_provenance.py b/implementations/python/packages/aces_sdl/_composition_provenance.py new file mode 100644 index 000000000..3051ecea2 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_composition_provenance.py @@ -0,0 +1,129 @@ +"""Portable provenance transformations for SDL module composition.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from ._errors import SDLParseError +from ._module_symbols import HASHMAP_SECTIONS +from .instantiate import _BoundScenarioResult +from .module_registry import ResolvedModule +from .phase_contracts import ( + CapabilityConstraint, + ExplicitnessProvenanceRecord, + ResolvedImportProvenance, +) +from .scenario import ImportDecl, ScenarioContent + + +def _normalized_digest(value: str) -> str: + return f"sha256:{value.removeprefix('sha256:')}" if value else "" + + +def prefixed_import_record( + record: ResolvedImportProvenance, + namespace: str, +) -> ResolvedImportProvenance: + payload = record.model_dump(mode="python") + payload["namespace"] = (namespace, *record.namespace) + return ResolvedImportProvenance.model_validate(payload) + + +def resolved_import_record( + resolved: ResolvedModule, + *, + requested: ImportDecl, + bindings: _BoundScenarioResult, +) -> ResolvedImportProvenance: + return ResolvedImportProvenance( + namespace=(requested.namespace,), + requested_source=requested.normalized_source, + requested_version=requested.version, + requested_digest=_normalized_digest(requested.digest), + module_id=resolved.module_descriptor.id, + module_version=resolved.module_descriptor.version, + resolved_source=resolved.resolved_source, + manifest_digest=_normalized_digest(resolved.manifest_digest), + content_digest=_normalized_digest(resolved.content_digest), + export_hash=_normalized_digest(resolved.export_hash), + signer_id=resolved.signer_id, + bindings=bindings.bindings, + ) + + +def prefixed_constraint( + constraint: CapabilityConstraint, + *, + namespace: str, + symbols: dict[str, dict[str, str] | set[str]], +) -> CapabilityConstraint: + parts = constraint.field_pointer.split("/") + if len(parts) < 3: + raise SDLParseError("Capability constraint does not address a namespaced declaration") + section_name = parts[1] + declaration_name = _decode_pointer_segment(parts[2]) + section_symbols = symbols.get(section_name) + if not isinstance(section_symbols, Mapping) or declaration_name not in section_symbols: + raise SDLParseError("Capability constraint does not resolve to an imported declaration") + parts[2] = _encode_pointer_segment(section_symbols[declaration_name]) + payload = constraint.model_dump(mode="python") + payload.update( + { + "field_pointer": "/".join(parts), + "parameter": (namespace, *constraint.parameter), + } + ) + return CapabilityConstraint.model_validate(payload) + + +def prefixed_explicitness( + record: ExplicitnessProvenanceRecord, + *, + namespace: str, + imported: ScenarioContent, + symbols: dict[str, dict[str, str] | set[str]], +) -> ExplicitnessProvenanceRecord: + payload = record.model_dump(mode="python") + payload.update( + { + "model_path": _prefixed_model_path(record.model_path, imported=imported, symbols=symbols), + "parameters": tuple((namespace, *parameter) for parameter in record.parameters), + } + ) + return ExplicitnessProvenanceRecord.model_validate(payload) + + +def _decode_pointer_segment(segment: str) -> str: + return segment.replace("~1", "/").replace("~0", "~") + + +def _encode_pointer_segment(segment: str) -> str: + return segment.replace("~", "~0").replace("/", "~1") + + +def _prefixed_model_path( + path: str, + *, + imported: ScenarioContent, + symbols: dict[str, dict[str, str] | set[str]], +) -> str: + prefixed_path = path + for section_name in HASHMAP_SECTIONS: + prefix = f"{section_name}." + if not path.startswith(prefix): + continue + section = getattr(imported, section_name, {}) + section_symbols = symbols.get(section_name) + if isinstance(section, Mapping) and isinstance(section_symbols, Mapping): + for declaration_name in sorted(section, key=len, reverse=True): + declaration_path = f"{prefix}{declaration_name}" + if ( + path == declaration_path + or path.startswith(f"{declaration_path}.") + or path.startswith(f"{declaration_path}[") + ): + suffix = path[len(declaration_path) :] + prefixed_path = f"{prefix}{section_symbols[declaration_name]}{suffix}" + break + break + return prefixed_path diff --git a/implementations/python/packages/aces_sdl/_declarations.py b/implementations/python/packages/aces_sdl/_declarations.py new file mode 100644 index 000000000..5e4344b94 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_declarations.py @@ -0,0 +1,426 @@ +"""Typed SDL declaration indexing and canonical-address collision checks.""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Iterable +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from ._errors import SDLValidationError +from ._identifiers import QualifiedName +from ._module_symbols import HASHMAP_SECTIONS +from ._reference_targetability import is_targetable_section +from ._runtime_service_families import RUNTIME_SERVICE_FAMILIES, RuntimeReferenceChild + +if TYPE_CHECKING: + from .entities import Entity + from .scenario import ScenarioContent + + +@dataclass(frozen=True) +class Declaration: + """One declared SDL identity before alias projection.""" + + kind: str + address: str + model_path: str + source: str | None = None + referenceable: bool = False + targetable: bool = False + + +class DeclarationIndex: + """Collision-preserving declarations plus non-authoritative lookup aliases.""" + + def __init__(self) -> None: + self._declarations: dict[str, Declaration] = {} + self._aliases: dict[str, set[str]] = defaultdict(set) + self._collisions: list[str] = [] + + @property + def addresses(self) -> frozenset[str]: + return frozenset(self._declarations) + + @property + def declarations(self) -> tuple[Declaration, ...]: + return tuple(self._declarations[address] for address in sorted(self._declarations)) + + @property + def collision_errors(self) -> tuple[str, ...]: + return tuple(dict.fromkeys(self._collisions)) + + def declaration_for(self, address: str) -> Declaration | None: + """Return the typed declaration at an exact canonical address.""" + + return self._declarations.get(address) + + def resolve(self, reference: str) -> set[str]: + return set(self._aliases.get(reference, ())) + + def reference_aliases(self, *, targetable: bool = False) -> dict[str, set[str]]: + """Return aliases projected through the typed reference policy.""" + + result: dict[str, set[str]] = {} + for alias, addresses in self._aliases.items(): + candidates = { + address + for address in addresses + if ( + (declaration := self._declarations.get(address)) is not None + and declaration.referenceable + and (declaration.targetable or not targetable) + ) + } + if candidates: + result[alias] = candidates + return result + + def reference_completions(self, *, targetable: bool = False) -> tuple[tuple[str, Declaration], ...]: + """Return one unambiguous preferred spelling per reference declaration.""" + + aliases = self.reference_aliases(targetable=targetable) + completions: list[tuple[str, Declaration]] = [] + for declaration in self.declarations: + if not declaration.referenceable or (targetable and not declaration.targetable): + continue + spellings = [alias for alias, candidates in aliases.items() if candidates == {declaration.address}] + spelling = min(spellings, key=lambda value: (value.count("."), len(value), value)) + completions.append((spelling, declaration)) + return tuple(completions) + + def spellings_for(self, reference: str) -> frozenset[str]: + """Return aliases denoting the same declarations as *reference*.""" + + targets = self.resolve(reference) + if not targets: + return frozenset({reference}) + return frozenset(alias for alias, candidates in self._aliases.items() if candidates.intersection(targets)) + + def add(self, declaration: Declaration, *, aliases: Iterable[str] = ()) -> None: + previous = self._declarations.get(declaration.address) + if previous is not None and previous != declaration: + self._collisions.append( + f"Canonical address '{declaration.address}' collides between " + f"{previous.kind} declaration at {previous.model_path} and " + f"{declaration.kind} declaration at {declaration.model_path}" + ) + return + self._declarations[declaration.address] = declaration + self._aliases[declaration.address].add(declaration.address) + for alias in aliases: + if alias: + self._aliases[alias].add(declaration.address) + + def raise_for_collisions(self) -> None: + if self._collisions: + raise SDLValidationError(list(dict.fromkeys(self._collisions))) + + +def _address(*parts: str) -> str: + return ".".join(parts) + + +def _qualified_parts(value: str) -> tuple[str, ...]: + return QualifiedName.parse(value).parts + + +def _add( + index: DeclarationIndex, + *, + kind: str, + address_parts: tuple[str, ...], + model_path: str, + aliases: Iterable[str] = (), + referenceable: bool = False, + targetable: bool = False, +) -> None: + index.add( + Declaration( + kind=kind, + address=_address(*address_parts), + model_path=model_path, + referenceable=referenceable, + targetable=targetable, + ), + aliases=aliases, + ) + + +def _add_entities( + index: DeclarationIndex, + entities: dict[str, Entity], + *, + address_prefix: tuple[str, ...], + model_prefix: str, +) -> None: + for name, entity in entities.items(): + parts = _qualified_parts(name) if not address_prefix else (name,) + entity_parts = (*address_prefix, *parts) + relative_name = _address(*entity_parts) + _add( + index, + kind="entity", + address_parts=("entities", *entity_parts), + model_path=f"{model_prefix}.{name}", + aliases=(relative_name,), + referenceable=True, + targetable=True, + ) + _add_entities( + index, + entity.entities, + address_prefix=entity_parts, + model_prefix=f"{model_prefix}.{name}.entities", + ) + + +def _add_runtime_children( + index: DeclarationIndex, + owner: object, + *, + address_prefix: tuple[str, ...], + model_prefix: str, + children: tuple[RuntimeReferenceChild, ...], +) -> None: + for child_spec in children: + for position, child in enumerate(getattr(owner, child_spec.collection_name, ())): + child_id = getattr(child, child_spec.id_field) + child_parts = (*address_prefix, child_spec.collection_name, child_id) + _add( + index, + kind=f"runtime-{child_spec.collection_name}", + address_parts=child_parts, + model_path=(f"{model_prefix}.{child_spec.collection_name}.{position}.{child_spec.id_field}"), + referenceable=True, + targetable=True, + ) + _add_runtime_children( + index, + child, + address_prefix=child_parts, + model_prefix=f"{model_prefix}.{child_spec.collection_name}.{position}", + children=child_spec.children, + ) + + +def _add_node_declarations(index: DeclarationIndex, scenario: ScenarioContent) -> None: + for node_name, node in scenario.nodes.items(): + node_parts = _qualified_parts(node_name) + _add( + index, + kind="node", + address_parts=("nodes", *node_parts), + model_path=f"nodes.{node_name}", + aliases=(node_name,), + referenceable=True, + targetable=True, + ) + for role_name in node.roles: + _add( + index, + kind="node-role", + address_parts=("nodes", *node_parts, "roles", role_name), + model_path=f"nodes.{node_name}.roles.{role_name}", + ) + for position, service in enumerate(node.services): + if service.name: + _add( + index, + kind="service", + address_parts=("nodes", *node_parts, "services", service.name), + model_path=f"nodes.{node_name}.services.{position}.name", + referenceable=True, + targetable=True, + ) + runtime = node.runtime + if runtime is None: + continue + for family in RUNTIME_SERVICE_FAMILIES: + for position, item in enumerate(getattr(runtime, family.collection_name, ())): + item_id = getattr(item, family.id_field) + runtime_parts = ( + "nodes", + *node_parts, + "runtime", + family.collection_name, + item_id, + ) + _add( + index, + kind=f"runtime-{family.collection_name}", + address_parts=runtime_parts, + model_path=(f"nodes.{node_name}.runtime.{family.collection_name}.{position}.{family.id_field}"), + referenceable=True, + targetable=True, + ) + _add_runtime_children( + index, + item, + address_prefix=runtime_parts, + model_prefix=f"nodes.{node_name}.runtime.{family.collection_name}.{position}", + children=family.child_refs, + ) + + +_REFERENCEABLE_SECTIONS = frozenset( + { + "features", + "conditions", + "propositions", + "assertions", + "vulnerabilities", + "injects", + "events", + "scripts", + "stories", + "accounts", + "relationships", + "agents", + "action_contracts", + "observation_boundaries", + "behavior_specifications", + "evidence_requirements", + "objectives", + } +) +_SPECIAL_SECTIONS = frozenset({"nodes", "infrastructure", "entities", "content", "workflows"}) + + +def _add_section_declarations(index: DeclarationIndex, scenario: ScenarioContent) -> None: + for section_name in HASHMAP_SECTIONS: + if section_name in _SPECIAL_SECTIONS: + continue + for name in getattr(scenario, section_name): + referenceable = section_name in _REFERENCEABLE_SECTIONS + _add( + index, + kind=section_name, + address_parts=(section_name, *_qualified_parts(name)), + model_path=f"{section_name}.{name}", + aliases=(name,), + referenceable=referenceable, + targetable=referenceable and is_targetable_section(section_name), + ) + + +def _add_variable_declarations(index: DeclarationIndex, scenario: ScenarioContent) -> None: + for name in getattr(scenario, "variables", {}): + _add( + index, + kind="variable", + address_parts=("variables", name), + model_path=f"variables.{name}", + aliases=(name,), + referenceable=True, + ) + + +def _add_infrastructure_declarations(index: DeclarationIndex, scenario: ScenarioContent) -> None: + for name, infrastructure in scenario.infrastructure.items(): + parts = _qualified_parts(name) + _add( + index, + kind="infrastructure", + address_parts=("infrastructure", *parts), + model_path=f"infrastructure.{name}", + referenceable=True, + targetable=True, + ) + for position, acl in enumerate(infrastructure.acls): + if acl.name: + _add( + index, + kind="infrastructure-acl", + address_parts=("infrastructure", *parts, "acls", acl.name), + model_path=f"infrastructure.{name}.acls.{position}.name", + referenceable=True, + targetable=True, + ) + + +def _add_content_declarations(index: DeclarationIndex, scenario: ScenarioContent) -> None: + for name, content in scenario.content.items(): + parts = _qualified_parts(name) + _add( + index, + kind="content", + address_parts=("content", *parts), + model_path=f"content.{name}", + aliases=(name,), + referenceable=True, + targetable=True, + ) + for position, item in enumerate(content.items): + _add( + index, + kind="content-item", + address_parts=("content", *parts, "items", item.name), + model_path=f"content.{name}.items.{position}.name", + aliases=(item.name,), + referenceable=True, + targetable=True, + ) + + +def _add_workflow_declarations(index: DeclarationIndex, scenario: ScenarioContent) -> None: + for name, workflow in scenario.workflows.items(): + parts = _qualified_parts(name) + _add( + index, + kind="workflow", + address_parts=("workflows", *parts), + model_path=f"workflows.{name}", + aliases=(name,), + referenceable=True, + ) + for step_name in workflow.steps: + _add( + index, + kind="workflow-step", + address_parts=("workflows", *parts, "steps", step_name), + model_path=f"workflows.{name}.steps.{step_name}", + aliases=(f"{name}.{step_name}",), + ) + + +def _add_forwarding_agent_declarations(index: DeclarationIndex, scenario: ScenarioContent) -> None: + for position, agent in enumerate(scenario.forwarding_agents): + _add( + index, + kind="forwarding-agent", + address_parts=("forwarding_agents", *_qualified_parts(agent.forwarding_agent_id)), + model_path=f"forwarding_agents.{position}.forwarding_agent_id", + ) + + +def build_declaration_index( + scenario: ScenarioContent, + *, + raise_on_collision: bool = True, +) -> DeclarationIndex: + """Index every catalogued declaration and reject non-injective rendering.""" + + index = DeclarationIndex() + _add( + index, + kind="scenario", + address_parts=("scenario", scenario.name), + model_path="name", + ) + + _add_section_declarations(index, scenario) + _add_variable_declarations(index, scenario) + _add_node_declarations(index, scenario) + _add_infrastructure_declarations(index, scenario) + _add_entities(index, scenario.entities, address_prefix=(), model_prefix="entities") + _add_content_declarations(index, scenario) + _add_workflow_declarations(index, scenario) + _add_forwarding_agent_declarations(index, scenario) + + if raise_on_collision: + index.raise_for_collisions() + return index + + +__all__ = ["Declaration", "DeclarationIndex", "build_declaration_index"] diff --git a/implementations/python/packages/aces_sdl/_errors.py b/implementations/python/packages/aces_sdl/_errors.py index 1e0145125..e38594e61 100644 --- a/implementations/python/packages/aces_sdl/_errors.py +++ b/implementations/python/packages/aces_sdl/_errors.py @@ -5,13 +5,76 @@ than failing on the first error. """ +from collections.abc import Iterable +from dataclasses import dataclass from pathlib import Path +from typing import Any class SDLError(Exception): """Base exception for all SDL operations.""" +@dataclass(frozen=True) +class SDLSourcePosition: + """One-based source position.""" + + line: int + column: int + + def as_dict(self) -> dict[str, int]: + return {"line": self.line, "column": self.column} + + +@dataclass(frozen=True) +class SDLSourceRange: + """Half-open source range for one authored token.""" + + start: SDLSourcePosition + end: SDLSourcePosition + + def as_dict(self) -> dict[str, dict[str, int]]: + return {"start": self.start.as_dict(), "end": self.end.as_dict()} + + +@dataclass(frozen=True) +class SDLParseDiagnostic: + """Structured diagnostic produced before SDL model construction.""" + + code: str + message: str + pointer: str + primary_range: SDLSourceRange + authored_keys: tuple[str, str] | None = None + related_range: SDLSourceRange | None = None + related_message: str | None = None + stage: str = "parse" + severity: str = "error" + source: str | None = None + + def as_dict(self) -> dict[str, Any]: + payload: dict[str, Any] = { + "stage": self.stage, + "severity": self.severity, + "code": self.code, + "message": self.message, + "path": self.pointer, + "range": self.primary_range.as_dict(), + } + if self.source is not None: + payload["source"] = self.source + if self.authored_keys is not None: + payload["authored_keys"] = list(self.authored_keys) + if self.related_range is not None: + payload["related"] = [ + { + "message": self.related_message or "Related authored key.", + "range": self.related_range.as_dict(), + } + ] + return payload + + class SDLParseError(SDLError): """YAML parsing or structural validation failed. @@ -20,9 +83,16 @@ class SDLParseError(SDLError): details: Detailed error message. """ - def __init__(self, message: str, path: Path | None = None) -> None: + def __init__( + self, + message: str, + path: Path | None = None, + *, + diagnostics: Iterable[SDLParseDiagnostic] = (), + ) -> None: self.path = path self.details = message + self.diagnostics = tuple(diagnostics) prefix = f"{path}: " if path else "" super().__init__(f"{prefix}{message}") diff --git a/implementations/python/packages/aces_sdl/_identifiers.py b/implementations/python/packages/aces_sdl/_identifiers.py new file mode 100644 index 000000000..766f34ccc --- /dev/null +++ b/implementations/python/packages/aces_sdl/_identifiers.py @@ -0,0 +1,140 @@ +"""Portable SDL identifiers and composition-generated qualified names.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Annotated, Any + +from pydantic import AfterValidator, WithJsonSchema + +PORTABLE_IDENTIFIER_PATTERN = r"[a-z0-9][a-z0-9_-]{0,63}" +PORTABLE_IDENTIFIER_RE = re.compile(PORTABLE_IDENTIFIER_PATTERN, re.ASCII) +PORTABLE_IDENTIFIER_MAX_LENGTH = 64 +QUALIFIED_IDENTIFIER_MAX_LENGTH = 2048 +PRIVATE_NAMESPACE_SEGMENT = "__private" + +# JSON Schema's `$` may match immediately before a final newline. Combining a +# first-character check with an explicit rejection of every character outside +# the alphabet keeps the machine-readable contract independent of that quirk. +PORTABLE_IDENTIFIER_JSON_SCHEMA: dict[str, Any] = { + "type": "string", + "minLength": 1, + "maxLength": PORTABLE_IDENTIFIER_MAX_LENGTH, + "pattern": "^[a-z0-9]", + "not": {"pattern": "[^a-z0-9_-]"}, +} + + +def is_portable_identifier(value: object) -> bool: + """Return whether *value* is one exact portable local identifier.""" + + return isinstance(value, str) and PORTABLE_IDENTIFIER_RE.fullmatch(value) is not None + + +def require_portable_identifier(value: object, *, field_name: str) -> str: + """Return a valid portable identifier or raise a value-only-safe error.""" + + if not is_portable_identifier(value): + raise ValueError( + f"{field_name} must be a portable SDL identifier: 1-64 lowercase " + "ASCII letters, digits, hyphens, or underscores, starting with a letter or digit" + ) + return value + + +def _validate_portable_identifier(value: str) -> str: + return require_portable_identifier(value, field_name="value") + + +def _validate_optional_portable_identifier(value: str) -> str: + return value if value == "" else require_portable_identifier(value, field_name="value") + + +PortableIdentifier = Annotated[ + str, + AfterValidator(_validate_portable_identifier), + WithJsonSchema(PORTABLE_IDENTIFIER_JSON_SCHEMA), +] +OptionalPortableIdentifier = Annotated[ + str, + AfterValidator(_validate_optional_portable_identifier), + WithJsonSchema( + { + "anyOf": [ + {"const": ""}, + PORTABLE_IDENTIFIER_JSON_SCHEMA, + ] + } + ), +] + + +def require_module_identifier(value: object, *, field_name: str = "module.id") -> str: + """Validate the exact ``publisher/name`` module identity shape.""" + + if not isinstance(value, str): + raise ValueError(f"{field_name} must use portable 'publisher/name' format") + parts = value.split("/") + if len(parts) != 2 or any(not is_portable_identifier(part) for part in parts): + raise ValueError(f"{field_name} must use portable 'publisher/name' format") + return value + + +@dataclass(frozen=True, order=True) +class QualifiedName: + """A composition-generated namespace path followed by one local symbol.""" + + parts: tuple[str, ...] + + def __post_init__(self) -> None: + if not self.parts or not is_portable_identifier(self.parts[-1]): + raise ValueError("qualified SDL name must end with a portable local identifier") + for part in self.parts[:-1]: + if part != PRIVATE_NAMESPACE_SEGMENT and not is_portable_identifier(part): + raise ValueError("qualified SDL namespace contains an invalid segment") + if len(self.render()) > QUALIFIED_IDENTIFIER_MAX_LENGTH: + raise ValueError("qualified SDL name exceeds the maximum length") + + @classmethod + def parse(cls, value: object) -> QualifiedName: + if not isinstance(value, str): + raise ValueError("qualified SDL name must be a string") + return cls(tuple(value.split("."))) + + @classmethod + def local(cls, value: object) -> QualifiedName: + return cls((require_portable_identifier(value, field_name="local identifier"),)) + + def prefixed(self, namespace: object, *, private: bool = False) -> QualifiedName: + segment = require_portable_identifier(namespace, field_name="namespace") + prefix = (segment, PRIVATE_NAMESPACE_SEGMENT) if private else (segment,) + return QualifiedName((*prefix, *self.parts)) + + def render(self) -> str: + return ".".join(self.parts) + + +def require_qualified_identifier(value: object, *, field_name: str) -> str: + """Validate one composition-generated qualified symbol spelling.""" + + try: + return QualifiedName.parse(value).render() + except ValueError as exc: + raise ValueError(f"{field_name} must be a qualified SDL identifier") from exc + + +__all__ = [ + "OptionalPortableIdentifier", + "PORTABLE_IDENTIFIER_JSON_SCHEMA", + "PORTABLE_IDENTIFIER_MAX_LENGTH", + "PORTABLE_IDENTIFIER_PATTERN", + "PRIVATE_NAMESPACE_SEGMENT", + "QUALIFIED_IDENTIFIER_MAX_LENGTH", + "PortableIdentifier", + "QualifiedName", + "is_portable_identifier", + "require_module_identifier", + "require_portable_identifier", + "require_qualified_identifier", +] diff --git a/implementations/python/packages/aces_sdl/_language_diagnostics.py b/implementations/python/packages/aces_sdl/_language_diagnostics.py index 2bca54455..de576519a 100644 --- a/implementations/python/packages/aces_sdl/_language_diagnostics.py +++ b/implementations/python/packages/aces_sdl/_language_diagnostics.py @@ -4,6 +4,8 @@ from typing import Any +from ._errors import SDLParseError + def invalid( stage: str, @@ -33,3 +35,14 @@ def diagnostic( if location is not None: payload["range"] = {"start": location, "end": location} return payload + + +def parse_error(error: SDLParseError) -> dict[str, Any]: + """Preserve structured parser diagnostics in language-service responses.""" + if error.diagnostics: + return { + "status": "invalid", + "stage": "parse", + "diagnostics": [item.as_dict() for item in error.diagnostics], + } + return invalid("parse", "sdl.parse", error.details) diff --git a/implementations/python/packages/aces_sdl/_language_metadata.py b/implementations/python/packages/aces_sdl/_language_metadata.py index 6c7fce8c8..3e68f8b9c 100644 --- a/implementations/python/packages/aces_sdl/_language_metadata.py +++ b/implementations/python/packages/aces_sdl/_language_metadata.py @@ -10,50 +10,67 @@ ("infrastructure", "links"): "infrastructure", ("infrastructure", "dependencies"): "infrastructure", ("features", "dependencies"): "features", - ("events", "conditions"): "conditions", + ("conditions", "proposition"): "propositions", + ("propositions", "subjects"): "targetable", + ("propositions", "evidence_requirements"): "evidence_requirements", + ("assertions", "proposition"): "propositions", + ("events", "assertions"): "assertions", ("events", "injects"): "injects", ("scripts", "events"): "events", ("stories", "scripts"): "scripts", ("content", "target"): "nodes", ("accounts", "node"): "nodes", - ("relationships", "source"): "any", - ("relationships", "target"): "any", + ("relationships", "source"): "targetable", + ("relationships", "target"): "targetable", ("agents", "entity"): "entities", ("agents", "starting_accounts"): "accounts", + ("agents", "starting_assertions"): "assertions", ("behavior_specifications", "participant_refs"): "agents", ("behavior_specifications", "action_contract_refs"): "action_contracts", ("behavior_specifications", "observation_boundary_refs"): "observation_boundaries", ("behavior_specifications", "outcome_interpretation_rule_refs"): "outcome_interpretation_rules", - ("behavior_specifications", "authority_scope_refs"): "any", - ("evidence_requirements", "source_refs"): "any", - ("evidence_requirements", "scope_refs"): "any", - ("evidence_requirements", "channel_refs"): "any", - ("evidence_requirements", "trigger_ref"): "any", - ("evidence_requirements", "boundary_ref"): "any", + ("behavior_specifications", "authority_scope_refs"): "targetable", + ("evidence_requirements", "source_refs"): "targetable", + ("evidence_requirements", "scope_refs"): "targetable", + ("evidence_requirements", "channel_refs"): "targetable", + ("evidence_requirements", "trigger_ref"): "targetable", + ("evidence_requirements", "boundary_ref"): "targetable", ("objectives", "agent"): "agents", ("objectives", "entity"): "entities", - ("objectives", "targets"): "any", + ("objectives", "targets"): "targetable", ("objectives", "depends_on"): "objectives", + ("objectives", "assertions"): "assertions", ("injects", "from_entity"): "entities", ("injects", "to_entities"): "entities", ("workflows", "start"): "workflow_steps", + ("workflows", "assertions"): "assertions", } SECTION_FIELD_COMPLETIONS = { "nodes": ("type", "description", "os", "resources", "features", "conditions", "services", "roles"), "infrastructure": ("count", "links", "dependencies", "properties"), "features": ("type", "source", "version", "dependencies"), - "conditions": ("command", "interval", "description"), + "conditions": ("proposition", "command", "interval", "source", "description"), + "propositions": ( + "description", + "subjects", + "basis", + "predicate", + "quantifier", + "threshold", + "evidence_requirements", + ), + "assertions": ("description", "proposition", "role", "polarity"), "vulnerabilities": ("name", "description", "technical", "class"), "entities": ("name", "role", "facts", "entities"), "injects": ("source", "from_entity", "to_entities"), - "events": ("conditions", "injects"), + "events": ("assertions", "injects"), "scripts": ("start_time", "end_time", "speed", "events"), "stories": ("speed", "scripts"), "content": ("type", "target", "format", "source", "path", "items"), "accounts": ("username", "node", "password_strength"), "relationships": ("type", "source", "target", "properties"), - "agents": ("entity", "actions", "starting_accounts", "initial_knowledge"), + "agents": ("entity", "actions", "starting_accounts", "starting_assertions", "initial_knowledge"), "behavior_specifications": ( "semantic_version", "lifecycle_state", diff --git a/implementations/python/packages/aces_sdl/_language_references.py b/implementations/python/packages/aces_sdl/_language_references.py index 9b7187c5b..e1fd6542f 100644 --- a/implementations/python/packages/aces_sdl/_language_references.py +++ b/implementations/python/packages/aces_sdl/_language_references.py @@ -5,15 +5,17 @@ from collections.abc import Collection from typing import Any -import yaml -from yaml.error import MarkedYAMLError, YAMLError from yaml.nodes import MappingNode, Node, ScalarNode, SequenceNode -from ._language_diagnostics import invalid as _invalid +from ._declarations import DeclarationIndex +from ._errors import SDLParseError +from ._identifiers import QualifiedName +from ._language_diagnostics import parse_error as _parse_error from ._language_metadata import REFERENCE_COMPLETION_TARGETS +from ._reference_targetability import is_targetable_section +from ._yaml_loader import compose_sdl_yaml -_CODE_PARSE = "sdl.parse" -_SUCCESS_REFERENCE_TARGETS = frozenset({"conditions"}) +_SUCCESS_REFERENCE_TARGETS = frozenset({"assertions"}) def find_references( @@ -21,20 +23,46 @@ def find_references( symbol: str, *, section_fields: Collection[str], + declaration_index: DeclarationIndex | None = None, ) -> dict[str, Any]: """Return definition and occurrence locations for an SDL symbol.""" - root, error = _compose_yaml(sdl_content) - if error is not None: - return error + if not sdl_content.strip(): + result = {"status": "ok", "symbol": symbol, "definitions": [], "occurrences": []} + else: + root, error = _compose_yaml(sdl_content) + result = ( + error + if error is not None + else _reference_result(root, symbol, section_fields, declaration_index=declaration_index) + ) + return result + + +def _reference_result( + root: Node | None, + symbol: str, + section_fields: Collection[str], + *, + declaration_index: DeclarationIndex | None, +) -> dict[str, Any]: if root is None: return {"status": "ok", "symbol": symbol, "definitions": [], "occurrences": []} - definitions = _collect_definitions(root, section_fields) - matching_definitions = [definition for definition in definitions if _definition_matches_symbol(definition, symbol)] + if declaration_index is not None: + definitions = [ + definition + for definition in definitions + if declaration_index.declaration_for(definition["qualified_name"]) is not None + ] occurrences: list[dict[str, Any]] = [] + spellings = ( + declaration_index.spellings_for(symbol) + if declaration_index is not None + else frozenset({symbol, _bare_symbol(symbol)}) + ) _collect_occurrences( root, - symbol, + spellings, [], occurrences, qualified_section=_qualified_symbol_section(symbol), @@ -42,23 +70,16 @@ def find_references( return { "status": "ok", "symbol": symbol, - "definitions": matching_definitions, + "definitions": [item for item in definitions if _definition_matches_symbol(item, symbol)], "occurrences": occurrences, } def _compose_yaml(sdl_content: str) -> tuple[Node | None, dict[str, Any] | None]: try: - return yaml.compose(sdl_content), None - except MarkedYAMLError as exc: - return None, _invalid( - "parse", - _CODE_PARSE, - str(exc.problem or exc), - location=_location_from_mark(exc.problem_mark), - ) - except YAMLError as exc: - return None, _invalid("parse", _CODE_PARSE, str(exc)) + return compose_sdl_yaml(sdl_content), None + except SDLParseError as exc: + return None, _parse_error(exc) def _collect_definitions(root: Node, section_fields: Collection[str]) -> list[dict[str, Any]]: @@ -110,7 +131,7 @@ def _collect_section_definitions( def _collect_occurrences( node: Node, - symbol: str, + spellings: Collection[str], path: list[str], occurrences: list[dict[str, Any]], *, @@ -119,7 +140,7 @@ def _collect_occurrences( if isinstance(node, MappingNode): _collect_mapping_occurrences( node, - symbol, + spellings, path, occurrences, qualified_section=qualified_section, @@ -129,7 +150,7 @@ def _collect_occurrences( if isinstance(node, SequenceNode): _collect_sequence_occurrences( node, - symbol, + spellings, path, occurrences, qualified_section=qualified_section, @@ -139,7 +160,7 @@ def _collect_occurrences( if isinstance(node, ScalarNode): _append_scalar_occurrence( node, - symbol, + spellings, path, occurrences, qualified_section=qualified_section, @@ -148,7 +169,7 @@ def _collect_occurrences( def _collect_mapping_occurrences( node: MappingNode, - symbol: str, + spellings: Collection[str], path: list[str], occurrences: list[dict[str, Any]], *, @@ -159,7 +180,7 @@ def _collect_mapping_occurrences( key_path = [*path, str(key)] if key is not None else [*path, "?"] if _is_matching_occurrence( key, - symbol, + spellings, key_path, qualified_section=qualified_section, mapping_key=True, @@ -173,7 +194,7 @@ def _collect_mapping_occurrences( ) _collect_occurrences( value_node, - symbol, + spellings, key_path, occurrences, qualified_section=qualified_section, @@ -182,7 +203,7 @@ def _collect_mapping_occurrences( def _collect_sequence_occurrences( node: SequenceNode, - symbol: str, + spellings: Collection[str], path: list[str], occurrences: list[dict[str, Any]], *, @@ -191,7 +212,7 @@ def _collect_sequence_occurrences( for index, item in enumerate(node.value): _collect_occurrences( item, - symbol, + spellings, [*path, str(index)], occurrences, qualified_section=qualified_section, @@ -200,7 +221,7 @@ def _collect_sequence_occurrences( def _append_scalar_occurrence( node: ScalarNode, - symbol: str, + spellings: Collection[str], path: list[str], occurrences: list[dict[str, Any]], *, @@ -209,7 +230,7 @@ def _append_scalar_occurrence( value = _scalar_value(node) if _is_matching_occurrence( value, - symbol, + spellings, path, qualified_section=qualified_section, mapping_key=False, @@ -225,7 +246,7 @@ def _append_scalar_occurrence( def _is_matching_occurrence( value: str | None, - symbol: str, + spellings: Collection[str], path: list[str], *, qualified_section: str | None, @@ -233,7 +254,7 @@ def _is_matching_occurrence( ) -> bool: return ( value is not None - and _matches_symbol(value, symbol) + and value in spellings and _include_occurrence( path, qualified_section=qualified_section, @@ -273,10 +294,6 @@ def _scalar_value(node: Node) -> str | None: return None -def _matches_symbol(value: str, symbol: str) -> bool: - return value == symbol or value == _bare_symbol(symbol) - - def _definition_matches_symbol(definition: dict[str, Any], symbol: str) -> bool: qualified_section = _qualified_symbol_section(symbol) if qualified_section is not None: @@ -285,9 +302,11 @@ def _definition_matches_symbol(definition: dict[str, Any], symbol: str) -> bool: def _qualified_symbol_section(symbol: str) -> str | None: - if "." not in symbol: + try: + parts = QualifiedName.parse(symbol).parts + except (TypeError, ValueError): return None - return symbol.split(".", 1)[0] + return parts[0] if len(parts) > 1 else None def _include_occurrence( @@ -299,7 +318,9 @@ def _include_occurrence( if qualified_section is None: return True target = _reference_target_for_path(path, mapping_key=mapping_key) - return target in {qualified_section, "any"} + if target in {qualified_section, "any"}: + return True + return target == "targetable" and is_targetable_section(qualified_section) def _reference_target_for_path(path: list[str], *, mapping_key: bool) -> str | None: @@ -315,7 +336,10 @@ def _reference_target_for_path(path: list[str], *, mapping_key: bool) -> str | N def _bare_symbol(symbol: str) -> str: - return symbol.rsplit(".", 1)[-1] + try: + return QualifiedName.parse(symbol).parts[-1] + except (TypeError, ValueError): + return symbol def _range_from_node(node: Node) -> dict[str, dict[str, int]]: diff --git a/implementations/python/packages/aces_sdl/_mapping_key_analyzer.py b/implementations/python/packages/aces_sdl/_mapping_key_analyzer.py new file mode 100644 index 000000000..a44234354 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_mapping_key_analyzer.py @@ -0,0 +1,462 @@ +"""Mapping-key analysis for the SDL YAML authoring boundary. + +Walks a composed YAML node graph and emits source-anchored diagnostics for +non-canonical fields/merges, duplicate/conflicting keys, invalid identifiers, and +alias cycles. Split from :mod:`aces_sdl._yaml_loader` to keep each module under the +ADR-015 source-size cap; the loaders there drive ``_MappingAnalyzer`` via +``_validate_mapping_keys``. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from yaml.nodes import MappingNode, Node, ScalarNode, SequenceNode + +from ._errors import SDLParseDiagnostic, SDLSourcePosition, SDLSourceRange +from ._identifiers import is_portable_identifier +from ._mapping_scopes import MappingScope, is_literal_map_field, normalize_field_key +from ._source_identifier_paths import is_declaration_key_path, is_scalar_identifier_path +from ._source_profile import SDLMigrationPolicy + +_MERGE_TAG = "tag:yaml.org,2002:merge" +_STRING_TAG = "tag:yaml.org,2002:str" + + +@dataclass(frozen=True) +class _Entry: + canonical: str + authored: str + key_node: ScalarNode + + +@dataclass(frozen=True) +class _EffectiveMapping: + entries: tuple[_Entry, ...] + conflicts: tuple[tuple[_Entry, _Entry], ...] + + +@dataclass +class _EffectiveAccumulator: + entries: list[_Entry] = field(default_factory=list) + conflicts: list[tuple[_Entry, _Entry]] = field(default_factory=list) + seen: dict[str, _Entry] = field(default_factory=dict) + + def add(self, entry: _Entry) -> None: + previous = self.seen.get(entry.canonical) + if previous is None: + self.seen[entry.canonical] = entry + self.entries.append(entry) + else: + self.conflicts.append((previous, entry)) + + def build(self) -> _EffectiveMapping: + return _EffectiveMapping(tuple(self.entries), tuple(self.conflicts)) + + +class _MappingAnalyzer: + def __init__( + self, + *, + migration_policy: SDLMigrationPolicy, + path: Path | None, + source_ranges: dict[str, SDLSourceRange] | None = None, + ) -> None: + self.diagnostics: list[SDLParseDiagnostic] = [] + self._migration_policy = migration_policy + self._source = str(path) if path is not None else None + self._effective_cache: dict[tuple[int, MappingScope], _EffectiveMapping] = {} + self._diagnostic_keys: set[tuple[Any, ...]] = set() + self._walked: set[tuple[int, MappingScope]] = set() + self._source_ranges = source_ranges + + def analyze( + self, + root: Node, + *, + scope: MappingScope, + base_tokens: list[str], + ) -> tuple[SDLParseDiagnostic, ...]: + self._walk(root, scope=scope, tokens=base_tokens, active=set()) + return tuple( + sorted( + self.diagnostics, + key=lambda item: ( + item.primary_range.start.line, + item.primary_range.start.column, + item.code, + item.pointer, + ), + ) + ) + + def _walk( + self, + node: Node, + *, + scope: MappingScope, + tokens: list[str], + active: set[int], + ) -> None: + if self._source_ranges is not None: + self._source_ranges[_encode_pointer(tokens)] = _range_from_node(node) + identity = id(node) + if identity in active: + self._add_alias_cycle(node, tokens) + else: + walk_key = (identity, scope) + if walk_key not in self._walked and isinstance(node, (MappingNode, SequenceNode)): + self._walked.add(walk_key) + active.add(identity) + try: + if isinstance(node, MappingNode): + self._walk_mapping(node, scope=scope, tokens=tokens, active=active) + else: + self._walk_sequence(node, scope=scope, tokens=tokens, active=active) + finally: + active.remove(identity) + + def _add_alias_cycle(self, node: Node, tokens: list[str]) -> None: + self._add( + SDLParseDiagnostic( + code="sdl.alias_cycle", + message="Cyclic YAML aliases are not valid SDL authoring input.", + pointer=_encode_pointer(tokens), + primary_range=_range_from_node(node), + source=self._source, + ) + ) + + def _walk_sequence( + self, + node: SequenceNode, + *, + scope: MappingScope, + tokens: list[str], + active: set[int], + ) -> None: + for index, item in enumerate(node.value): + self._walk(item, scope=scope, tokens=[*tokens, str(index)], active=active) + + def _walk_mapping( + self, + node: MappingNode, + *, + scope: MappingScope, + tokens: list[str], + active: set[int], + ) -> None: + effective = self._effective_mapping(node, scope=scope, active=set()) + conflicted_key_nodes = {id(entry.key_node) for pair in effective.conflicts for entry in pair} + for first, conflicting in effective.conflicts: + self._add_conflict(first, conflicting, tokens) + for key_node, value_node in node.value: + self._walk_mapping_entry( + key_node, + value_node, + scope=scope, + tokens=tokens, + active=active, + suppress_field_migration=id(key_node) in conflicted_key_nodes, + ) + + def _add_conflict(self, first: _Entry, conflicting: _Entry, tokens: list[str]) -> None: + self._add( + SDLParseDiagnostic( + code="sdl.mapping_key_conflict", + message=_conflict_message(first, conflicting), + pointer=_encode_pointer([*tokens, conflicting.canonical]), + authored_keys=(first.authored, conflicting.authored), + primary_range=_range_from_node(conflicting.key_node), + related_range=_range_from_node(first.key_node), + related_message=f"First authored key '{first.authored}'.", + source=self._source, + ) + ) + + def _walk_mapping_entry( + self, + key_node: Node, + value_node: Node, + *, + scope: MappingScope, + tokens: list[str], + active: set[int], + suppress_field_migration: bool, + ) -> None: + if _is_merge_key(key_node): + self._add_migration_diagnostic( + key_node, + code="sdl.noncanonical_merge", + message="YAML merge keys are migration syntax, not canonical sdl-yaml/v1.", + pointer=_encode_pointer(tokens), + authored_keys=("<<", "<<"), + ) + self._walk_merge_value(value_node, scope=scope, tokens=tokens, active=active) + return + authored = _authored_key(key_node) + if not _is_string_key(key_node): + self._add_key_type_diagnostic(key_node, authored, tokens) + return + canonical = normalize_field_key(authored) if scope is MappingScope.STRUCTURAL else authored + if scope is MappingScope.STRUCTURAL and canonical != authored and not suppress_field_migration: + self._add_migration_diagnostic( + key_node, + code="sdl.noncanonical_field", + message=f"Structural field '{authored}' must use canonical spelling '{canonical}'.", + pointer=_encode_pointer([*tokens, canonical]), + authored_keys=(authored, canonical), + ) + child_tokens = [*tokens, canonical] + self._validate_entry_identifiers( + key_node, + value_node, + authored, + tokens=tokens, + child_tokens=child_tokens, + suppress_field_migration=suppress_field_migration, + ) + child_scope = _child_scope(scope, canonical, value_node) + self._walk(value_node, scope=child_scope, tokens=child_tokens, active=active) + + def _validate_entry_identifiers( + self, + key_node: Node, + value_node: Node, + authored: str, + *, + tokens: list[str], + child_tokens: list[str], + suppress_field_migration: bool, + ) -> None: + if is_declaration_key_path(tokens) and not suppress_field_migration: + self._validate_identifier_node(key_node, pointer_tokens=child_tokens) + if tokens == ["nodes"] and len(authored) > 35: + self._add_identifier_diagnostic(key_node, pointer_tokens=child_tokens, node_limit=True) + if child_tokens == ["name"]: + self._validate_identifier_node(value_node, pointer_tokens=child_tokens) + if is_scalar_identifier_path(child_tokens): + self._validate_identifier_node(value_node, pointer_tokens=child_tokens) + + def _validate_identifier_node(self, node: Node, *, pointer_tokens: list[str]) -> None: + if not isinstance(node, ScalarNode) or node.tag != _STRING_TAG or not is_portable_identifier(node.value): + self._add_identifier_diagnostic(node, pointer_tokens=pointer_tokens) + + def _add_identifier_diagnostic( + self, + node: Node, + *, + pointer_tokens: list[str], + node_limit: bool = False, + ) -> None: + message = ( + "Authored node identifiers must be at most 35 characters." + if node_limit + else ( + "Authored identifiers must be 1-64 lowercase ASCII letters, digits, hyphens, or " + "underscores and start with a letter or digit." + ) + ) + self._add( + SDLParseDiagnostic( + code="sdl.identifier.invalid", + message=message, + pointer=_encode_pointer(pointer_tokens), + primary_range=_range_from_node(node), + source=self._source, + ) + ) + + def _add_key_type_diagnostic(self, key_node: Node, authored: str, tokens: list[str]) -> None: + message = ( + "SDL top-level mapping keys must be strings" + if not tokens + else f"SDL mapping key '{authored}' must be a string." + ) + self._add( + SDLParseDiagnostic( + code="sdl.mapping_key_type", + message=message, + pointer=_encode_pointer([*tokens, authored]), + primary_range=_range_from_node(key_node), + source=self._source, + ) + ) + + def _add_migration_diagnostic( + self, + key_node: Node, + *, + code: str, + message: str, + pointer: str, + authored_keys: tuple[str, str], + ) -> None: + self._add( + SDLParseDiagnostic( + code=code, + message=message, + pointer=pointer, + primary_range=_range_from_node(key_node), + authored_keys=authored_keys, + severity="warning" if self._migration_policy is SDLMigrationPolicy.ACCEPT else "error", + source=self._source, + ) + ) + + def _walk_merge_value( + self, + node: Node, + *, + scope: MappingScope, + tokens: list[str], + active: set[int], + ) -> None: + if isinstance(node, MappingNode): + self._walk(node, scope=scope, tokens=tokens, active=active) + elif isinstance(node, SequenceNode): + for item in node.value: + self._walk(item, scope=scope, tokens=tokens, active=active) + + def _effective_mapping( + self, + node: MappingNode, + *, + scope: MappingScope, + active: set[int], + ) -> _EffectiveMapping: + cache_key = (id(node), scope) + cached = self._effective_cache.get(cache_key) + if cached is not None: + return cached + if id(node) in active: + return _EffectiveMapping((), ()) + + active.add(id(node)) + accumulator = _EffectiveAccumulator() + try: + merge_keys = self._inherit_merge_entries(node, scope=scope, active=active, accumulator=accumulator) + self._record_duplicate_merge_keys(merge_keys, accumulator) + self._add_local_entries(node, scope=scope, accumulator=accumulator) + finally: + active.remove(id(node)) + + result = accumulator.build() + self._effective_cache[cache_key] = result + return result + + def _inherit_merge_entries( + self, + node: MappingNode, + *, + scope: MappingScope, + active: set[int], + accumulator: _EffectiveAccumulator, + ) -> list[ScalarNode]: + merge_keys: list[ScalarNode] = [] + for key_node, value_node in node.value: + if not _is_merge_key(key_node): + continue + assert isinstance(key_node, ScalarNode) + merge_keys.append(key_node) + for source in _merge_sources(value_node): + if id(source) in active: + continue + inherited = self._effective_mapping(source, scope=scope, active=active) + for entry in inherited.entries: + accumulator.add(entry) + return merge_keys + + @staticmethod + def _record_duplicate_merge_keys( + merge_keys: list[ScalarNode], + accumulator: _EffectiveAccumulator, + ) -> None: + if len(merge_keys) < 2: + return + first = _Entry("<<", "<<", merge_keys[0]) + for key_node in merge_keys[1:]: + accumulator.conflicts.append((first, _Entry("<<", "<<", key_node))) + + @staticmethod + def _add_local_entries( + node: MappingNode, + *, + scope: MappingScope, + accumulator: _EffectiveAccumulator, + ) -> None: + for key_node, _value_node in node.value: + if not _is_string_key(key_node) or _is_merge_key(key_node): + continue + assert isinstance(key_node, ScalarNode) + authored = key_node.value + canonical = normalize_field_key(authored) if scope is MappingScope.STRUCTURAL else authored + accumulator.add(_Entry(canonical, authored, key_node)) + + def _add(self, diagnostic: SDLParseDiagnostic) -> None: + related = diagnostic.related_range + key = ( + diagnostic.code, + diagnostic.pointer, + diagnostic.primary_range.start.line, + diagnostic.primary_range.start.column, + related.start.line if related else None, + related.start.column if related else None, + ) + if key not in self._diagnostic_keys: + self._diagnostic_keys.add(key) + self.diagnostics.append(diagnostic) + + +def _is_merge_key(node: Node) -> bool: + return isinstance(node, ScalarNode) and node.tag == _MERGE_TAG + + +def _is_string_key(node: Node) -> bool: + return isinstance(node, ScalarNode) and node.tag == _STRING_TAG + + +def _authored_key(node: Node) -> str: + if isinstance(node, ScalarNode): + return node.value + return "?" + + +def _merge_sources(node: Node) -> Iterator[MappingNode]: + if isinstance(node, MappingNode): + yield node + elif isinstance(node, SequenceNode): + yield from (item for item in node.value if isinstance(item, MappingNode)) + + +def _child_scope(scope: MappingScope, canonical: str, value_node: Node) -> MappingScope: + is_literal = scope is MappingScope.STRUCTURAL and is_literal_map_field( + canonical, + value_is_mapping=isinstance(value_node, MappingNode), + value_is_sequence=isinstance(value_node, SequenceNode), + ) + return MappingScope.LITERAL if is_literal else MappingScope.STRUCTURAL + + +def _conflict_message(first: _Entry, conflicting: _Entry) -> str: + if first.authored == conflicting.authored: + return f"Duplicate mapping key '{conflicting.authored}'." + return ( + f"Structural field keys '{first.authored}' and '{conflicting.authored}' both address '{conflicting.canonical}'." + ) + + +def _range_from_node(node: Node) -> SDLSourceRange: + return SDLSourceRange( + start=SDLSourcePosition(node.start_mark.line + 1, node.start_mark.column + 1), + end=SDLSourcePosition(node.end_mark.line + 1, node.end_mark.column + 1), + ) + + +def _encode_pointer(tokens: list[str]) -> str: + if not tokens: + return "" + return "/" + "/".join(token.replace("~", "~0").replace("/", "~1") for token in tokens) diff --git a/implementations/python/packages/aces_sdl/_mapping_scopes.py b/implementations/python/packages/aces_sdl/_mapping_scopes.py new file mode 100644 index 000000000..c7382d40f --- /dev/null +++ b/implementations/python/packages/aces_sdl/_mapping_scopes.py @@ -0,0 +1,79 @@ +"""Classify SDL mappings whose keys are authored identifiers rather than fields.""" + +from __future__ import annotations + +from enum import Enum + + +class MappingScope(str, Enum): + """Key interpretation for one mapping node.""" + + STRUCTURAL = "structural" + LITERAL = "literal" + + +HASHMAP_SECTIONS = frozenset( + { + "nodes", + "infrastructure", + "features", + "conditions", + "propositions", + "assertions", + "vulnerabilities", + "entities", + "injects", + "events", + "scripts", + "stories", + "content", + "accounts", + "relationships", + "agents", + "action_contracts", + "observation_boundaries", + "outcome_interpretation_rules", + "behavior_specifications", + "evidence_requirements", + "objectives", + "workflows", + "variables", + } +) + +NESTED_HASHMAP_FIELDS = frozenset( + { + "features", + "conditions", + "injects", + "roles", + "log_options", + "labels", + "driver_options", + "ipam_options", + "facts", + "entities", + "events", + "steps", + "extensions", + } +) + + +def normalize_field_key(key: str) -> str: + """Return the canonical spelling of an SDL structural field key.""" + return key.lower().replace("-", "_") + + +def is_literal_map_field( + key: str, + *, + value_is_mapping: bool, + value_is_sequence: bool, +) -> bool: + """Return whether a structural field's immediate child keys are literal.""" + if key in HASHMAP_SECTIONS: + return value_is_mapping + if key in NESTED_HASHMAP_FIELDS: + return True + return key == "properties" and value_is_sequence diff --git a/implementations/python/packages/aces_sdl/_model_diagnostics.py b/implementations/python/packages/aces_sdl/_model_diagnostics.py new file mode 100644 index 000000000..989150d72 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_model_diagnostics.py @@ -0,0 +1,108 @@ +"""Pydantic-error → SDL-diagnostic rendering for the SDL parser. + +Converts a Pydantic :class:`ValidationError` into bounded, source-anchored +:class:`SDLParseError` diagnostics. Kept separate from :mod:`aces_sdl.parser` +so the parser module stays focused on loading/normalization; ``parser`` re-imports +these helpers so ``from aces_sdl.parser import ...`` call sites remain stable. +""" + +from __future__ import annotations + +from pathlib import Path + +from pydantic import ValidationError + +from ._errors import ( + SDLParseDiagnostic, + SDLParseError, + SDLSourcePosition, + SDLSourceRange, +) + + +def _dedupe_source_diagnostics( + diagnostics: list[SDLParseDiagnostic], +) -> list[SDLParseDiagnostic]: + unique: list[SDLParseDiagnostic] = [] + seen: set[tuple[object, ...]] = set() + for diagnostic in diagnostics: + start = diagnostic.primary_range.start + end = diagnostic.primary_range.end + key = ( + diagnostic.source, + diagnostic.code, + diagnostic.pointer, + start.line, + start.column, + end.line, + end.column, + ) + if key not in seen: + seen.add(key) + unique.append(diagnostic) + return unique + + +def _pointer_from_location(location: tuple[object, ...]) -> str: + tokens = [str(part) for part in location if str(part) != "[key]"] + return "".join(f"/{token.replace('~', '~0').replace('/', '~1')}" for token in tokens) + + +def _nearest_source_range(pointer: str, source_ranges: dict[str, SDLSourceRange]) -> SDLSourceRange: + candidate = pointer + while candidate: + source_range = source_ranges.get(candidate) + if source_range is not None: + return source_range + candidate = candidate.rsplit("/", 1)[0] + source_range = source_ranges.get("") + if source_range is not None: + return source_range + position = SDLSourcePosition(1, 1) + return SDLSourceRange(start=position, end=position) + + +_MODEL_DIAGNOSTIC_MESSAGE_MAX_LENGTH = 512 + + +def _bounded_model_message(message: str) -> str: + """Render validator-owned prose without Pydantic's input or traceback.""" + + if message.startswith("Value error, "): + message = message.removeprefix("Value error, ") + escaped = "".join(character if character.isprintable() else f"\\u{ord(character):04x}" for character in message) + if len(escaped) <= _MODEL_DIAGNOSTIC_MESSAGE_MAX_LENGTH: + return escaped + return escaped[: _MODEL_DIAGNOSTIC_MESSAGE_MAX_LENGTH - 3] + "..." + + +def _model_parse_error( + error: ValidationError, + *, + path: Path | None, + source_ranges: dict[str, SDLSourceRange], +) -> SDLParseError: + diagnostics: list[SDLParseDiagnostic] = [] + for item in error.errors(): + pointer = _pointer_from_location(tuple(item.get("loc", ()))) + raw_message = str(item.get("msg", "")) + is_identifier = "portable SDL identifier" in raw_message or "qualified SDL identifier" in raw_message + message = _bounded_model_message(raw_message) + diagnostics.append( + SDLParseDiagnostic( + code="sdl.identifier.invalid" if is_identifier else "sdl.model.invalid", + message=message, + pointer=pointer, + primary_range=_nearest_source_range(pointer, source_ranges), + source=str(path) if path is not None else None, + ) + ) + diagnostics = _dedupe_source_diagnostics(diagnostics) + rendered = "; ".join(f"{diagnostic.pointer or '/'}: {diagnostic.message}" for diagnostic in diagnostics[:8]) + if len(diagnostics) > 8: + rendered += f", and {len(diagnostics) - 8} more" + return SDLParseError( + f"SDL model validation failed at {rendered or '/'}", + path=path, + diagnostics=diagnostics, + ) diff --git a/implementations/python/packages/aces_sdl/_module_provenance.py b/implementations/python/packages/aces_sdl/_module_provenance.py deleted file mode 100644 index fdae65218..000000000 --- a/implementations/python/packages/aces_sdl/_module_provenance.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Helpers for carrying capability-variable provenance across SDL module imports. - -Lives next to ``composition.py`` rather than inside it so the composition file -stays within the repo-policy line cap, and so the provenance helpers can be -unit-tested or reused without pulling in the full module-expansion machinery. -The three helpers here are intentionally narrow: - -* :func:`add_unique_provenance` rejects silent collisions on a shared - provenance accumulator. Two imports that would map to the same generated - private-prefixed key must fail at parse time, not silently shadow each - other, otherwise the runtime planner can validate an imported node against - the wrong ``allowed_values`` domain. -* :func:`dump_variable_spec` serializes the ``aces_sdl.variables.Variable`` - Pydantic model (or a plain-dict equivalent) into a stable shape consumed - downstream by the runtime planner's ``model.variable_specs`` lookup. -* :func:`rename_variable_ref` applies the current composition level's - namespace to a captured variable reference, handling both this level's - local renames and deeper-import refs that already carry their own prefix. -""" - -from __future__ import annotations - -from collections.abc import Mapping -from pathlib import Path -from typing import Any - -from ._errors import SDLParseError - - -def add_unique_provenance( - accumulator: dict[str, Any], - key: str, - value: Any, - *, - kind: str, - source_path: Path, -) -> None: - """Insert a generated provenance entry, rejecting silent collisions. - - The private-prefix namespace (``.__private.``) is reserved for - composition-generated names. Two imports producing the same generated - key, or a deeper-import entry colliding with this level's local entry, - would otherwise let one provenance record silently shadow the other and - cause the planner to validate against the wrong ``allowed_values`` - domain. Raising here surfaces the bad import combination at parse time. - """ - - if key in accumulator: - raise SDLParseError( - ( - f"Module import would silently shadow {kind} '{key}'; " - "rename one of the colliding imports to a distinct namespace." - ), - path=source_path, - ) - accumulator[key] = value - - -def dump_variable_spec(variable: Any) -> dict[str, object]: - """Serialize a Variable Pydantic model to a plain dict for storage.""" - - if hasattr(variable, "model_dump"): - return variable.model_dump(mode="python") - if isinstance(variable, dict): - return dict(variable) - return {"value": variable} - - -def rename_variable_ref( - ref: str | None, - local_var_renames: Mapping[str, str], - namespace: str, - *, - prefix: callable, # type: ignore[valid-type] -) -> str | None: - """Apply this composition level's namespace to a captured variable ref. - - Local imported variables are renamed via ``local_var_renames``. Refs - that are already deeper-prefixed (because they came from a nested - expansion) receive this level's namespace as an additional prefix so - they line up with the prefixed specs in ``module_variable_specs``. - ``prefix`` is the namespace-prefix function from ``composition.py`` - (``_prefix``); accepting it as a parameter avoids a circular import. - """ - - if ref is None: - return None - if ref in local_var_renames: - return local_var_renames[ref] - return prefix(namespace, ref) diff --git a/implementations/python/packages/aces_sdl/_module_symbols.py b/implementations/python/packages/aces_sdl/_module_symbols.py index 9c9fef841..11af02ea7 100644 --- a/implementations/python/packages/aces_sdl/_module_symbols.py +++ b/implementations/python/packages/aces_sdl/_module_symbols.py @@ -3,11 +3,14 @@ from __future__ import annotations from collections.abc import Mapping +from contextlib import suppress from typing import Any +from ._base import is_variable_ref +from ._identifiers import QualifiedName from ._module_runtime_aliases import nested_node_runtime_aliases from .entities import flatten_entities -from .scenario import ModuleDescriptor, Scenario +from .scenario import ModuleDescriptor, ScenarioContent # Canonical list of scenario top-level sections that hold user-defined # hashmap keys. Re-exported as both the public ``HASHMAP_SECTIONS`` name @@ -18,6 +21,8 @@ "infrastructure", "features", "conditions", + "propositions", + "assertions", "vulnerabilities", "entities", "injects", @@ -37,21 +42,39 @@ "workflows", ) _HASHMAP_SECTIONS = HASHMAP_SECTIONS +FORWARDING_AGENTS_SECTION = "forwarding_agents" def _prefix(namespace: str, name: str) -> str: - return f"{namespace}.{name}" if namespace else name + return QualifiedName.parse(name).prefixed(namespace).render() if namespace else QualifiedName.parse(name).render() def _private_prefix(namespace: str, name: str) -> str: - return _prefix(namespace, f"__private.{name}") + return QualifiedName.parse(name).prefixed(namespace, private=True).render() + + +def rewrite_objective_window_ref(ref: str, workflow_names: Mapping[str, str]) -> str: + """Rewrite a workflow-step window reference through a symbol map.""" + + rewritten = ref + parts: tuple[str, ...] = () + if not is_variable_ref(ref): + with suppress(TypeError, ValueError): + parts = QualifiedName.parse(ref).parts + if len(parts) >= 2: + workflow_name = QualifiedName(parts[:-1]).render() + if workflow_name in workflow_names: + rewritten = f"{workflow_names[workflow_name]}.{parts[-1]}" + return rewritten def explicit_exports( - scenario: Scenario, + scenario: ScenarioContent, descriptor: ModuleDescriptor, + *, + restrict_to_descriptor: bool, ) -> dict[str, set[str]]: - if scenario.module is None: + if not restrict_to_descriptor: return { section: set(getattr(scenario, section).keys()) for section in _HASHMAP_SECTIONS @@ -80,7 +103,7 @@ def _qualified_section_aliases(section_name: str, rename_map: Mapping[str, str]) def _nested_node_service_aliases( - scenario: Scenario, + scenario: ScenarioContent, node_rename_map: Mapping[str, str], ) -> dict[str, str]: """Qualified service refs ``nodes..services.``. @@ -103,7 +126,7 @@ def _nested_node_service_aliases( def _nested_content_item_aliases( - scenario: Scenario, + scenario: ScenarioContent, content_rename_map: Mapping[str, str], ) -> dict[str, str]: """Qualified content-item refs ``content.
.items.``.""" @@ -122,13 +145,18 @@ def _nested_content_item_aliases( def symbol_index( - scenario: Scenario, + scenario: ScenarioContent, *, namespace: str, descriptor: ModuleDescriptor, + restrict_to_descriptor: bool = False, ) -> dict[str, dict[str, str] | set[str]]: entities = set(flatten_entities(scenario.entities)) - exported = explicit_exports(scenario, descriptor) + exported = explicit_exports( + scenario, + descriptor, + restrict_to_descriptor=restrict_to_descriptor, + ) named: dict[str, str] = {} section_maps: dict[str, dict[str, str]] = {} for section_name in _HASHMAP_SECTIONS: @@ -156,11 +184,20 @@ def symbol_index( named.update(nested_node_runtime_aliases(scenario, section_maps.get("nodes", {}))) named.update(_nested_content_item_aliases(scenario, section_maps.get("content", {}))) + forwarding_agent_map = _section_rename_map( + {agent.forwarding_agent_id: agent for agent in scenario.forwarding_agents}, + namespace=namespace, + exported_names=exported.get(FORWARDING_AGENTS_SECTION, set()), + ) + named.update(_qualified_section_aliases(FORWARDING_AGENTS_SECTION, forwarding_agent_map)) + return { "nodes": section_maps.get("nodes", {}), "infrastructure": section_maps.get("infrastructure", {}), "features": section_maps.get("features", {}), "conditions": section_maps.get("conditions", {}), + "propositions": section_maps.get("propositions", {}), + "assertions": section_maps.get("assertions", {}), "vulnerabilities": section_maps.get("vulnerabilities", {}), "entities": entity_map, "injects": section_maps.get("injects", {}), @@ -178,8 +215,9 @@ def symbol_index( "evidence_requirements": section_maps.get("evidence_requirements", {}), "objectives": section_maps.get("objectives", {}), "workflows": section_maps.get("workflows", {}), + FORWARDING_AGENTS_SECTION: forwarding_agent_map, "named": named, } -__all__ = ["HASHMAP_SECTIONS", "explicit_exports", "symbol_index"] +__all__ = ["FORWARDING_AGENTS_SECTION", "HASHMAP_SECTIONS", "explicit_exports", "symbol_index"] diff --git a/implementations/python/packages/aces_sdl/_realization_envelope_domains.py b/implementations/python/packages/aces_sdl/_realization_envelope_domains.py new file mode 100644 index 000000000..805de2425 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_realization_envelope_domains.py @@ -0,0 +1,197 @@ +"""Domain-kind dispatch for the realization-envelope relation. + +Per-domain-kind logic — finite enumeration, subset comparison (R4), deterministic +witness selection (R5), and out-of-envelope variation (R6) — dispatched by type so +each public entry point stays flat. Kept separate from the flattening/path engine +so neither file exceeds the repo source-size cap and so the type-dispatch tables +live in one place. Pure functions over the contract types; no SDL, diagnostic, or +parser dependency. +""" + +from __future__ import annotations + +from collections.abc import Callable + +from aces_contracts.realization_envelope import ( + BooleanDomain, + DomainDescriptor, + DomainScalar, + EnumDomain, + ExactDomain, + GovernedReferenceDomain, + NumericIntervalDomain, + NumericType, + scalar_in_domain, +) + +# Sentinel meaning "no out-of-envelope value can be formed within the domain kind". +_MISSING = object() + +WitnessSelection = tuple[object, str | None] + + +# --------------------------------------------------------------------------- # +# Finite enumeration # +# --------------------------------------------------------------------------- # + +_FINITE_MEMBERS: dict[type, Callable[..., list[DomainScalar]]] = { + ExactDomain: lambda d: [d.value], + EnumDomain: lambda d: list(d.values), + BooleanDomain: lambda d: [d.value] if d.value is not None else [False, True], + GovernedReferenceDomain: lambda d: list(d.allowed_refs), +} + + +def finite_members(domain: DomainDescriptor) -> list[DomainScalar] | None: + """Finite value list for a scalar domain, or ``None`` when infinite.""" + + factory = _FINITE_MEMBERS.get(type(domain)) + return factory(domain) if factory is not None else None + + +# --------------------------------------------------------------------------- # +# Subset comparison (R4) # +# --------------------------------------------------------------------------- # + + +def _governed_subset(sub: DomainDescriptor, sup: DomainDescriptor) -> bool: + # Governed references carry an authority that scopes their refs; subsumption + # must not drop it. Only a same-authority governed-reference domain subsumes; + # raw refs from a different kind are never authority-scoped. + if not (isinstance(sub, GovernedReferenceDomain) and isinstance(sup, GovernedReferenceDomain)): + return False + if sub.authority != sup.authority: + return False + return set(sub.allowed_refs) <= set(sup.allowed_refs) + + +def _interval_subset(sub: NumericIntervalDomain, sup: NumericIntervalDomain) -> bool: + if sup.numeric_type is NumericType.INTEGER and sub.numeric_type is NumericType.NUMBER: + return False + lower_ok = sup.lower < sub.lower or (sup.lower == sub.lower and (sup.lower_closed or not sub.lower_closed)) + upper_ok = sup.upper > sub.upper or (sup.upper == sub.upper and (sup.upper_closed or not sub.upper_closed)) + return lower_ok and upper_ok + + +def _numeric_subset(sub: NumericIntervalDomain, sup: DomainDescriptor) -> bool: + if sub.lower == sub.upper: + value: DomainScalar = int(sub.lower) if sub.numeric_type is NumericType.INTEGER else sub.lower + return scalar_in_domain(value, sup) + if isinstance(sup, NumericIntervalDomain): + return _interval_subset(sub, sup) + return False + + +def domain_subset(sub: DomainDescriptor, sup: DomainDescriptor) -> bool: + """Return whether every value admitted by ``sub`` is admitted by ``sup``.""" + + if isinstance(sub, GovernedReferenceDomain) or isinstance(sup, GovernedReferenceDomain): + return _governed_subset(sub, sup) + if isinstance(sub, NumericIntervalDomain): + return _numeric_subset(sub, sup) + members = finite_members(sub) + return members is not None and all(scalar_in_domain(candidate, sup) for candidate in members) + + +# --------------------------------------------------------------------------- # +# Deterministic witness selection (R5) # +# --------------------------------------------------------------------------- # + + +def _enum_sort_key(value: DomainScalar) -> tuple[int, str]: + if isinstance(value, bool): + kind = 0 + elif isinstance(value, (int, float)): + kind = 1 + else: + kind = 2 + return (kind, str(value)) + + +def _integer_witness_value(domain: NumericIntervalDomain) -> WitnessSelection: + lower = int(domain.lower) + upper = int(domain.upper) + candidate = lower if domain.lower_closed else lower + 1 + admissible = candidate < upper or (candidate == upper and domain.upper_closed) + return (candidate, None) if admissible else (None, "integer interval admits no witness value") + + +def _real_witness_value(domain: NumericIntervalDomain) -> WitnessSelection: + # A closed lower bound is itself admissible; an open lower bound on a + # non-degenerate interval (open degenerate intervals are forbidden by the + # contract) admits the interior midpoint. + if domain.lower_closed: + return domain.lower, None + return (domain.lower + domain.upper) / 2, None + + +def _numeric_witness_value(domain: NumericIntervalDomain) -> WitnessSelection: + if domain.numeric_type is NumericType.INTEGER: + return _integer_witness_value(domain) + return _real_witness_value(domain) + + +_WITNESS_SELECTORS: dict[type, Callable[..., WitnessSelection]] = { + ExactDomain: lambda d: (d.value, None), + EnumDomain: lambda d: (min(d.values, key=_enum_sort_key), None), + BooleanDomain: lambda d: (d.value if d.value is not None else False, None), + GovernedReferenceDomain: lambda d: (min(d.allowed_refs), None), + NumericIntervalDomain: _numeric_witness_value, +} + + +def default_witness_value(domain: DomainDescriptor) -> WitnessSelection: + """Deterministic default selection for a scalar domain (R5).""" + + selector = _WITNESS_SELECTORS.get(type(domain)) + if selector is None: + return None, "record domains are not scalar witness leaves" + return selector(domain) + + +# --------------------------------------------------------------------------- # +# Out-of-envelope variation (R6) # +# --------------------------------------------------------------------------- # + + +def _perturb_scalar(value: DomainScalar) -> DomainScalar: + if isinstance(value, bool): + return not value + if isinstance(value, (int, float)): + return value + 1 + return f"{value}-out-of-envelope" + + +def _fresh_scalar_outside(members: list[DomainScalar]) -> object: + if all(isinstance(member, bool) for member in members): + return _MISSING + numeric = [m for m in members if isinstance(m, (int, float)) and not isinstance(m, bool)] + if numeric and len(numeric) == len(members): + return max(numeric) + 1 + candidate = "out-of-envelope" + existing = {str(member) for member in members} + while candidate in existing: + candidate += "-x" + return candidate + + +def _interval_out_of_domain(domain: NumericIntervalDomain) -> object: + step: int | float = 1 if domain.numeric_type is NumericType.INTEGER else 1.0 + upper: int | float = int(domain.upper) if domain.numeric_type is NumericType.INTEGER else domain.upper + return upper + step if domain.upper_closed else upper + + +_OUT_OF_DOMAIN: dict[type, Callable[..., object]] = { + ExactDomain: lambda d: _perturb_scalar(d.value), + EnumDomain: lambda d: _fresh_scalar_outside(list(d.values)), + BooleanDomain: lambda d: _MISSING if d.value is None else (not d.value), + GovernedReferenceDomain: lambda d: _fresh_scalar_outside(list(d.allowed_refs)), + NumericIntervalDomain: _interval_out_of_domain, +} + + +def out_of_domain_value(domain: DomainDescriptor) -> object: + """A scalar just outside ``domain``, or ``_MISSING`` when none can be formed.""" + + factory = _OUT_OF_DOMAIN.get(type(domain)) + return factory(domain) if factory is not None else _MISSING diff --git a/implementations/python/packages/aces_sdl/_realization_envelope_engine.py b/implementations/python/packages/aces_sdl/_realization_envelope_engine.py new file mode 100644 index 000000000..b7e916195 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_realization_envelope_engine.py @@ -0,0 +1,364 @@ +"""Internal engine for the realization-envelope relation. + +SDL-path navigation, most-specific-wins constraint flattening (R2), and closure +bookkeeping shared by the four relation kinds in +:mod:`aces_sdl.realization_envelope`. Domain-kind dispatch (subset, witness, +variation) lives in :mod:`aces_sdl._realization_envelope_domains`. Kept separate +from the public relation module so neither file exceeds the repo source-size cap. +This module has no diagnostic, parser, or validator dependency; those live in the +public module. +""" + +from __future__ import annotations + +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from enum import Enum + +from aces_contracts.realization_envelope import ( + Closure, + DomainDescriptor, + EnvelopeBinding, + Posture, + RealizationEnvelopeModel, + RecordDomain, + WitnessPolicy, +) +from pydantic import BaseModel + +from ._realization_envelope_domains import default_witness_value, domain_subset + +PathToken = str | int + + +@dataclass(frozen=True) +class LeafConstraint: + """A flattened scalar constraint at a concrete SDL path.""" + + domain: DomainDescriptor + domain_name: str + posture: Posture + overrideable: bool + + +# --------------------------------------------------------------------------- # +# Path handling # +# --------------------------------------------------------------------------- # + +_PATH_TOKEN_RE = re.compile(r"[^.\[\]]+|\[\d+\]") + + +def tokenize_path(path: str) -> list[PathToken]: + """Split an SDL path into attribute/key segments and ``[i]`` list indices.""" + + tokens: list[PathToken] = [] + for raw in _PATH_TOKEN_RE.findall(path): + tokens.append(int(raw[1:-1]) if raw.startswith("[") else raw) + return tokens + + +def _navigate_step(current: object, token: PathToken) -> tuple[bool, object]: + result: tuple[bool, object] = (False, None) + if isinstance(token, int): + if isinstance(current, Sequence) and not isinstance(current, (str, bytes)) and 0 <= token < len(current): + result = (True, current[token]) + elif isinstance(current, BaseModel) and token in type(current).model_fields: + result = (True, getattr(current, token)) + elif isinstance(current, Mapping) and token in current: + result = (True, current[token]) + return result + + +def navigate(root: object, tokens: Sequence[PathToken]) -> tuple[bool, object]: + """Resolve ``tokens`` against a model/mapping/sequence tree.""" + + current: object = root + for token in tokens: + found, current = _navigate_step(current, token) + if not found: + return False, None + return True, current + + +def normalize_scalar(value: object) -> object: + """Reduce an SDL model value to a portable scalar for domain comparison. + + SDL string enums (e.g. ``OSFamily``, ``NodeType``) navigate out as ``Enum`` + instances; envelope domains carry plain JSON scalars. Comparing them requires + the enum's underlying value. ``bool`` is preserved (it is its own domain kind). + """ + + return value.value if isinstance(value, Enum) else value + + +def _is_nonempty(value: object) -> bool: + if value is None: + return False + return not (isinstance(value, (dict, list, tuple, set)) and len(value) == 0) + + +def present_children(value: object) -> set[str]: + """Immediate realizable child keys carrying a value under ``value``. + + A child is a *present realizable dimension* when it is non-empty and either a + required field or set to something other than its declared default. This is + robust to the instantiation round-trip (``model_dump`` → ``model_validate``), + which marks every field as set and so makes ``model_fields_set`` unusable: + default identity scalars (e.g. ``version="*"``) are not realizable dimensions + and must not count as closed-world extras (envelope-semantics.md I3). + """ + + if isinstance(value, BaseModel): + return _model_present_children(value) + if isinstance(value, Mapping): + return {str(key) for key in value} + return set() + + +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: + continue + child = getattr(model, name) + if not _is_nonempty(child): + continue + if info.is_required() or child != info.get_default(call_default_factory=True): + present.add(name) + return present + + +# --------------------------------------------------------------------------- # +# Effective constraints (most-specific-wins flattening, R2) # +# --------------------------------------------------------------------------- # + + +def _expand_constraint( + path: str, + domain: DomainDescriptor, + domain_name: str, + posture: Posture, + overrideable: bool, + envelope: RealizationEnvelopeModel, + constraints: dict[str, LeafConstraint], +) -> None: + if isinstance(domain, RecordDomain): + for field_name, referenced in domain.fields.items(): + _expand_constraint( + f"{path}.{field_name}", + envelope.domains[referenced], + referenced, + posture, + overrideable, + envelope, + constraints, + ) + return + constraints[path] = LeafConstraint( + domain=domain, domain_name=domain_name, posture=posture, overrideable=overrideable + ) + + +def _collect_record_closures( + path: str, + domain: DomainDescriptor, + envelope: RealizationEnvelopeModel, + closed: dict[str, set[str]], +) -> None: + if not isinstance(domain, RecordDomain): + return + if not domain.extra: + closed[path] = set(domain.fields) + for field_name, referenced in domain.fields.items(): + _collect_record_closures(f"{path}.{field_name}", envelope.domains[referenced], envelope, closed) + + +def _record_closed_scopes(envelope: RealizationEnvelopeModel) -> dict[str, set[str]]: + """Closed record domains contribute an admitted-child-key set at their path.""" + + closed: dict[str, set[str]] = {} + for binding in envelope.bindings: + if binding.domain is not None: + _collect_record_closures(binding.path, envelope.domains[binding.domain], envelope, closed) + return closed + + +def _prefixed_paths(constraints: dict[str, LeafConstraint], prefix: str) -> list[str]: + return [path for path in constraints if path == prefix or path.startswith(prefix + ".")] + + +def _apply_binding( + binding: EnvelopeBinding, + envelope: RealizationEnvelopeModel, + constraints: dict[str, LeafConstraint], +) -> None: + if binding.posture is Posture.OPEN: + for path in _prefixed_paths(constraints, binding.path): + del constraints[path] + return + if binding.domain is None: + return + _expand_constraint( + binding.path, + envelope.domains[binding.domain], + binding.domain, + binding.posture, + binding.overrideable, + envelope, + constraints, + ) + + +def _add_admitted_children(envelope: RealizationEnvelopeModel, scope_path: str, admitted: set[str]) -> None: + prefix = scope_path + "." if scope_path else "" + for binding in envelope.bindings: + if not binding.path.startswith(prefix): + continue + remainder = binding.path[len(prefix) :] + tokens = tokenize_path(remainder) if remainder else [] + if tokens and isinstance(tokens[0], str): + admitted.add(tokens[0]) + + +def effective_constraints( + envelope: RealizationEnvelopeModel, +) -> tuple[dict[str, LeafConstraint], dict[str, set[str]]]: + """Flatten bindings into per-path scalar constraints and closed-scope key sets. + + Most-specific-wins (R2) is realized by processing bindings shortest-path-first + so a more-specific explicit binding overwrites a record expansion, and an + ``open`` binding removes any constraint at (and under) its path. + """ + + constraints: dict[str, LeafConstraint] = {} + for binding in sorted(envelope.bindings, key=lambda b: len(tokenize_path(b.path))): + _apply_binding(binding, envelope, constraints) + + closed = _record_closed_scopes(envelope) + for overlay in envelope.closure: + if overlay.closure is Closure.CLOSED_WORLD: + _add_admitted_children(envelope, overlay.path, closed.setdefault(overlay.path, set())) + return constraints, closed + + +# --------------------------------------------------------------------------- # +# Overridability (R2 well-formedness) # +# --------------------------------------------------------------------------- # + + +def _record_open_widenings(constraints: dict[str, LeafConstraint], prefix: str, violations: list[str]) -> None: + for path in _prefixed_paths(constraints, prefix): + if not constraints[path].overrideable: + # open posture widens a fixed inherited value + violations.append(path) + del constraints[path] + + +def _record_overwrite_widenings( + binding: EnvelopeBinding, + envelope: RealizationEnvelopeModel, + constraints: dict[str, LeafConstraint], + violations: list[str], +) -> None: + new_leaves: dict[str, LeafConstraint] = {} + _expand_constraint( + binding.path, + envelope.domains[binding.domain], + binding.domain, + binding.posture, + binding.overrideable, + envelope, + new_leaves, + ) + for path, new_constraint in new_leaves.items(): + existing = constraints.get(path) + # A narrowing (subset) overwrite is allowed; a non-subset overwrite of a + # non-overrideable inherited value is a forbidden widening. + if ( + existing is not None + and not existing.overrideable + and not domain_subset(new_constraint.domain, existing.domain) + ): + violations.append(path) + constraints[path] = new_constraint + + +def overridability_violations(envelope: RealizationEnvelopeModel) -> list[str]: + """Paths where a more-specific binding illegally widens a fixed inherited value. + + Envelope-semantics.md R2: a more-specific binding may not widen a value an + enclosing scope fixed (or excluded) unless the enclosing binding marked that + child ``overrideable``. Replays the same shortest-path-first order as + :func:`effective_constraints`. An empty list means the envelope is well-formed. + """ + + violations: list[str] = [] + constraints: dict[str, LeafConstraint] = {} + for binding in sorted(envelope.bindings, key=lambda b: len(tokenize_path(b.path))): + if binding.posture is Posture.OPEN: + _record_open_widenings(constraints, binding.path, violations) + elif binding.domain is not None: + _record_overwrite_widenings(binding, envelope, constraints, violations) + return violations + + +# --------------------------------------------------------------------------- # +# Witness assembly helpers # +# --------------------------------------------------------------------------- # + + +def witness_value(constraint: LeafConstraint, policy: WitnessPolicy | None) -> tuple[object, str | None]: + """Selected witness value for one leaf, honoring an explicit policy override (R5).""" + + if policy is not None and constraint.domain_name in policy.selections: + return policy.selections[constraint.domain_name], None + return default_witness_value(constraint.domain) + + +def assign_path(payload: dict[str, object], tokens: Sequence[PathToken], value: object) -> str | None: + """Set ``value`` at ``tokens`` in a nested dict payload. + + Returns an error string if the path uses list indices, which witness assembly + does not support (SDL sections are keyed mappings). + """ + + current = payload + for token in tokens[:-1]: + if isinstance(token, int): + return "list-indexed paths are not supported for witness generation" + nested = current.get(token) + if not isinstance(nested, dict): + nested = {} + current[token] = nested + current = nested + last = tokens[-1] + if isinstance(last, int): + return "list-indexed paths are not supported for witness generation" + current[last] = value + return None + + +def remove_path(payload: dict[str, object], tokens: Sequence[PathToken]) -> bool: + """Delete the leaf at ``tokens`` from a nested dict payload; return whether removed.""" + + current: object = payload + for token in tokens[:-1]: + if not isinstance(current, dict) or not isinstance(token, str) or token not in current: + return False + current = current[token] + last = tokens[-1] + if isinstance(current, dict) and isinstance(last, str) and last in current: + del current[last] + return True + return False + + +def fresh_extra_key(admitted: set[str]) -> str: + """A child key not in ``admitted`` (for closed-scope extra-dimension probes).""" + + candidate = "out_of_envelope" + while candidate in admitted: + candidate += "_x" + return candidate diff --git a/implementations/python/packages/aces_sdl/_reference_targetability.py b/implementations/python/packages/aces_sdl/_reference_targetability.py new file mode 100644 index 000000000..2a5e87558 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_reference_targetability.py @@ -0,0 +1,11 @@ +"""Canonical policy for SDL declarations accepted by targetable references.""" + +from __future__ import annotations + +NON_TARGETABLE_REFERENCE_SECTIONS = frozenset({"variables", "evidence_requirements", "objectives", "workflows"}) + + +def is_targetable_section(section: str) -> bool: + """Return whether top-level declarations in a section are targetable.""" + + return section not in NON_TARGETABLE_REFERENCE_SECTIONS diff --git a/implementations/python/packages/aces_sdl/_runtime_service_families.py b/implementations/python/packages/aces_sdl/_runtime_service_families.py index b8942b970..cfb848ac3 100644 --- a/implementations/python/packages/aces_sdl/_runtime_service_families.py +++ b/implementations/python/packages/aces_sdl/_runtime_service_families.py @@ -4,244 +4,24 @@ from collections.abc import Iterable, Mapping, MutableMapping from dataclasses import dataclass -from types import ModuleType -from typing import Any - -from . import runtime_app_authorization as _runtime_app_authorization -from . import runtime_application as _runtime_application -from . import runtime_database as _runtime_database -from . import runtime_datastore as _runtime_datastore -from . import runtime_directory_identity as _runtime_directory_identity -from . import runtime_dns as _runtime_dns -from . import runtime_file_service as _runtime_file_service -from . import runtime_forwarding_agent as _runtime_forwarding_agent -from . import runtime_listeners as _runtime_listeners -from . import runtime_mail_service as _runtime_mail_service -from . import runtime_network_detection as _runtime_network_detection -from . import runtime_network_sensor as _runtime_network_sensor -from . import runtime_orchestration as _runtime_orchestration -from . import runtime_platform_application as _runtime_platform_application -from . import runtime_scheduled_job as _runtime_scheduled_job -from . import runtime_security_monitoring as _runtime_security_monitoring -from . import runtime_ssh_server as _runtime_ssh_server - -@dataclass(frozen=True) -class RuntimeReferenceChild: - """A stable child collection that can be addressed below a runtime family.""" - - collection_name: str - id_field: str - children: tuple[RuntimeReferenceChild, ...] = () +from ._runtime_service_family_registry import ( + RUNTIME_SERVICE_FAMILIES, + RuntimeReferenceChild, + RuntimeServiceFamily, +) @dataclass(frozen=True) -class RuntimeServiceFamily: - """Static registration metadata for one node-scoped runtime family.""" - - key: str - module: ModuleType - collection_name: str - id_field: str - child_refs: tuple[RuntimeReferenceChild, ...] = () - - @property - def public_symbols(self) -> tuple[str, ...]: - return tuple(getattr(self.module, "__all__", ())) - - -RUNTIME_SERVICE_FAMILIES: tuple[RuntimeServiceFamily, ...] = ( - RuntimeServiceFamily( - key="service-listeners", - module=_runtime_listeners, - collection_name="service_listeners", - id_field="service_listener_id", - ), - RuntimeServiceFamily( - key="applications", - module=_runtime_application, - collection_name="applications", - id_field="application_id", - ), - RuntimeServiceFamily( - key="database-services", - module=_runtime_database, - collection_name="database_services", - id_field="database_service_id", - child_refs=(RuntimeReferenceChild("databases", "database_id"),), - ), - RuntimeServiceFamily( - key="dns-services", - module=_runtime_dns, - collection_name="dns_services", - id_field="dns_service_id", - child_refs=( - RuntimeReferenceChild( - "zones", - "zone_id", - children=(RuntimeReferenceChild("rrsets", "rrset_id"),), - ), - ), - ), - RuntimeServiceFamily( - key="identity-authorities", - module=_runtime_directory_identity, - collection_name="identity_authorities", - id_field="identity_authority_id", - child_refs=( - RuntimeReferenceChild("services", "service_id"), - RuntimeReferenceChild("subjects", "subject_id"), - RuntimeReferenceChild("policies", "policy_id"), - RuntimeReferenceChild("relationships", "relationship_id"), - ), - ), - RuntimeServiceFamily( - key="file-services", - module=_runtime_file_service, - collection_name="file_services", - id_field="file_service_id", - child_refs=( - RuntimeReferenceChild("shares", "share_id"), - RuntimeReferenceChild("principals", "principal_id"), - RuntimeReferenceChild("access_rules", "rule_id"), - RuntimeReferenceChild("access_observations", "observation_id"), - ), - ), - RuntimeServiceFamily( - key="mail-services", - module=_runtime_mail_service, - collection_name="mail_services", - id_field="mail_service_id", - child_refs=( - RuntimeReferenceChild("components", "component_id"), - RuntimeReferenceChild("listeners", "listener_id"), - RuntimeReferenceChild("domains", "domain_id"), - RuntimeReferenceChild("mailbox_stores", "store_id"), - RuntimeReferenceChild("mailboxes", "mailbox_id"), - RuntimeReferenceChild("aliases", "alias_id"), - RuntimeReferenceChild("routing_rules", "rule_id"), - RuntimeReferenceChild("queues", "queue_id"), - RuntimeReferenceChild("settings", "setting_id"), - ), - ), - RuntimeServiceFamily( - key="network-sensors", - module=_runtime_network_sensor, - collection_name="network_sensors", - id_field="network_sensor_id", - ), - RuntimeServiceFamily( - key="network-detection-engines", - module=_runtime_network_detection, - collection_name="network_detection_engines", - id_field="network_detection_engine_id", - child_refs=( - RuntimeReferenceChild("rule_sources", "source_id"), - RuntimeReferenceChild("network_sets", "set_id"), - RuntimeReferenceChild("output_streams", "stream_id"), - RuntimeReferenceChild("control_channels", "channel_id"), - ), - ), - RuntimeServiceFamily( - key="security-monitoring-managers", - module=_runtime_security_monitoring, - collection_name="security_monitoring_managers", - id_field="security_monitoring_manager_id", - child_refs=( - RuntimeReferenceChild("listeners", "listener_id"), - RuntimeReferenceChild("components", "component_id"), - RuntimeReferenceChild("agents", "agent_id"), - RuntimeReferenceChild("agent_groups", "group_id"), - RuntimeReferenceChild("content_sets", "content_id"), - RuntimeReferenceChild("detection_definitions", "definition_id"), - RuntimeReferenceChild("settings", "setting_id"), - ), - ), - RuntimeServiceFamily( - key="ssh-servers", - module=_runtime_ssh_server, - collection_name="ssh_servers", - id_field="ssh_server_id", - child_refs=(RuntimeReferenceChild("match_rules", "match_id"),), - ), - RuntimeServiceFamily( - key="app-authorizations", - module=_runtime_app_authorization, - collection_name="app_authorizations", - id_field="app_authorization_id", - child_refs=( - RuntimeReferenceChild("principals", "principal_id"), - RuntimeReferenceChild("roles", "role_id"), - RuntimeReferenceChild("permission_grants", "grant_id"), - RuntimeReferenceChild("role_mappings", "mapping_id"), - RuntimeReferenceChild("tenants", "tenant_id"), - ), - ), - RuntimeServiceFamily( - key="scheduled-jobs", - module=_runtime_scheduled_job, - collection_name="scheduled_jobs", - id_field="scheduled_job_id", - ), - RuntimeServiceFamily( - key="datastore-services", - module=_runtime_datastore, - collection_name="datastore_services", - id_field="datastore_service_id", - child_refs=( - RuntimeReferenceChild( - "nodes", - "node_id", - children=( - RuntimeReferenceChild("plugins", "plugin_id"), - RuntimeReferenceChild("endpoints", "endpoint_id"), - ), - ), - RuntimeReferenceChild("partitions", "partition_id"), - RuntimeReferenceChild("templates", "template_id"), - RuntimeReferenceChild("mappings", "mapping_id"), - RuntimeReferenceChild("settings", "setting_id"), - ), - ), - RuntimeServiceFamily( - key="platform-applications", - module=_runtime_platform_application, - collection_name="platform_applications", - id_field="platform_application_id", - child_refs=( - RuntimeReferenceChild("organizations", "organization_id"), - RuntimeReferenceChild("tenants", "tenant_id"), - RuntimeReferenceChild("content_objects", "content_object_id"), - RuntimeReferenceChild("markings", "marking_id"), - RuntimeReferenceChild("upstream_bindings", "binding_id"), - RuntimeReferenceChild("connectors", "connector_id"), - RuntimeReferenceChild("settings", "setting_id"), - ), - ), - RuntimeServiceFamily( - key="forwarding-agents", - module=_runtime_forwarding_agent, - collection_name="forwarding_agents", - id_field="forwarding_agent_id", - child_refs=( - RuntimeReferenceChild("sources", "source_id"), - RuntimeReferenceChild("transforms", "transform_id"), - RuntimeReferenceChild("ship_targets", "target_id"), - RuntimeReferenceChild("reload_channels", "reload_channel_id"), - RuntimeReferenceChild("settings", "setting_id"), - ), - ), - RuntimeServiceFamily( - key="orchestration-authorities", - module=_runtime_orchestration, - collection_name="orchestration_authorities", - id_field="orchestration_authority_id", - child_refs=( - RuntimeReferenceChild("spawn_templates", "template_id"), - RuntimeReferenceChild("realized_children", "workload_id"), - ), - ), -) +class RuntimeFamilyReference: + """One exact qualified address in a node runtime inventory.""" + + address: str + node_name: str + family: RuntimeServiceFamily + item: object + owning_item: object + collection_path: tuple[str, ...] = () def runtime_service_family_export_names() -> tuple[str, ...]: @@ -262,18 +42,18 @@ def runtime_service_family_export_names() -> tuple[str, ...]: return tuple(names) -def runtime_service_family_exports() -> dict[str, Any]: +def runtime_service_family_exports() -> dict[str, object]: """Return the public model symbols exported by all registered families.""" runtime_service_family_export_names() - exports: dict[str, Any] = {} + exports: dict[str, object] = {} for family in RUNTIME_SERVICE_FAMILIES: for name in family.public_symbols: exports[name] = getattr(family.module, name) return exports -def install_runtime_service_family_exports(namespace: MutableMapping[str, Any]) -> tuple[str, ...]: +def install_runtime_service_family_exports(namespace: MutableMapping[str, object]) -> tuple[str, ...]: """Install family symbols into a facade module namespace.""" exports = runtime_service_family_exports() @@ -284,22 +64,85 @@ def install_runtime_service_family_exports(namespace: MutableMapping[str, Any]) def collect_qualified_runtime_family_refs( - scenario: Any, + scenario: object, *, family_keys: Iterable[str] | None = None, ) -> set[str]: """Return targetable qualified refs for all registered runtime families.""" - refs: set[str] = set() + return {reference.address for reference in iter_runtime_family_references(scenario, family_keys=family_keys)} + + +def iter_runtime_family_references( + scenario: object, + *, + family_keys: Iterable[str] | None = None, +) -> Iterable[RuntimeFamilyReference]: + """Yield registered runtime declarations without decoding rendered addresses.""" + selected = _selected_family_keys(family_keys) for node_name, _prefixed_node, runtime in _runtime_instances(scenario, {}): for family in _families(selected): - refs.update(_runtime_family_refs(node_name=node_name, runtime=runtime, family=family)) - return refs + for item in getattr(runtime, family.collection_name, []): + item_id = getattr(item, family.id_field, "") + if not item_id: + continue + base = f"nodes.{node_name}.runtime.{family.collection_name}.{item_id}" + yield RuntimeFamilyReference( + address=base, + node_name=node_name, + family=family, + item=item, + owning_item=item, + ) + yield from _iter_child_references( + item, + base=base, + node_name=node_name, + family=family, + owning_item=item, + collection_path=(), + child_specs=family.child_refs, + ) + + +def _iter_child_references( + item: object, + *, + base: str, + node_name: str, + family: RuntimeServiceFamily, + owning_item: object, + collection_path: tuple[str, ...], + child_specs: tuple[RuntimeReferenceChild, ...], +) -> Iterable[RuntimeFamilyReference]: + for child_spec in child_specs: + for child in getattr(item, child_spec.collection_name, []): + child_id = getattr(child, child_spec.id_field, "") + if not child_id: + continue + child_base = f"{base}.{child_spec.collection_name}.{child_id}" + yield RuntimeFamilyReference( + address=child_base, + node_name=node_name, + family=family, + item=child, + owning_item=owning_item, + collection_path=(*collection_path, child_spec.collection_name), + ) + yield from _iter_child_references( + child, + base=child_base, + node_name=node_name, + family=family, + owning_item=owning_item, + collection_path=(*collection_path, child_spec.collection_name), + child_specs=child_spec.children, + ) def nested_node_runtime_family_aliases( - scenario: Any, + scenario: object, node_rename_map: Mapping[str, str], *, family_keys: Iterable[str] | None = None, @@ -336,17 +179,22 @@ def _families(selected: set[str] | None) -> Iterable[RuntimeServiceFamily]: def _runtime_instances( - scenario: Any, + scenario: object, node_rename_map: Mapping[str, str], -) -> Iterable[tuple[str, str, Any]]: - for node_name, node in scenario.nodes.items(): +) -> Iterable[tuple[str, str, object]]: + nodes = getattr(scenario, "nodes", {}) + if not isinstance(nodes, Mapping): + return + for node_name, node in nodes.items(): + if not isinstance(node_name, str): + continue prefixed_node = node_rename_map.get(node_name, node_name) runtime = getattr(node, "runtime", None) if runtime is not None: yield node_name, prefixed_node, runtime -def _runtime_family_refs(*, node_name: str, runtime: Any, family: RuntimeServiceFamily) -> set[str]: +def _runtime_family_refs(*, node_name: str, runtime: object, family: RuntimeServiceFamily) -> set[str]: refs: set[str] = set() for item in getattr(runtime, family.collection_name, []): item_id = getattr(item, family.id_field, "") @@ -358,7 +206,7 @@ def _runtime_family_refs(*, node_name: str, runtime: Any, family: RuntimeService return refs -def _child_refs(item: Any, base: str, child_specs: tuple[RuntimeReferenceChild, ...]) -> set[str]: +def _child_refs(item: object, base: str, child_specs: tuple[RuntimeReferenceChild, ...]) -> set[str]: refs: set[str] = set() for child_spec in child_specs: for child in getattr(item, child_spec.collection_name, []): @@ -375,7 +223,7 @@ def _runtime_family_aliases( *, node_name: str, prefixed_node: str, - runtime: Any, + runtime: object, family: RuntimeServiceFamily, ) -> dict[str, str]: aliases: dict[str, str] = {} @@ -391,7 +239,7 @@ def _runtime_family_aliases( def _child_aliases( - item: Any, + item: object, bare_base: str, prefixed_base: str, child_specs: tuple[RuntimeReferenceChild, ...], @@ -411,10 +259,12 @@ def _child_aliases( __all__ = [ "RUNTIME_SERVICE_FAMILIES", + "RuntimeFamilyReference", "RuntimeReferenceChild", "RuntimeServiceFamily", "collect_qualified_runtime_family_refs", "install_runtime_service_family_exports", + "iter_runtime_family_references", "nested_node_runtime_family_aliases", "runtime_service_family_export_names", "runtime_service_family_exports", diff --git a/implementations/python/packages/aces_sdl/_runtime_service_family_registry.py b/implementations/python/packages/aces_sdl/_runtime_service_family_registry.py new file mode 100644 index 000000000..d0801a9c8 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_runtime_service_family_registry.py @@ -0,0 +1,256 @@ +"""Static registry of node-scoped runtime service families. + +This module holds the pure registration data — the :class:`RuntimeServiceFamily` +and :class:`RuntimeReferenceChild` dataclasses plus the canonical +``RUNTIME_SERVICE_FAMILIES`` table. The traversal/alias logic that consumes the +registry lives in :mod:`aces_sdl._runtime_service_families`, which re-exports +these names so existing import paths remain stable. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from types import ModuleType + +from . import runtime_app_authorization as _runtime_app_authorization +from . import runtime_application as _runtime_application +from . import runtime_database as _runtime_database +from . import runtime_datastore as _runtime_datastore +from . import runtime_directory_identity as _runtime_directory_identity +from . import runtime_dns as _runtime_dns +from . import runtime_file_service as _runtime_file_service +from . import runtime_forwarding_agent as _runtime_forwarding_agent +from . import runtime_listeners as _runtime_listeners +from . import runtime_mail_service as _runtime_mail_service +from . import runtime_network_detection as _runtime_network_detection +from . import runtime_network_sensor as _runtime_network_sensor +from . import runtime_orchestration as _runtime_orchestration +from . import runtime_platform_application as _runtime_platform_application +from . import runtime_scheduled_job as _runtime_scheduled_job +from . import runtime_security_monitoring as _runtime_security_monitoring +from . import runtime_ssh_server as _runtime_ssh_server + + +@dataclass(frozen=True) +class RuntimeReferenceChild: + """A stable child collection that can be addressed below a runtime family.""" + + collection_name: str + id_field: str + children: tuple[RuntimeReferenceChild, ...] = () + + +@dataclass(frozen=True) +class RuntimeServiceFamily: + """Static registration metadata for one node-scoped runtime family.""" + + key: str + module: ModuleType + collection_name: str + id_field: str + child_refs: tuple[RuntimeReferenceChild, ...] = () + + @property + def public_symbols(self) -> tuple[str, ...]: + return tuple(getattr(self.module, "__all__", ())) + + +RUNTIME_SERVICE_FAMILIES: tuple[RuntimeServiceFamily, ...] = ( + RuntimeServiceFamily( + key="service-listeners", + module=_runtime_listeners, + collection_name="service_listeners", + id_field="service_listener_id", + ), + RuntimeServiceFamily( + key="applications", + module=_runtime_application, + collection_name="applications", + id_field="application_id", + ), + RuntimeServiceFamily( + key="database-services", + module=_runtime_database, + collection_name="database_services", + id_field="database_service_id", + child_refs=(RuntimeReferenceChild("databases", "database_id"),), + ), + RuntimeServiceFamily( + key="dns-services", + module=_runtime_dns, + collection_name="dns_services", + id_field="dns_service_id", + child_refs=( + RuntimeReferenceChild( + "zones", + "zone_id", + children=(RuntimeReferenceChild("rrsets", "rrset_id"),), + ), + ), + ), + RuntimeServiceFamily( + key="identity-authorities", + module=_runtime_directory_identity, + collection_name="identity_authorities", + id_field="identity_authority_id", + child_refs=( + RuntimeReferenceChild("services", "service_id"), + RuntimeReferenceChild("subjects", "subject_id"), + RuntimeReferenceChild("policies", "policy_id"), + RuntimeReferenceChild("relationships", "relationship_id"), + ), + ), + RuntimeServiceFamily( + key="file-services", + module=_runtime_file_service, + collection_name="file_services", + id_field="file_service_id", + child_refs=( + RuntimeReferenceChild("shares", "share_id"), + RuntimeReferenceChild("principals", "principal_id"), + RuntimeReferenceChild("access_rules", "rule_id"), + RuntimeReferenceChild("access_observations", "observation_id"), + ), + ), + RuntimeServiceFamily( + key="mail-services", + module=_runtime_mail_service, + collection_name="mail_services", + id_field="mail_service_id", + child_refs=( + RuntimeReferenceChild("components", "component_id"), + RuntimeReferenceChild("listeners", "listener_id"), + RuntimeReferenceChild("domains", "domain_id"), + RuntimeReferenceChild("mailbox_stores", "store_id"), + RuntimeReferenceChild("mailboxes", "mailbox_id"), + RuntimeReferenceChild("aliases", "alias_id"), + RuntimeReferenceChild("routing_rules", "rule_id"), + RuntimeReferenceChild("queues", "queue_id"), + RuntimeReferenceChild("settings", "setting_id"), + ), + ), + RuntimeServiceFamily( + key="network-sensors", + module=_runtime_network_sensor, + collection_name="network_sensors", + id_field="network_sensor_id", + ), + RuntimeServiceFamily( + key="network-detection-engines", + module=_runtime_network_detection, + collection_name="network_detection_engines", + id_field="network_detection_engine_id", + child_refs=( + RuntimeReferenceChild("rule_sources", "source_id"), + RuntimeReferenceChild("network_sets", "set_id"), + RuntimeReferenceChild("output_streams", "stream_id"), + RuntimeReferenceChild("control_channels", "channel_id"), + ), + ), + RuntimeServiceFamily( + key="security-monitoring-managers", + module=_runtime_security_monitoring, + collection_name="security_monitoring_managers", + id_field="security_monitoring_manager_id", + child_refs=( + RuntimeReferenceChild("listeners", "listener_id"), + RuntimeReferenceChild("components", "component_id"), + RuntimeReferenceChild("agents", "agent_id"), + RuntimeReferenceChild("agent_groups", "group_id"), + RuntimeReferenceChild("content_sets", "content_id"), + RuntimeReferenceChild("detection_definitions", "definition_id"), + RuntimeReferenceChild("settings", "setting_id"), + ), + ), + RuntimeServiceFamily( + key="ssh-servers", + module=_runtime_ssh_server, + collection_name="ssh_servers", + id_field="ssh_server_id", + child_refs=(RuntimeReferenceChild("match_rules", "match_id"),), + ), + RuntimeServiceFamily( + key="app-authorizations", + module=_runtime_app_authorization, + collection_name="app_authorizations", + id_field="app_authorization_id", + child_refs=( + RuntimeReferenceChild("principals", "principal_id"), + RuntimeReferenceChild("roles", "role_id"), + RuntimeReferenceChild("permission_grants", "grant_id"), + RuntimeReferenceChild("role_mappings", "mapping_id"), + RuntimeReferenceChild("tenants", "tenant_id"), + ), + ), + RuntimeServiceFamily( + key="scheduled-jobs", + module=_runtime_scheduled_job, + collection_name="scheduled_jobs", + id_field="scheduled_job_id", + ), + RuntimeServiceFamily( + key="datastore-services", + module=_runtime_datastore, + collection_name="datastore_services", + id_field="datastore_service_id", + child_refs=( + RuntimeReferenceChild( + "nodes", + "node_id", + children=( + RuntimeReferenceChild("plugins", "plugin_id"), + RuntimeReferenceChild("endpoints", "endpoint_id"), + ), + ), + RuntimeReferenceChild("partitions", "partition_id"), + RuntimeReferenceChild("templates", "template_id"), + RuntimeReferenceChild("mappings", "mapping_id"), + RuntimeReferenceChild("settings", "setting_id"), + ), + ), + RuntimeServiceFamily( + key="platform-applications", + module=_runtime_platform_application, + collection_name="platform_applications", + id_field="platform_application_id", + child_refs=( + RuntimeReferenceChild("organizations", "organization_id"), + RuntimeReferenceChild("tenants", "tenant_id"), + RuntimeReferenceChild("content_objects", "content_object_id"), + RuntimeReferenceChild("markings", "marking_id"), + RuntimeReferenceChild("upstream_bindings", "binding_id"), + RuntimeReferenceChild("connectors", "connector_id"), + RuntimeReferenceChild("settings", "setting_id"), + ), + ), + RuntimeServiceFamily( + key="forwarding-agents", + module=_runtime_forwarding_agent, + collection_name="forwarding_agents", + id_field="forwarding_agent_id", + child_refs=( + RuntimeReferenceChild("sources", "source_id"), + RuntimeReferenceChild("transforms", "transform_id"), + RuntimeReferenceChild("ship_targets", "target_id"), + RuntimeReferenceChild("reload_channels", "reload_channel_id"), + RuntimeReferenceChild("settings", "setting_id"), + ), + ), + RuntimeServiceFamily( + key="orchestration-authorities", + module=_runtime_orchestration, + collection_name="orchestration_authorities", + id_field="orchestration_authority_id", + child_refs=( + RuntimeReferenceChild("spawn_templates", "template_id"), + RuntimeReferenceChild("realized_children", "workload_id"), + ), + ), +) + + +__all__ = [ + "RUNTIME_SERVICE_FAMILIES", + "RuntimeReferenceChild", + "RuntimeServiceFamily", +] diff --git a/implementations/python/packages/aces_sdl/_source_identifier_paths.py b/implementations/python/packages/aces_sdl/_source_identifier_paths.py new file mode 100644 index 000000000..5848dc8fe --- /dev/null +++ b/implementations/python/packages/aces_sdl/_source_identifier_paths.py @@ -0,0 +1,70 @@ +"""Source-pointer classification for SDL declaration identities.""" + +from ._mapping_scopes import HASHMAP_SECTIONS +from ._runtime_service_families import RUNTIME_SERVICE_FAMILIES, RuntimeReferenceChild, RuntimeServiceFamily + + +def is_declaration_key_path(tokens: list[str]) -> bool: + return _is_flat_declaration_scope(tokens) or _is_nested_entity_scope(tokens) + + +def _is_flat_declaration_scope(tokens: list[str]) -> bool: + if len(tokens) == 1: + return tokens[0] in HASHMAP_SECTIONS + if len(tokens) == 3: + return (tokens[0], tokens[2]) in {("nodes", "roles"), ("workflows", "steps")} + return False + + +def _is_nested_entity_scope(tokens: list[str]) -> bool: + return ( + bool(tokens) + and tokens[0] == "entities" + and tokens[-1] == "entities" + and all(segment == "entities" for segment in tokens[::2]) + ) + + +def is_scalar_identifier_path(tokens: list[str]) -> bool: + if len(tokens) == 3 and tokens[0] == "forwarding_agents" and tokens[1].isdigit(): + return tokens[2] == "forwarding_agent_id" + if len(tokens) == 5 and tokens[3].isdigit(): + return (tokens[0], tokens[2], tokens[4]) in { + ("nodes", "services", "name"), + ("infrastructure", "acls", "name"), + ("content", "items", "name"), + } + return _is_registered_runtime_identifier_path(tokens) + + +def _is_registered_runtime_identifier_path(tokens: list[str]) -> bool: + matched = False + for family in RUNTIME_SERVICE_FAMILIES: + suffix = _runtime_identifier_suffix(tokens, family) + if suffix is not None and (not suffix or _matches_runtime_child_path(suffix, family.child_refs)): + matched = True + break + return matched + + +def _runtime_identifier_suffix(tokens: list[str], family: RuntimeServiceFamily) -> list[str] | None: + if len(tokens) >= 6: + node_root = (tokens[0], tokens[2], tokens[3], tokens[5]) + expected = ("nodes", "runtime", family.collection_name, family.id_field) + if node_root == expected and tokens[4].isdigit(): + return tokens[6:] + if family.collection_name == "forwarding_agents" and len(tokens) >= 3: + forwarding_root = (tokens[0], tokens[2]) + if forwarding_root == ("forwarding_agents", family.id_field) and tokens[1].isdigit(): + return tokens[3:] + return None + + +def _matches_runtime_child_path(tokens: list[str], child_specs: tuple[RuntimeReferenceChild, ...]) -> bool: + if len(tokens) < 3 or not tokens[1].isdigit(): + return False + for child in child_specs: + if tokens[0] != child.collection_name or tokens[2] != child.id_field: + continue + return len(tokens) == 3 or _matches_runtime_child_path(tokens[3:], child.children) + return False diff --git a/implementations/python/packages/aces_sdl/_source_profile.py b/implementations/python/packages/aces_sdl/_source_profile.py new file mode 100644 index 000000000..c7f1e0117 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_source_profile.py @@ -0,0 +1,98 @@ +"""Versioned SDL source-profile policy and YAML 1.2 Core resolver setup.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from enum import Enum + +import yaml +from yaml.nodes import ScalarNode + +SDL_SOURCE_FORMAT = "sdl-yaml/v1" +SDL_CANONICAL_PROFILE = "aces-sdl-semantic/v1" + + +class SDLMigrationPolicy(str, Enum): + """Treatment of recognized non-canonical SDL source spellings.""" + + REJECT = "reject" + ACCEPT = "accept" + + +@dataclass(frozen=True) +class SDLParserLimits: + """Operational work limits for one source and its composition graph.""" + + max_input_bytes: int = 8 * 1024 * 1024 + max_scalar_bytes: int = 1024 * 1024 + max_depth: int = 128 + max_nodes: int = 100_000 + max_aliases: int = 256 + max_expanded_nodes: int = 250_000 + max_imports: int = 256 + max_composed_nodes: int = 500_000 + max_composed_bytes: int = 32 * 1024 * 1024 + max_composition_depth: int = 32 + max_namespace_depth: int = 32 + + def __post_init__(self) -> None: + for name, value in vars(self).items(): + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise ValueError(f"{name} must be a positive integer") + + +DEFAULT_PARSER_LIMITS = SDLParserLimits() + + +@dataclass(frozen=True) +class SDLSourceParseOptions: + """Versioned source-profile choices shared by internal parse boundaries.""" + + source_format: str = SDL_SOURCE_FORMAT + migration_policy: SDLMigrationPolicy | str = SDLMigrationPolicy.REJECT + limits: SDLParserLimits = DEFAULT_PARSER_LIMITS + + +DEFAULT_SOURCE_PARSE_OPTIONS = SDLSourceParseOptions() + + +_BOOL_RE = re.compile(r"^(?:true|True|TRUE|false|False|FALSE)$") +_FLOAT_RESOLVERS = tuple( + re.compile(pattern, re.ASCII) + for pattern in ( + r"^[-+]?\.\d+(?:[eE][-+]?\d+)?$", + r"^[-+]?\d+(?:\.\d*)?[eE][-+]?\d+$", + r"^[-+]?\d+\.\d*$", + r"^[-+]?\.(?:inf|Inf|INF)$", + r"^\.(?:nan|NaN|NAN)$", + ) +) +_INT_RESOLVERS = tuple(re.compile(pattern, re.ASCII) for pattern in (r"^[-+]?\d+$", r"^0o[0-7]+$", r"^0x[\da-fA-F]+$")) +_MERGE_RE = re.compile(r"^<<$") +_NULL_RE = re.compile(r"^(?:~|null|Null|NULL|)$") + + +def install_yaml_12_core_resolvers(loader_cls: type[yaml.SafeLoader]) -> None: + """Install SDL's private YAML 1.2 Core resolver table on ``loader_cls``.""" + + loader_cls.yaml_implicit_resolvers = {} + loader_cls.add_implicit_resolver("tag:yaml.org,2002:bool", _BOOL_RE, list("tTfF")) + for resolver in _FLOAT_RESOLVERS: + loader_cls.add_implicit_resolver("tag:yaml.org,2002:float", resolver, list("-+0123456789.")) + for resolver in _INT_RESOLVERS: + loader_cls.add_implicit_resolver("tag:yaml.org,2002:int", resolver, list("-+0123456789")) + loader_cls.add_implicit_resolver("tag:yaml.org,2002:merge", _MERGE_RE, ["<"]) + loader_cls.add_implicit_resolver("tag:yaml.org,2002:null", _NULL_RE, ["~", "n", "N", ""]) + loader_cls.add_constructor("tag:yaml.org,2002:int", _construct_core_int) + + +def _construct_core_int(loader: yaml.SafeLoader, node: ScalarNode) -> int: + value = loader.construct_scalar(node).replace("_", "") + sign = -1 if value.startswith("-") else 1 + unsigned = value[1:] if value[:1] in {"-", "+"} else value + if unsigned.startswith("0o"): + return sign * int(unsigned[2:], 8) + if unsigned.startswith("0x"): + return sign * int(unsigned[2:], 16) + return sign * int(unsigned, 10) diff --git a/implementations/python/packages/aces_sdl/_source_validation.py b/implementations/python/packages/aces_sdl/_source_validation.py new file mode 100644 index 000000000..18245a3fb --- /dev/null +++ b/implementations/python/packages/aces_sdl/_source_validation.py @@ -0,0 +1,343 @@ +"""Operational validation for the versioned SDL YAML source profile.""" + +from __future__ import annotations + +import math +from pathlib import Path +from typing import cast + +import yaml +from yaml.error import Mark +from yaml.nodes import MappingNode, Node, SequenceNode +from yaml.tokens import AliasToken, DirectiveToken, ScalarToken, TagToken, Token + +from ._errors import SDLParseDiagnostic, SDLParseError, SDLSourcePosition, SDLSourceRange +from ._source_profile import SDL_SOURCE_FORMAT, SDLMigrationPolicy, SDLParserLimits + +EMPTY_CONTENT_MESSAGE = "SDL content is empty" + + +def prepare_content(content: str, *, path: Path | None) -> str: + """Reject empty or non-UTF-8-compatible source without rewriting it.""" + if not content.strip(): + raise SDLParseError(EMPTY_CONTENT_MESSAGE, path=path) + try: + content.encode("utf-8") + except UnicodeEncodeError as exc: + diagnostic = _diagnostic_at_start( + code="sdl.utf8", + message="SDL source must be valid UTF-8 without unpaired surrogate code points.", + path=path, + ) + raise SDLParseError(diagnostic.message, path=path, diagnostics=(diagnostic,)) from exc + return content + + +def validate_source_format(source_format: str, *, path: Path | None) -> None: + """Require the one implemented versioned source profile.""" + if source_format == SDL_SOURCE_FORMAT: + return + diagnostic = _diagnostic_at_start( + code="sdl.source_format", + message=f"Unsupported SDL source format '{source_format}'; expected '{SDL_SOURCE_FORMAT}'.", + path=path, + ) + raise SDLParseError(diagnostic.message, path=path, diagnostics=(diagnostic,)) + + +def coerce_migration_policy( + migration_policy: SDLMigrationPolicy | str, + *, + path: Path | None, +) -> SDLMigrationPolicy: + """Validate the explicit migration-policy selector.""" + try: + return SDLMigrationPolicy(migration_policy) + except ValueError as exc: + allowed = ", ".join(policy.value for policy in SDLMigrationPolicy) + diagnostic = _diagnostic_at_start( + code="sdl.migration_policy", + message=f"Unsupported SDL migration policy '{migration_policy}'; expected one of: {allowed}.", + path=path, + ) + raise SDLParseError(diagnostic.message, path=path, diagnostics=(diagnostic,)) from exc + + +def validate_source_tokens(content: str, *, path: Path | None, limits: SDLParserLimits) -> None: + """Enforce byte/scalar/alias limits and forbidden presentation tokens.""" + input_bytes = len(content.encode("utf-8")) + if input_bytes > limits.max_input_bytes: + _raise_source_limit( + f"SDL source is {input_bytes} bytes; limit is {limits.max_input_bytes} bytes.", + path=path, + ) + + aliases = 0 + try: + for token in yaml.scan(content, Loader=yaml.SafeLoader): + if isinstance(token, AliasToken): + aliases += 1 + if aliases > limits.max_aliases: + _raise_source_limit( + f"SDL source has more than {limits.max_aliases} alias occurrences.", + path=path, + token=token, + ) + elif isinstance(token, ScalarToken): + scalar_bytes = len(token.value.encode("utf-8")) + if scalar_bytes > limits.max_scalar_bytes: + _raise_source_limit( + f"SDL scalar is {scalar_bytes} bytes; limit is {limits.max_scalar_bytes} bytes.", + path=path, + token=token, + ) + elif isinstance(token, TagToken): + _raise_token_diagnostic( + code="sdl.explicit_tag", + message="Explicit YAML tags are not valid sdl-yaml/v1 authoring syntax.", + path=path, + token=token, + ) + elif isinstance(token, DirectiveToken): + _raise_token_diagnostic( + code="sdl.directive", + message="YAML directives are not valid sdl-yaml/v1 authoring syntax.", + path=path, + token=token, + ) + except SDLParseError: + raise + except yaml.YAMLError as exc: + raise yaml_parse_error(exc, path=path) from exc + + +def validate_source_graph(root: Node, *, path: Path | None, limits: SDLParserLimits) -> None: + """Bound unique nodes, expanded alias work, and effective depth.""" + _GraphMetrics(path=path, limits=limits).calculate(root, 1) + + +class _GraphMetrics: + def __init__(self, *, path: Path | None, limits: SDLParserLimits) -> None: + self.path = path + self.limits = limits + self.unique: set[int] = set() + self.active: set[int] = set() + self.memoized: dict[int, tuple[int, int]] = {} + + def calculate(self, node: Node, depth: int) -> tuple[int, int]: + self._check_depth(node, depth) + identity = id(node) + if identity in self.active: + return 0, 0 + self._track_unique(node, identity) + cached = self.memoized.get(identity) + if cached is not None: + self._check_cached_depth(node, depth, cached) + return cached + self.active.add(identity) + try: + metrics = self._calculate_children(node, depth) + finally: + self.active.remove(identity) + self.memoized[identity] = metrics + return metrics + + def _calculate_children(self, node: Node, depth: int) -> tuple[int, int]: + cost = 1 + height = 1 + for child in self._children(node): + child_cost, child_height = self.calculate(child, depth + 1) + cost += child_cost + height = max(height, child_height + 1) + _check_expanded_cost(cost, node=node, path=self.path, limits=self.limits) + return cost, height + + @staticmethod + def _children(node: Node) -> tuple[Node, ...]: + if isinstance(node, MappingNode): + return tuple(child for pair in node.value for child in pair) + if isinstance(node, SequenceNode): + return tuple(node.value) + return () + + def _track_unique(self, node: Node, identity: int) -> None: + if identity in self.unique: + return + self.unique.add(identity) + if len(self.unique) > self.limits.max_nodes: + _raise_source_limit( + f"SDL node graph has more than {self.limits.max_nodes} unique nodes.", + path=self.path, + node=node, + ) + + def _check_depth(self, node: Node, depth: int) -> None: + if depth > self.limits.max_depth: + _raise_source_limit(f"SDL node depth exceeds {self.limits.max_depth}.", path=self.path, node=node) + + def _check_cached_depth(self, node: Node, depth: int, metrics: tuple[int, int]) -> None: + _cost, height = metrics + if depth + height - 1 > self.limits.max_depth: + _raise_source_limit(f"SDL node depth exceeds {self.limits.max_depth}.", path=self.path, node=node) + + +def _check_expanded_cost( + cost: int, + *, + node: Node, + path: Path | None, + limits: SDLParserLimits, +) -> None: + if cost > limits.max_expanded_nodes: + _raise_source_limit( + f"SDL alias-expanded work exceeds {limits.max_expanded_nodes} nodes.", + path=path, + node=node, + ) + + +def validate_constructed_domain(value: object, *, path: Path | None) -> None: + """Reject values outside the string-keyed JSON data domain.""" + _ConstructedDomainValidator(path=path).visit(value) + + +class _ConstructedDomainValidator: + _SCALAR_TYPES = frozenset({str, bool, int}) + + def __init__(self, *, path: Path | None) -> None: + self.path = path + self.active: set[int] = set() + self.visited: set[int] = set() + + def visit(self, item: object) -> None: + item_type = type(item) + if item is None or item_type in self._SCALAR_TYPES: + return + if item_type is float: + self._visit_float(cast(float, item)) + elif item_type is list: + self._visit_list(cast(list[object], item)) + elif item_type is dict: + self._visit_dict(cast(dict[object, object], item)) + else: + _raise_domain_error( + f"YAML value type '{item_type.__name__}' is outside the SDL JSON domain.", path=self.path + ) + + def _visit_float(self, item: float) -> None: + if not math.isfinite(item): + _raise_domain_error("Non-finite numbers are not valid SDL values.", path=self.path) + + def _visit_list(self, item: list[object]) -> None: + if not self._begin_container(item): + return + try: + for child in item: + self.visit(child) + finally: + self._finish_container(item) + + def _visit_dict(self, item: dict[object, object]) -> None: + if not self._begin_container(item): + return + try: + for key, child in item.items(): + if not isinstance(key, str): + _raise_domain_error("SDL mapping keys must construct as strings.", path=self.path) + self.visit(child) + finally: + self._finish_container(item) + + def _begin_container(self, item: object) -> bool: + identity = id(item) + if identity in self.active or identity in self.visited: + return False + self.active.add(identity) + return True + + def _finish_container(self, item: object) -> None: + identity = id(item) + self.active.remove(identity) + self.visited.add(identity) + + +def _raise_domain_error(message: str, *, path: Path | None) -> None: + diagnostic = _diagnostic_at_start(code="sdl.non_json_value", message=message, path=path) + raise SDLParseError(message, path=path, diagnostics=(diagnostic,)) + + +def _raise_source_limit( + message: str, + *, + path: Path | None, + token: Token | None = None, + node: Node | None = None, +) -> None: + if token is not None: + primary_range = _range_from_marks(token.start_mark, token.end_mark) + elif node is not None: + primary_range = _range_from_node(node) + else: + primary_range = _start_range() + diagnostic = SDLParseDiagnostic( + code="sdl.source_limit", + message=message, + pointer="", + primary_range=primary_range, + source=str(path) if path is not None else None, + ) + raise SDLParseError(message, path=path, diagnostics=(diagnostic,)) + + +def _raise_token_diagnostic(*, code: str, message: str, path: Path | None, token: Token) -> None: + diagnostic = SDLParseDiagnostic( + code=code, + message=message, + pointer="", + primary_range=_range_from_marks(token.start_mark, token.end_mark), + source=str(path) if path is not None else None, + ) + raise SDLParseError(message, path=path, diagnostics=(diagnostic,)) + + +def _diagnostic_at_start(*, code: str, message: str, path: Path | None) -> SDLParseDiagnostic: + return SDLParseDiagnostic( + code=code, + message=message, + pointer="", + primary_range=_start_range(), + source=str(path) if path is not None else None, + ) + + +def _start_range() -> SDLSourceRange: + position = SDLSourcePosition(1, 1) + return SDLSourceRange(start=position, end=position) + + +def _range_from_marks(start: Mark, end: Mark) -> SDLSourceRange: + return SDLSourceRange( + start=SDLSourcePosition(start.line + 1, start.column + 1), + end=SDLSourcePosition(end.line + 1, end.column + 1), + ) + + +def _range_from_node(node: Node) -> SDLSourceRange: + return _range_from_marks(node.start_mark, node.end_mark) + + +def yaml_parse_error(error: yaml.YAMLError, *, path: Path | None) -> SDLParseError: + """Translate a PyYAML error into the stable SDL parse envelope.""" + mark = getattr(error, "problem_mark", None) + problem = getattr(error, "problem", None) + if mark is None: + return SDLParseError(f"Invalid YAML: {error}", path=path) + position = SDLSourcePosition(mark.line + 1, mark.column + 1) + diagnostic = SDLParseDiagnostic( + code="sdl.parse", + message=str(problem or error), + pointer="", + primary_range=SDLSourceRange(start=position, end=position), + source=str(path) if path is not None else None, + ) + return SDLParseError(f"Invalid YAML: {error}", path=path, diagnostics=(diagnostic,)) diff --git a/implementations/python/packages/aces_sdl/_yaml_loader.py b/implementations/python/packages/aces_sdl/_yaml_loader.py new file mode 100644 index 000000000..4a57eeda5 --- /dev/null +++ b/implementations/python/packages/aces_sdl/_yaml_loader.py @@ -0,0 +1,170 @@ +"""Safe, source-marked YAML composition for the SDL authoring boundary.""" + +from __future__ import annotations + +from pathlib import Path + +import yaml +from yaml.nodes import Node + +from ._errors import ( + SDLParseDiagnostic, + SDLParseError, + SDLSourceRange, +) +from ._mapping_key_analyzer import _MappingAnalyzer +from ._mapping_scopes import MappingScope +from ._source_profile import ( + DEFAULT_SOURCE_PARSE_OPTIONS, + SDLMigrationPolicy, + SDLSourceParseOptions, + install_yaml_12_core_resolvers, +) +from ._source_validation import ( + EMPTY_CONTENT_MESSAGE, + coerce_migration_policy, + prepare_content, + validate_constructed_domain, + validate_source_format, + validate_source_graph, + validate_source_tokens, + yaml_parse_error, +) + + +class _SDLSafeLoader(yaml.SafeLoader): + """SafeLoader with an isolated YAML 1.2 Core implicit resolver table.""" + + +install_yaml_12_core_resolvers(_SDLSafeLoader) + + +def load_sdl_yaml( + content: str, + *, + path: Path | None = None, + scope: MappingScope = MappingScope.STRUCTURAL, + base_pointer: str = "", + source_options: SDLSourceParseOptions = DEFAULT_SOURCE_PARSE_OPTIONS, + source_diagnostics: list[SDLParseDiagnostic] | None = None, + source_ranges: dict[str, SDLSourceRange] | None = None, +) -> object: + """Validate and safely construct one SDL YAML document or fragment.""" + prepared = prepare_content(content, path=path) + policy = coerce_migration_policy(source_options.migration_policy, path=path) + validate_source_format(source_options.source_format, path=path) + validate_source_tokens(prepared, path=path, limits=source_options.limits) + loader: _SDLSafeLoader | None = None + try: + loader = _SDLSafeLoader(prepared) + root = loader.get_single_node() + if root is None: + raise SDLParseError(EMPTY_CONTENT_MESSAGE, path=path) + validate_source_graph(root, path=path, limits=source_options.limits) + _validate_mapping_keys( + root, + path=path, + scope=scope, + base_pointer=base_pointer, + migration_policy=policy, + source_diagnostics=source_diagnostics, + source_ranges=source_ranges, + ) + constructed = loader.construct_document(root) + validate_constructed_domain(constructed, path=path) + return constructed + except SDLParseError: + raise + except yaml.YAMLError as exc: + raise yaml_parse_error(exc, path=path) from exc + finally: + if loader is not None: + loader.dispose() + + +def compose_sdl_yaml( + content: str, + *, + path: Path | None = None, + scope: MappingScope = MappingScope.STRUCTURAL, + base_pointer: str = "", + source_options: SDLSourceParseOptions = DEFAULT_SOURCE_PARSE_OPTIONS, + source_diagnostics: list[SDLParseDiagnostic] | None = None, + source_ranges: dict[str, SDLSourceRange] | None = None, +) -> Node: + """Compose and key-validate SDL YAML while retaining source nodes.""" + prepared = prepare_content(content, path=path) + policy = coerce_migration_policy(source_options.migration_policy, path=path) + validate_source_format(source_options.source_format, path=path) + validate_source_tokens(prepared, path=path, limits=source_options.limits) + loader: _SDLSafeLoader | None = None + try: + loader = _SDLSafeLoader(prepared) + root = loader.get_single_node() + if root is None: + raise SDLParseError(EMPTY_CONTENT_MESSAGE, path=path) + validate_source_graph(root, path=path, limits=source_options.limits) + _validate_mapping_keys( + root, + path=path, + scope=scope, + base_pointer=base_pointer, + migration_policy=policy, + source_diagnostics=source_diagnostics, + source_ranges=source_ranges, + ) + return root + except SDLParseError: + raise + except yaml.YAMLError as exc: + raise yaml_parse_error(exc, path=path) from exc + finally: + if loader is not None: + loader.dispose() + + +def _validate_mapping_keys( + root: Node, + *, + path: Path | None, + scope: MappingScope, + base_pointer: str, + migration_policy: SDLMigrationPolicy, + source_diagnostics: list[SDLParseDiagnostic] | None, + source_ranges: dict[str, SDLSourceRange] | None, +) -> None: + diagnostics = _MappingAnalyzer( + migration_policy=migration_policy, + path=path, + source_ranges=source_ranges, + ).analyze( + root, + scope=scope, + base_tokens=_decode_pointer(base_pointer), + ) + warnings = tuple(item for item in diagnostics if item.severity == "warning") + errors = tuple(item for item in diagnostics if item.severity != "warning") + if source_diagnostics is not None: + source_diagnostics.extend(warnings) + if not errors: + return + rendered: list[str] = [] + for item in errors: + location = item.primary_range.start + detail = ( + f"[{item.code}] {item.pointer or '/'} at line {location.line}, column {location.column}: {item.message}" + ) + if item.related_range is not None: + related = item.related_range.start + detail += f" First declaration at line {related.line}, column {related.column}." + rendered.append(detail) + details = "SDL mapping-key validation failed:\n " + "\n ".join(rendered) + raise SDLParseError(details, path=path, diagnostics=errors) + + +def _decode_pointer(pointer: str) -> list[str]: + if not pointer: + return [] + if not pointer.startswith("/"): + raise ValueError("base_pointer must be an RFC 6901 pointer") + return [token.replace("~1", "/").replace("~0", "~") for token in pointer[1:].split("/")] diff --git a/implementations/python/packages/aces_sdl/agents.py b/implementations/python/packages/aces_sdl/agents.py index c6c881648..3f3834afc 100644 --- a/implementations/python/packages/aces_sdl/agents.py +++ b/implementations/python/packages/aces_sdl/agents.py @@ -12,7 +12,7 @@ The ``Agent`` model is the SDL-authoring surface for declarative participant framing (ACT-601, ADR-020). Identity binds to a declared ``entities`` entry, role reuses ``entities.role``, starting conditions -combine ``starting_accounts``/``initial_knowledge``/``starting_conditions``, +combine ``starting_accounts``/``initial_knowledge``/``starting_assertions``, authority anchors point at declared SDL elements, and operating scope combines ``allowed_subnets`` with the broader ``operating_scope`` list. """ @@ -46,9 +46,9 @@ class Agent(SDLModel): - ``starting_accounts`` links to the accounts section - ``allowed_subnets`` links to infrastructure entries - ``initial_knowledge`` references nodes and infrastructure - - ``starting_conditions`` links to the conditions section, giving the - authoring surface a declarative hook for participant-relevant - precondition checks (ACT-601) + - ``starting_assertions`` links to precondition assertions, giving the + authoring surface a declarative hook for participant-relevant starting + state without equating a probe implementation with truth (ACT-601) - ``authority_anchors`` links to declared SDL elements (entities, relationships, content, etc.) that anchor what the participant is allowed or expected to do in scenario meaning (ACT-601, ADR-020) @@ -70,11 +70,21 @@ class Agent(SDLModel): starting_accounts: list[str] = Field(default_factory=list) initial_knowledge: InitialKnowledge | None = None allowed_subnets: list[str] = Field(default_factory=list) - starting_conditions: list[str] = Field(default_factory=list) + starting_assertions: list[str] = Field(default_factory=list) authority_anchors: list[str] = Field(default_factory=list) operating_scope: list[str] = Field(default_factory=list) observation_boundaries: list[str] = Field(default_factory=list) + @model_validator(mode="before") + @classmethod + def reject_legacy_starting_conditions(cls, value: object) -> object: + if isinstance(value, dict) and "starting_conditions" in value: + raise ValueError( + "agent starting_conditions cannot state backend-neutral truth; " + "reference precondition assertions via starting_assertions" + ) + return value + @model_validator(mode="after") def validate_required_entity(self) -> "Agent": if not self.entity: diff --git a/implementations/python/packages/aces_sdl/canonical.py b/implementations/python/packages/aces_sdl/canonical.py new file mode 100644 index 000000000..b248fe8b1 --- /dev/null +++ b/implementations/python/packages/aces_sdl/canonical.py @@ -0,0 +1,109 @@ +"""Versioned canonical semantic identity for validated SDL authoring scenarios.""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from typing import Literal + +import rfc8785 +from pydantic import ConfigDict + +from ._base import SDLModel +from ._errors import SDLParseError +from ._source_profile import SDL_CANONICAL_PROFILE +from .scenario import ExpandedScenario, InstantiatedScenario, Scenario + +INSTANTIATED_SNAPSHOT_PROFILE = "aces-sdl-instantiated-snapshot/v1" + + +@dataclass(frozen=True) +class SDLCanonicalDigest: + """Profile-labelled digest of canonical SDL semantic bytes.""" + + profile: str + algorithm: str + value: str + + def as_dict(self) -> dict[str, str]: + return {"profile": self.profile, "algorithm": self.algorithm, "value": self.value} + + +def canonical_sdl_bytes(scenario: Scenario | ExpandedScenario) -> bytes: + """Return RFC 8785 bytes for one validated, post-expansion authoring scenario.""" + if isinstance(scenario, InstantiatedScenario): + raise SDLParseError("Canonical SDL semantic identity requires an authoring scenario, not an instantiated one") + if not scenario.semantic_validated: + raise SDLParseError("Canonical SDL semantic identity requires successful semantic validation") + + payload = { + "profile": SDL_CANONICAL_PROFILE, + "scenario": scenario.model_dump( + mode="json", + by_alias=True, + exclude_unset=True, + exclude={"expansion_provenance"}, + ), + "module_variable_specs": scenario.module_variable_specs, + "module_node_variable_refs": scenario.module_node_variable_refs, + } + try: + return rfc8785.dumps(payload) + except rfc8785.CanonicalizationError as exc: + raise SDLParseError(f"SDL canonicalization failed: {exc}") from exc + + +def canonical_sdl_digest(scenario: Scenario | ExpandedScenario) -> SDLCanonicalDigest: + """Return the profile-labelled SHA-256 digest of canonical SDL semantic bytes.""" + digest = hashlib.sha256(canonical_sdl_bytes(scenario)).hexdigest() + return SDLCanonicalDigest( + profile=SDL_CANONICAL_PROFILE, + algorithm="sha256", + value=f"sha256:{digest}", + ) + + +class InstantiatedScenarioSnapshot(SDLModel): + """Sealed canonical envelope for one portable instantiated artifact.""" + + model_config = ConfigDict( + title="SDL Instantiated Scenario Snapshot v1", + extra="forbid", + frozen=True, + json_schema_extra={"x-aces-document-phase": "canonical-instantiated-snapshot"}, + ) + + profile: Literal["aces-sdl-instantiated-snapshot/v1"] + scenario: InstantiatedScenario + + +def canonical_instantiated_sdl_bytes(scenario: InstantiatedScenario) -> bytes: + """Return RFC 8785 bytes for one admitted instantiated artifact.""" + + # Keep canonicalization pure with respect to the caller's private validation + # flags while applying the same structural and semantic admission boundary + # used by the compiler. + from .instantiate import admit_instantiated_scenario + + admitted = admit_instantiated_scenario( + InstantiatedScenario.model_validate(scenario.model_dump(mode="python", by_alias=True)) + ) + snapshot = InstantiatedScenarioSnapshot( + profile=INSTANTIATED_SNAPSHOT_PROFILE, + scenario=admitted, + ) + try: + return rfc8785.dumps(snapshot.model_dump(mode="json")) + except rfc8785.CanonicalizationError as exc: + raise SDLParseError(f"Instantiated SDL canonicalization failed: {exc}") from exc + + +def canonical_instantiated_sdl_digest(scenario: InstantiatedScenario) -> SDLCanonicalDigest: + """Return the profile-labelled digest of an instantiated snapshot.""" + + digest = hashlib.sha256(canonical_instantiated_sdl_bytes(scenario)).hexdigest() + return SDLCanonicalDigest( + profile=INSTANTIATED_SNAPSHOT_PROFILE, + algorithm="sha256", + value=f"sha256:{digest}", + ) diff --git a/implementations/python/packages/aces_sdl/composition.py b/implementations/python/packages/aces_sdl/composition.py index 820191f94..c6a5120d8 100644 --- a/implementations/python/packages/aces_sdl/composition.py +++ b/implementations/python/packages/aces_sdl/composition.py @@ -6,40 +6,62 @@ from pathlib import Path from typing import Any +from pydantic import ValidationError + from ._base import is_variable_ref -from ._errors import SDLInstantiationError, SDLParseError -from ._module_provenance import ( - add_unique_provenance as _add_unique_provenance, +from ._composition_budget import CompositionBudget, CompositionTraversal +from ._composition_provenance import ( + prefixed_constraint as _prefixed_constraint, +) +from ._composition_provenance import ( + prefixed_explicitness as _prefixed_explicitness, ) -from ._module_provenance import ( - dump_variable_spec as _dump_variable_spec, +from ._composition_provenance import ( + prefixed_import_record as _prefixed_import_record, ) -from ._module_provenance import ( - rename_variable_ref, +from ._composition_provenance import ( + resolved_import_record as _resolved_import_record, ) +from ._errors import SDLInstantiationError, SDLParseDiagnostic, SDLParseError +from ._identifiers import QualifiedName +from ._module_symbols import FORWARDING_AGENTS_SECTION +from ._module_symbols import HASHMAP_SECTIONS as _HASHMAP_SECTIONS from ._module_symbols import ( - HASHMAP_SECTIONS as _HASHMAP_SECTIONS, + rewrite_objective_window_ref as _rewrite_objective_window_ref, ) from ._module_symbols import ( symbol_index as _symbol_index, ) +from ._source_profile import ( + DEFAULT_PARSER_LIMITS, + SDL_SOURCE_FORMAT, + SDLMigrationPolicy, + SDLParserLimits, + SDLSourceParseOptions, +) from .entities import flatten_entities -from .instantiate import instantiate_scenario +from .instantiate import _bind_scenario_content from .module_registry import ( load_lockfile, load_trust_policy, resolve_import, ) from .parser import _load_normalized_data -from .scenario import ImportDecl, ModuleDescriptor, Scenario +from .phase_contracts import ( + CapabilityConstraint, + ExpansionProvenance, + ExplicitnessProvenanceRecord, + ResolvedImportProvenance, +) +from .scenario import ExpandedScenario, ImportDecl, ModuleDescriptor, ScenarioContent def _prefix(namespace: str, name: str) -> str: - return f"{namespace}.{name}" if namespace else name + return QualifiedName.parse(name).prefixed(namespace).render() if namespace else QualifiedName.parse(name).render() def _private_prefix(namespace: str, name: str) -> str: - return _prefix(namespace, f"__private.{name}") + return QualifiedName.parse(name).prefixed(namespace, private=True).render() def _maybe_rename(name: str, name_map: Mapping[str, str]) -> str: @@ -49,11 +71,17 @@ def _maybe_rename(name: str, name_map: Mapping[str, str]) -> str: def _validate_descriptor_exports( - scenario: Scenario, + scenario: ScenarioContent, descriptor: ModuleDescriptor, ) -> None: for section_name, exported_names in descriptor.exports.items(): - if section_name == "entities": + if section_name not in {*_HASHMAP_SECTIONS, FORWARDING_AGENTS_SECTION}: + raise SDLParseError(f"Module '{descriptor.id}' exports unknown SDL section '{section_name}'") + for exported_name in exported_names: + QualifiedName.parse(exported_name) + if section_name == FORWARDING_AGENTS_SECTION: + available_names = {agent.forwarding_agent_id for agent in scenario.forwarding_agents} + elif section_name == "entities": available_names = set(flatten_entities(scenario.entities)) else: section_payload = getattr(scenario, section_name, None) @@ -112,15 +140,6 @@ def _rewrite_entity(payload: dict[str, Any], symbols: dict[str, dict[str, str] | _rewrite_entity(child, symbols) -def _rewrite_objective_window_ref(ref: str, workflow_names: Mapping[str, str]) -> str: - if "." not in ref or is_variable_ref(ref): - return ref - workflow_name, step_name = ref.rsplit(".", 1) - if workflow_name not in workflow_names: - return ref - return f"{workflow_names[workflow_name]}.{step_name}" - - def _rewrite_workflow(payload: dict[str, Any], symbols: dict[str, dict[str, str] | set[str]]) -> None: for step in payload.get("steps", {}).values(): if not isinstance(step, dict): @@ -133,7 +152,7 @@ def _rewrite_workflow(payload: dict[str, Any], symbols: dict[str, dict[str, str] step["compensate_with"] = _maybe_rename(str(step["compensate_with"]), symbols["workflows"]) when = step.get("when") if isinstance(when, dict): - when["conditions"] = [_maybe_rename(name, symbols["conditions"]) for name in when.get("conditions", [])] + when["assertions"] = [_maybe_rename(name, symbols["assertions"]) for name in when.get("assertions", [])] when["objectives"] = [_maybe_rename(name, symbols["objectives"]) for name in when.get("objectives", [])] @@ -150,22 +169,18 @@ def _rewrite_evidence_requirement( def _namespace_payload( payload: dict[str, Any], - imported: Scenario, + imported: ScenarioContent, namespace: str, + descriptor: ModuleDescriptor, ) -> dict[str, Any]: namespaced = dict(payload) - descriptor = imported.module or ModuleDescriptor( - id=imported.name, - version=imported.version, - parameters=sorted(imported.variables.keys()), - exports={ - section: sorted(getattr(imported, section).keys()) - for section in _HASHMAP_SECTIONS - if getattr(imported, section) - }, - ) _validate_descriptor_exports(imported, descriptor) - symbols = _symbol_index(imported, namespace=namespace, descriptor=descriptor) + symbols = _symbol_index( + imported, + namespace=namespace, + descriptor=descriptor, + restrict_to_descriptor=True, + ) for node in namespaced.get("nodes", {}).values(): if isinstance(node, dict): @@ -176,6 +191,21 @@ def _namespace_payload( for feature in namespaced.get("features", {}).values(): if isinstance(feature, dict): _rewrite_feature(feature, symbols) + for condition in namespaced.get("conditions", {}).values(): + if isinstance(condition, dict) and condition.get("proposition"): + condition["proposition"] = _maybe_rename(str(condition["proposition"]), symbols["propositions"]) + for proposition in namespaced.get("propositions", {}).values(): + if isinstance(proposition, dict): + proposition["subjects"] = [ + _maybe_rename(name, symbols["named"]) for name in proposition.get("subjects", []) + ] + proposition["evidence_requirements"] = [ + _maybe_rename(name, symbols["evidence_requirements"]) + for name in proposition.get("evidence_requirements", []) + ] + for assertion in namespaced.get("assertions", {}).values(): + if isinstance(assertion, dict) and assertion.get("proposition"): + assertion["proposition"] = _maybe_rename(str(assertion["proposition"]), symbols["propositions"]) for entity in namespaced.get("entities", {}).values(): if isinstance(entity, dict): _rewrite_entity(entity, symbols) @@ -186,7 +216,7 @@ def _namespace_payload( inject["to_entities"] = [_maybe_rename(name, symbols["entities"]) for name in inject.get("to_entities", [])] for event in namespaced.get("events", {}).values(): if isinstance(event, dict): - event["conditions"] = [_maybe_rename(name, symbols["conditions"]) for name in event.get("conditions", [])] + event["assertions"] = [_maybe_rename(name, symbols["assertions"]) for name in event.get("assertions", [])] event["injects"] = [_maybe_rename(name, symbols["injects"]) for name in event.get("injects", [])] for script in namespaced.get("scripts", {}).values(): if isinstance(script, dict): @@ -208,6 +238,12 @@ def _namespace_payload( relationship["source"] = _maybe_rename(str(relationship["source"]), symbols["named"]) if relationship.get("target"): relationship["target"] = _maybe_rename(str(relationship["target"]), symbols["named"]) + forwarding_edge = relationship.get("forwarding_edge") + if isinstance(forwarding_edge, dict) and forwarding_edge.get("forwarder_ref"): + forwarding_edge["forwarder_ref"] = _maybe_rename( + str(forwarding_edge["forwarder_ref"]), + symbols[FORWARDING_AGENTS_SECTION], + ) for agent in namespaced.get("agents", {}).values(): if isinstance(agent, dict): if agent.get("entity"): @@ -231,8 +267,8 @@ def _namespace_payload( # symbols["named"] carries both forms after the symbol-index # update, so a single rename handles `health` and # `conditions.health` symmetrically. - agent["starting_conditions"] = [ - _maybe_rename(name, symbols["named"]) for name in agent.get("starting_conditions", []) + agent["starting_assertions"] = [ + _maybe_rename(name, symbols["assertions"]) for name in agent.get("starting_assertions", []) ] agent["authority_anchors"] = [ _maybe_rename(name, symbols["named"]) for name in agent.get("authority_anchors", []) @@ -276,8 +312,8 @@ def _namespace_payload( ] success = objective.get("success") if isinstance(success, dict): - success["conditions"] = [ - _maybe_rename(name, symbols["conditions"]) for name in success.get("conditions", []) + success["assertions"] = [ + _maybe_rename(name, symbols["assertions"]) for name in success.get("assertions", []) ] window = objective.get("window") if isinstance(window, dict): @@ -302,9 +338,22 @@ def _namespace_payload( namespaced[section_name] = { symbols[section_name].get(name, _prefix(namespace, name)): value for name, value in section_payload.items() } - namespaced["variables"] = {} - namespaced["module"] = None - namespaced["imports"] = [] + forwarding_agents = namespaced.get(FORWARDING_AGENTS_SECTION, []) + if isinstance(forwarding_agents, list): + for agent in forwarding_agents: + if not isinstance(agent, dict): + continue + identifier = agent.get("forwarding_agent_id") + if isinstance(identifier, str): + agent["forwarding_agent_id"] = symbols[FORWARDING_AGENTS_SECTION].get( + identifier, + _private_prefix(namespace, identifier), + ) + namespaced.pop("variables", None) + namespaced.pop("module", None) + namespaced.pop("imports", None) + namespaced.pop("expansion_provenance", None) + namespaced.pop("instantiation_provenance", None) return namespaced @@ -323,6 +372,14 @@ def _merge_sections( raise SDLParseError(f"Import from {path} collides on {section_name}: {', '.join(collisions)}") current.update(additions) merged[section_name] = current + current_agents = list(merged.get(FORWARDING_AGENTS_SECTION, [])) + incoming_agents = list(incoming.get(FORWARDING_AGENTS_SECTION, [])) + current_ids = {agent.get("forwarding_agent_id") for agent in current_agents if isinstance(agent, dict)} + incoming_ids = {agent.get("forwarding_agent_id") for agent in incoming_agents if isinstance(agent, dict)} + collisions = sorted(identifier for identifier in current_ids.intersection(incoming_ids) if identifier) + if collisions: + raise SDLParseError(f"Import from {path} collides on {FORWARDING_AGENTS_SECTION}: {', '.join(collisions)}") + merged[FORWARDING_AGENTS_SECTION] = [*current_agents, *incoming_agents] merged["imports"] = [] return merged @@ -337,41 +394,38 @@ def expand_sdl_modules( data: dict[str, Any], *, path: Path, - seen: set[Path] | None = None, -) -> tuple[ - dict[str, Any], - dict[str, str], - dict[str, dict[str, object]], - dict[str, dict[str, str | None]], -]: - """Expand local SDL imports into one canonical merged payload. - - Returns ``(merged_payload, namespaces, module_variable_specs, - module_node_variable_refs)``. The trailing two channels carry capability- - variable provenance across the import boundary so the runtime planner can - enforce `allowed_values` against backend support for imported parameterized - modules. The composition step strips imported variables from the merged - payload by design; these side-channel dicts preserve their specs - (namespace-prefixed) and the imported nodes' `${name}` refs (with both - node and variable names namespace-prefixed) for downstream consumers. - """ - - seen = set() if seen is None else set(seen) + source_format: str = SDL_SOURCE_FORMAT, + migration_policy: SDLMigrationPolicy | str = SDLMigrationPolicy.REJECT, + limits: SDLParserLimits = DEFAULT_PARSER_LIMITS, + source_diagnostics: list[SDLParseDiagnostic] | None = None, + _traversal: CompositionTraversal | None = None, +) -> tuple[dict[str, Any], ExpansionProvenance]: + """Expand trusted imports into executable content and portable evidence.""" + + traversal = _traversal or CompositionTraversal( + seen=frozenset(), + budget=CompositionBudget(limits), + depth=0, + ) + budget = traversal.budget + budget.check_depth(traversal.depth, path=path) + budget.add_document(data, path=path) resolved_path = path.resolve() - if resolved_path in seen: + if resolved_path in traversal.seen: raise SDLParseError(f"Import cycle detected at {resolved_path}", path=path) - seen.add(resolved_path) + child_traversal = traversal.descend_from(resolved_path) merged = dict(data) merged.setdefault("imports", []) merged.setdefault("version", "*") - namespaces: dict[str, str] = {} - module_variable_specs: dict[str, dict[str, object]] = {} - module_node_variable_refs: dict[str, dict[str, str | None]] = {} + import_records: list[ResolvedImportProvenance] = [] + capability_constraints: list[CapabilityConstraint] = [] + explicitness_records: list[ExplicitnessProvenanceRecord] = [] lockfile = load_lockfile(resolved_path.parent) trust_policy = load_trust_policy(resolved_path.parent) for raw_import in list(merged.get("imports", [])): + budget.add_import(path=path) import_decl = _import_decl(raw_import) if "__private." in import_decl.namespace: raise SDLParseError( @@ -383,132 +437,82 @@ def expand_sdl_modules( base_dir=resolved_path.parent, lockfile=lockfile, trust_policy=trust_policy, + source_options=SDLSourceParseOptions( + source_format=source_format, + migration_policy=migration_policy, + limits=limits, + ), + source_diagnostics=source_diagnostics, ) import_path = resolved_import.root_file imported_raw = _load_normalized_data( import_path.read_text(encoding="utf-8"), path=import_path, + source_format=source_format, + migration_policy=migration_policy, + limits=limits, + source_diagnostics=source_diagnostics, ) - ( - imported_expanded, - imported_namespaces, - inner_module_variable_specs, - inner_module_node_variable_refs, - ) = expand_sdl_modules( + imported_expanded, inner_provenance = expand_sdl_modules( imported_raw, path=import_path, - seen=seen, + source_format=source_format, + migration_policy=migration_policy, + limits=limits, + source_diagnostics=source_diagnostics, + _traversal=child_traversal, ) try: - imported_scenario = Scenario.model_validate(imported_expanded) - # Re-attach the deeper-import provenance so `instantiate_scenario` - # can propagate it onto the `InstantiatedScenario` alongside - # whatever local refs it captures. - imported_scenario._set_module_variable_specs(inner_module_variable_specs) - imported_scenario._set_module_node_variable_refs(inner_module_node_variable_refs) - imported_instantiated = instantiate_scenario( - imported_scenario, - parameters=import_decl.parameters, - validate_semantics=False, - ) + imported_scenario = ExpandedScenario.model_validate(imported_expanded) + bound = _bind_scenario_content(imported_scenario, import_decl.parameters) + except ValidationError as exc: + raise SDLParseError("Imported SDL unit is structurally invalid", path=import_path) from exc except SDLInstantiationError as exc: raise SDLParseError(str(exc), path=import_path) from exc - imported_instantiated.module = resolved_import.module_descriptor - namespace = import_decl.namespace or resolved_import.module_descriptor.id.split("/")[-1] - - descriptor = imported_instantiated.module or ModuleDescriptor( - id=imported_instantiated.name, - version=imported_instantiated.version, - parameters=sorted(imported_instantiated.variables.keys()), - exports={ - section: sorted(getattr(imported_instantiated, section).keys()) - for section in _HASHMAP_SECTIONS - if getattr(imported_instantiated, section) - }, + namespace = import_decl.namespace + descriptor = resolved_import.module_descriptor + symbols = _symbol_index( + bound.content, + namespace=namespace, + descriptor=descriptor, + restrict_to_descriptor=True, ) - symbols = _symbol_index(imported_instantiated, namespace=namespace, descriptor=descriptor) - - # Local imported variables get namespace-prefixed (private prefix; they - # are not exported) and threaded into the module-variable-specs - # accumulator. Deeper-import specs from `imported_instantiated.module_variable_specs` - # are already prefixed at their level; re-prefix them at this level too. - # Collisions on the generated private-prefix namespace are rejected - # outright: silently letting one provenance entry overwrite another - # would let the planner validate an imported node against the wrong - # `allowed_values` domain. The same applies to the node-ref merge below. - local_var_renames = {name: _private_prefix(namespace, name) for name in imported_instantiated.variables} - for var_name, spec in imported_instantiated.variables.items(): - _add_unique_provenance( - module_variable_specs, - local_var_renames[var_name], - _dump_variable_spec(spec), - kind="variable spec", - source_path=import_path, - ) - for inner_name, spec in imported_instantiated.module_variable_specs.items(): - _add_unique_provenance( - module_variable_specs, - _prefix(namespace, inner_name), - dict(spec), - kind="variable spec", - source_path=import_path, - ) - - # Captured node refs come from two sources: locally-captured refs in - # the imported scenario (`imported_instantiated.node_variable_refs`) - # whose node-name keys are still the imported scenario's own names, - # and pre-existing deeper-import refs in - # `imported_instantiated.module_node_variable_refs` whose node names - # are already deeper-prefixed. Both need this level's namespace - # applied to node names and variable names, with the variable name - # routed through the local rename map when local, else through the - # outer-prefix when deeper. - node_renames = symbols.get("nodes") or {} - for node_name, refs in imported_instantiated.node_variable_refs.items(): - renamed_node = node_renames.get(node_name) or _prefix(namespace, node_name) - _add_unique_provenance( - module_node_variable_refs, - renamed_node, - { - "os": _rename_variable_ref(refs.get("os"), local_var_renames, namespace), - "count": _rename_variable_ref(refs.get("count"), local_var_renames, namespace), - }, - kind="node variable ref", - source_path=import_path, - ) - for inner_node_name, refs in imported_instantiated.module_node_variable_refs.items(): - renamed_node = _prefix(namespace, inner_node_name) - _add_unique_provenance( - module_node_variable_refs, - renamed_node, - { - "os": _rename_variable_ref(refs.get("os"), local_var_renames, namespace), - "count": _rename_variable_ref(refs.get("count"), local_var_renames, namespace), - }, - kind="node variable ref", - source_path=import_path, - ) namespaced_payload = _namespace_payload( - imported_instantiated.model_dump(mode="python", by_alias=True), - imported_instantiated, + bound.content.model_dump(mode="python", by_alias=True), + bound.content, namespace, + descriptor, ) + budget.check_namespaces(namespaced_payload, path=import_path) merged = _merge_sections(merged, namespaced_payload, path=import_path) - namespaces[str(import_path)] = namespace - namespaces.update(imported_namespaces) - - merged["imports"] = [] - return merged, namespaces, module_variable_specs, module_node_variable_refs - -def _rename_variable_ref( - ref: str | None, - local_var_renames: Mapping[str, str], - namespace: str, -) -> str | None: - """Thin shim binding `_module_provenance.rename_variable_ref` to this - file's local `_prefix` namespace helper. The provenance module accepts - the prefix function as a parameter to avoid a circular import.""" + import_records.append(_resolved_import_record(resolved_import, requested=import_decl, bindings=bound)) + import_records.extend(_prefixed_import_record(record, namespace) for record in inner_provenance.imports) + capability_constraints.extend( + _prefixed_constraint( + constraint, + namespace=namespace, + symbols=symbols, + ) + for constraint in bound.capability_constraints + ) + explicitness_records.extend( + _prefixed_explicitness( + record, + namespace=namespace, + imported=bound.content, + symbols=symbols, + ) + for record in bound.explicitness + ) - return rename_variable_ref(ref, local_var_renames, namespace, prefix=_prefix) + provenance = ExpansionProvenance( + imports=tuple(import_records), + capability_constraints=tuple(capability_constraints), + explicitness=tuple(explicitness_records), + ) + merged.pop("imports", None) + merged.pop("module", None) + merged["expansion_provenance"] = provenance.model_dump(mode="python") + return merged, provenance diff --git a/implementations/python/packages/aces_sdl/conditions.py b/implementations/python/packages/aces_sdl/conditions.py index bde707119..e74cfec44 100644 --- a/implementations/python/packages/aces_sdl/conditions.py +++ b/implementations/python/packages/aces_sdl/conditions.py @@ -1,8 +1,8 @@ -"""Condition models — monitoring checks run on VMs. +"""Legacy probe implementation declarations run on VMs. -A condition is either command-based (command + interval) or -source-based (a library package). The two forms are mutually -exclusive, enforced by a model validator. +A condition is either command-based (command + interval) or source-based (a +library package). It may explicitly bind the proposition it realizes, but its +command/source fields never define that proposition's meaning. """ from pydantic import Field, model_validator @@ -12,12 +12,13 @@ class Condition(SDLModel): - """A monitoring check deployed to a VM. + """A probe implementation deployed to a VM. Either ``command`` + ``interval`` or ``source`` must be set, not both. """ name: str = "" + proposition: str = "" command: str | None = None interval: int | str | None = None timeout: int | str | None = None diff --git a/implementations/python/packages/aces_sdl/content.py b/implementations/python/packages/aces_sdl/content.py index aa8462470..bb1f11b29 100644 --- a/implementations/python/packages/aces_sdl/content.py +++ b/implementations/python/packages/aces_sdl/content.py @@ -15,6 +15,7 @@ from pydantic import Field, field_validator, model_validator from ._base import SDLModel, normalize_enum_value, parse_bool_or_var +from ._identifiers import PortableIdentifier from ._source import Source @@ -29,7 +30,8 @@ class ContentType(str, Enum): class ContentItem(SDLModel): """A single item within a dataset (e.g., one email, one record).""" - name: str + name: PortableIdentifier + display_name: str = "" tags: list[str] = Field(default_factory=list) description: str = "" diff --git a/implementations/python/packages/aces_sdl/entities.py b/implementations/python/packages/aces_sdl/entities.py index 03adc63c0..115402a32 100644 --- a/implementations/python/packages/aces_sdl/entities.py +++ b/implementations/python/packages/aces_sdl/entities.py @@ -10,6 +10,7 @@ from pydantic import Field, field_validator from ._base import SDLModel, parse_enum_or_var +from ._identifiers import PortableIdentifier class ExerciseRole(str, Enum): @@ -43,7 +44,7 @@ def normalize_role(cls, v): vulnerabilities: list[str] = Field(default_factory=list) facts: dict[str, str] = Field(default_factory=dict) events: list[str] = Field(default_factory=list) - entities: dict[str, "Entity"] = Field(default_factory=dict) + entities: dict[PortableIdentifier, "Entity"] = Field(default_factory=dict) def flatten_entities(entities: dict[str, Entity], prefix: str = "") -> dict[str, Entity]: diff --git a/implementations/python/packages/aces_sdl/explicitness.py b/implementations/python/packages/aces_sdl/explicitness.py index d611a3791..559770159 100644 --- a/implementations/python/packages/aces_sdl/explicitness.py +++ b/implementations/python/packages/aces_sdl/explicitness.py @@ -2,7 +2,8 @@ from __future__ import annotations -from collections.abc import Iterable +import re +from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass from enum import Enum @@ -16,12 +17,15 @@ "ExplicitnessProvenance", "ExplicitnessRecord", "ExplicitnessResult", + "classify_authoring_specificity", + "classify_model_explicitness", "classify_scenario_explicitness", "derive_instantiated_explicitness", ] _OPEN_ENUM_SENTINELS = frozenset({"unknown", "other"}) _EXPLICITNESS_ORDER: dict[ExplicitnessClass, int] = {} +_PATH_TOKEN_RE = re.compile(r"[^.\[\]]+|\[\d+\]") class ExplicitnessClass(str, Enum): @@ -71,12 +75,60 @@ class ExplicitnessResult: def classify_scenario_explicitness(scenario: BaseModel) -> ExplicitnessResult: """Classify authored SDL declarations on ``scenario``.""" - variables = getattr(scenario, "variables", {}) + return classify_model_explicitness(scenario) + + +def classify_model_explicitness( + model: BaseModel, + *, + variables: dict[str, Variable] | None = None, +) -> ExplicitnessResult: + """Classify authored declarations on any closed ACES Pydantic model.""" + + variables = variables if variables is not None else getattr(model, "variables", {}) classifier = _ExplicitnessClassifier(variables) - classifier.visit(scenario, "") + classifier.visit(model, "") return ExplicitnessResult(records=dict(classifier.records), errors=tuple(classifier.errors)) +def classify_authoring_specificity( + model: BaseModel, + *, + admitted_open_paths: Iterable[str] = (), + variables: dict[str, Variable] | None = None, +) -> ExplicitnessResult: + """Classify DSL-115 specificity with opt-in open/underspecified paths. + + Missing fields are not open by default. A caller must supply the exact + owning-surface path for any concern whose owning rule explicitly admits an + open or underspecified form. This records authoring review metadata only; + it is not backend-realization permission. + """ + + result = classify_model_explicitness(model, variables=variables) + records = dict(result.records) + errors = list(result.errors) + for path in admitted_open_paths: + normalized_path = path.strip() if isinstance(path, str) else "" + if not normalized_path: + errors.append("Cannot classify open specificity for an empty path") + continue + if not _path_addresses_model_surface(model, normalized_path): + errors.append( + f"Cannot classify open specificity for '{normalized_path}': path does not resolve to a model surface" + ) + continue + records.setdefault( + normalized_path, + ExplicitnessRecord( + path=normalized_path, + classification=ExplicitnessClass.OPEN, + reason="explicitly admitted open/underspecified concern; not backend-realization permission", + ), + ) + return ExplicitnessResult(records=records, errors=tuple(errors)) + + def derive_instantiated_explicitness( raw_scenario: BaseModel, instantiated_scenario: BaseModel, @@ -237,6 +289,76 @@ def _authored_paths(value: object) -> set[str]: return collector.paths +def _path_addresses_model_surface(root: object, path: str) -> bool: + tokens = _path_tokens(path) + if not tokens: + return False + + current = root + for index, token in enumerate(tokens): + is_final = index == len(tokens) - 1 + found, current = _resolve_path_token(current, token, allow_unset_model_field=is_final) + if not found: + return False + return True + + +def _path_tokens(path: str) -> tuple[str | int, ...]: + tokens: list[str | int] = [] + for raw in _PATH_TOKEN_RE.findall(path): + tokens.append(int(raw[1:-1]) if raw.startswith("[") else raw) + return tuple(tokens) + + +def _resolve_path_token( + current: object, + token: str | int, + *, + allow_unset_model_field: bool, +) -> tuple[bool, object]: + resolved: tuple[bool, object] = (False, None) + if isinstance(token, int): + resolved = _resolve_sequence_path_token(current, token) + elif isinstance(current, BaseModel): + resolved = _resolve_model_path_token( + current, + token, + allow_unset_model_field=allow_unset_model_field, + ) + elif isinstance(current, Mapping): + resolved = _resolve_mapping_path_token(current, token) + return resolved + + +def _resolve_sequence_path_token(current: object, token: int) -> tuple[bool, object]: + resolved: tuple[bool, object] = (False, None) + if isinstance(current, Sequence) and not isinstance(current, (str, bytes, bytearray)) and 0 <= token < len(current): + resolved = (True, current[token]) + return resolved + + +def _resolve_model_path_token( + current: BaseModel, + token: str, + *, + allow_unset_model_field: bool, +) -> tuple[bool, object]: + resolved: tuple[bool, object] = (False, None) + if token in type(current).model_fields: + value = getattr(current, token) + missing_unset_optional = value is None and token not in current.model_fields_set + if not missing_unset_optional or allow_unset_model_field: + resolved = (True, value) + return resolved + + +def _resolve_mapping_path_token(current: Mapping[object, object], token: str) -> tuple[bool, object]: + resolved: tuple[bool, object] = (False, None) + if token in current: + resolved = (True, current[token]) + return resolved + + class _AuthoredPathCollector: def __init__(self) -> None: self.paths: set[str] = set() diff --git a/implementations/python/packages/aces_sdl/formatting.py b/implementations/python/packages/aces_sdl/formatting.py new file mode 100644 index 000000000..092b5dba7 --- /dev/null +++ b/implementations/python/packages/aces_sdl/formatting.py @@ -0,0 +1,57 @@ +"""Canonical SDL source formatting and explicit legacy-syntax migration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +import yaml +from pydantic import ValidationError + +from ._errors import SDLParseDiagnostic, SDLParseError +from ._source_profile import DEFAULT_PARSER_LIMITS, SDLMigrationPolicy, SDLParserLimits +from .parser import _load_normalized_data, parse_sdl +from .scenario import Scenario + + +@dataclass(frozen=True) +class SDLFormatResult: + """Canonical source text plus advisories for each migrated construct.""" + + content: str + diagnostics: tuple[SDLParseDiagnostic, ...] + + +def format_sdl_source( + content: str, + *, + path: Path | None = None, + limits: SDLParserLimits = DEFAULT_PARSER_LIMITS, +) -> SDLFormatResult: + """Rewrite recognized migration syntax as strict ``sdl-yaml/v1`` YAML.""" + diagnostics: list[SDLParseDiagnostic] = [] + data = _load_normalized_data( + content, + path=path, + migration_policy=SDLMigrationPolicy.ACCEPT, + limits=limits, + source_diagnostics=diagnostics, + ) + try: + scenario = Scenario(**data) + except ValidationError as exc: + raise SDLParseError(str(exc), path=path) from exc + normalized = scenario.model_dump(mode="json", by_alias=True, exclude_unset=True) + formatted = yaml.safe_dump( + normalized, + allow_unicode=False, + default_flow_style=False, + sort_keys=False, + ) + parse_sdl( + formatted, + path=path, + skip_semantic_validation=True, + limits=limits, + ) + return SDLFormatResult(content=formatted, diagnostics=tuple(diagnostics)) diff --git a/implementations/python/packages/aces_sdl/identifiers.py b/implementations/python/packages/aces_sdl/identifiers.py new file mode 100644 index 000000000..836219937 --- /dev/null +++ b/implementations/python/packages/aces_sdl/identifiers.py @@ -0,0 +1,27 @@ +"""Public portable-identifier and qualified-name contracts.""" + +from ._identifiers import ( + PORTABLE_IDENTIFIER_JSON_SCHEMA, + PORTABLE_IDENTIFIER_PATTERN, + QUALIFIED_IDENTIFIER_MAX_LENGTH, + OptionalPortableIdentifier, + PortableIdentifier, + QualifiedName, + is_portable_identifier, + require_module_identifier, + require_portable_identifier, + require_qualified_identifier, +) + +__all__ = [ + "OptionalPortableIdentifier", + "PORTABLE_IDENTIFIER_JSON_SCHEMA", + "PORTABLE_IDENTIFIER_PATTERN", + "PortableIdentifier", + "QUALIFIED_IDENTIFIER_MAX_LENGTH", + "QualifiedName", + "is_portable_identifier", + "require_module_identifier", + "require_portable_identifier", + "require_qualified_identifier", +] diff --git a/implementations/python/packages/aces_sdl/infrastructure.py b/implementations/python/packages/aces_sdl/infrastructure.py index 1185e0973..98bd2e0b7 100644 --- a/implementations/python/packages/aces_sdl/infrastructure.py +++ b/implementations/python/packages/aces_sdl/infrastructure.py @@ -19,6 +19,7 @@ parse_enum_or_var, parse_int_or_var, ) +from ._identifiers import OptionalPortableIdentifier MINIMUM_NODE_COUNT = 1 DEFAULT_NODE_COUNT = 1 @@ -38,7 +39,7 @@ class ACLRule(SDLModel): traffic rules between network segments. """ - name: str = "" + name: OptionalPortableIdentifier = "" direction: str = "" from_net: str = "" to_net: str = "" diff --git a/implementations/python/packages/aces_sdl/instantiate.py b/implementations/python/packages/aces_sdl/instantiate.py index 65f9f64f5..b320fffdb 100644 --- a/implementations/python/packages/aces_sdl/instantiate.py +++ b/implementations/python/packages/aces_sdl/instantiate.py @@ -9,14 +9,24 @@ import re from collections.abc import Mapping -from typing import Any +from dataclasses import dataclass +from typing import Any, ClassVar -from pydantic import ValidationError +from pydantic import ConfigDict, ValidationError from ._base import VARIABLE_TOKEN_RE, extract_variable_name from ._errors import SDLInstantiationError, SDLValidationError -from .explicitness import derive_instantiated_explicitness -from .scenario import InstantiatedScenario, Scenario +from .canonical import canonical_sdl_digest +from .explicitness import ExplicitnessRecord, derive_instantiated_explicitness +from .phase_contracts import ( + BindingOrigin, + CapabilityConstraint, + ExplicitnessProvenanceRecord, + InstantiationProvenance, + ParameterBinding, + SemanticDigest, +) +from .scenario import ExpandedScenario, InstantiatedScenario, Scenario, ScenarioContent from .validator import SemanticValidator from .variables import Variable, VariableType @@ -24,6 +34,21 @@ JSONLike = JSONScalar | list["JSONLike"] | dict[str, "JSONLike"] +class _BoundScenarioContent(ScenarioContent): + """Private structural result of substitution, before public admission.""" + + _allows_qualified_declaration_keys: ClassVar[bool] = True + model_config = ConfigDict(title="SDL Private Bound Content", extra="forbid") + + +@dataclass(frozen=True) +class _BoundScenarioResult: + content: _BoundScenarioContent + bindings: tuple[ParameterBinding, ...] + capability_constraints: tuple[CapabilityConstraint, ...] + explicitness: tuple[ExplicitnessProvenanceRecord, ...] + + def _matches_value_type(value: object, variable: Variable) -> bool: if variable.type == VariableType.STRING: return isinstance(value, str) @@ -37,17 +62,20 @@ def _matches_value_type(value: object, variable: Variable) -> bool: def _resolve_variable_values( - scenario: Scenario, + scenario: Scenario | ExpandedScenario, parameters: Mapping[str, JSONLike], -) -> tuple[dict[str, JSONLike], list[str]]: +) -> tuple[dict[str, JSONLike], dict[str, BindingOrigin], list[str]]: resolved: dict[str, JSONLike] = {} + origins: dict[str, BindingOrigin] = {} errors: list[str] = [] for name, variable in scenario.variables.items(): if name in parameters: value = parameters[name] + origin = BindingOrigin.PROVIDED elif variable.default is not None: value = variable.default + origin = BindingOrigin.DEFAULT elif variable.required: errors.append(f"Variable '{name}' is required and has no provided value or default.") continue @@ -58,15 +86,16 @@ def _resolve_variable_values( errors.append(f"Variable '{name}' expects type '{variable.type.value}', got {type(value).__name__}.") continue if variable.allowed_values and value not in variable.allowed_values: - errors.append(f"Variable '{name}' must be one of {variable.allowed_values!r}; got {value!r}.") + errors.append(f"Variable '{name}' does not satisfy its allowed_values constraint.") continue resolved[name] = value + origins[name] = origin undeclared = sorted(name for name in parameters if name not in scenario.variables) for name in undeclared: errors.append(f"Instantiation parameter '{name}' is not a declared variable.") - return resolved, errors + return resolved, origins, errors def _substitute_value( @@ -113,65 +142,86 @@ def replace_token(match: re.Match[str]) -> str: return VARIABLE_TOKEN_RE.sub(replace_token, value) -def _capture_node_variable_refs( - scenario: Scenario, -) -> dict[str, dict[str, str | None]]: - """Snapshot `${name}` refs on `nodes.os` and `infrastructure.count`. +def _json_pointer_segment(value: str) -> str: + return value.replace("~", "~0").replace("/", "~1") - Captured before variable substitution so downstream consumers - (typically the runtime planner via `InstantiatedScenario.node_variable_refs`) - can still reach each variable's `allowed_values` after the resolved - values have been written onto the concrete scenario. Covers every node - name regardless of `NodeType` because the runtime planner's - `max_total_nodes` check runs across both networks (switches) and node - deployments. - """ - refs: dict[str, dict[str, str | None]] = {} +def _finite_domain_constraint( + *, + field_pointer: str, + value: object, + variables: Mapping[str, Variable], +) -> CapabilityConstraint | None: + variable_ref = extract_variable_name(value) if isinstance(value, str) else None + variable = variables.get(variable_ref) if variable_ref is not None else None + if variable is None or not variable.allowed_values: + return None + return CapabilityConstraint( + field_pointer=field_pointer, + parameter=(variable_ref,), + allowed_values=tuple(variable.allowed_values), + ) + + +def _capture_capability_constraints( + scenario: Scenario | ExpandedScenario, +) -> tuple[CapabilityConstraint, ...]: + """Retain the finite domains needed by pre-realization capability checks.""" + + constraints: list[CapabilityConstraint] = [] for node_name, node in scenario.nodes.items(): - os_ref = extract_variable_name(node.os) if isinstance(node.os, str) else None - count_ref: str | None = None - infra = scenario.infrastructure.get(node_name) - if infra is not None and isinstance(infra.count, str): - count_ref = extract_variable_name(infra.count) - if os_ref is None and count_ref is None: - continue - refs[node_name] = {"os": os_ref, "count": count_ref} - return refs + constraint = _finite_domain_constraint( + field_pointer=f"/nodes/{_json_pointer_segment(node_name)}/os", + value=node.os, + variables=scenario.variables, + ) + if constraint is not None: + constraints.append(constraint) + for node_name, infrastructure in scenario.infrastructure.items(): + constraint = _finite_domain_constraint( + field_pointer=f"/infrastructure/{_json_pointer_segment(node_name)}/count", + value=infrastructure.count, + variables=scenario.variables, + ) + if constraint is not None: + constraints.append(constraint) + return tuple(constraints) -def instantiate_scenario( - raw_scenario: Scenario, - parameters: Mapping[str, JSONLike] | None = None, - profile: str | None = None, +def _portable_explicitness_record(record: ExplicitnessRecord) -> ExplicitnessProvenanceRecord: + return ExplicitnessProvenanceRecord( + model_path=record.path, + classification=record.classification, + provenance=record.provenance, + reason=record.reason, + parameters=tuple((name,) for name in record.variables), + ) + + +def _safe_model_validation_errors( + exc: ValidationError, *, - validate_semantics: bool | None = None, -) -> InstantiatedScenario: - """Return a fully concrete scenario ready for compilation. + subject: str = "Bound scenario", +) -> list[str]: + diagnostics: list[str] = [] + for error in exc.errors()[:50]: + location = "/" + "/".join(str(segment) for segment in error.get("loc", ())) + error_type = str(error.get("type", "invalid_value")) + diagnostics.append(f"{subject} is invalid at {location or '/'} ({error_type}).") + return diagnostics or [f"{subject} failed structural validation."] - Instantiation applies parameter values and variable defaults, rejects - unresolved placeholders, rebuilds the Pydantic model, and reruns semantic - validation on the concrete result. - """ - effective_parameters = dict(parameters or {}) - # Capture `${var}` refs on `nodes.os` and `infrastructure.count` BEFORE - # substitution so downstream consumers (e.g. the runtime planner's - # capability checks) can still reach each variable's `allowed_values` - # after the resolved values are written onto the concrete scenario. - node_variable_refs = _capture_node_variable_refs(raw_scenario) - # Merge in any module-import provenance attached by `expand_sdl_modules` - # (composition.py). Outer node names take precedence over deeper ones if - # the same key appears in both (cannot happen with namespaced names, but - # guard explicitly to keep the merge predictable). - module_node_refs = raw_scenario.module_node_variable_refs - for node_name, refs in module_node_refs.items(): - node_variable_refs.setdefault(node_name, refs) - module_variable_specs = raw_scenario.module_variable_specs - variable_values, errors = _resolve_variable_values(raw_scenario, effective_parameters) +def _bind_scenario_content( + raw_scenario: Scenario | ExpandedScenario, + parameters: Mapping[str, JSONLike] | None = None, +) -> _BoundScenarioResult: + """Bind one already-expanded object without minting a public phase artifact.""" + + variable_values, binding_origins, errors = _resolve_variable_values(raw_scenario, dict(parameters or {})) if errors: raise SDLInstantiationError(errors) + local_constraints = _capture_capability_constraints(raw_scenario) raw_payload = raw_scenario.model_dump(mode="python", by_alias=True) unresolved_refs: set[str] = set() substituted_payload = _substitute_value( @@ -185,31 +235,102 @@ def instantiate_scenario( [f"Scenario contains unresolved variable references after instantiation: {unresolved_list}."] ) + for authoring_field in ("variables", "imports", "module", "expansion_provenance"): + substituted_payload.pop(authoring_field, None) try: - instantiated = InstantiatedScenario.model_validate(substituted_payload) + content = _BoundScenarioContent.model_validate(substituted_payload) except ValidationError as exc: - raise SDLInstantiationError([str(exc)]) from exc + raise SDLInstantiationError(_safe_model_validation_errors(exc)) from exc - should_validate_semantics = raw_scenario.semantic_validated if validate_semantics is None else validate_semantics - if should_validate_semantics: - validator = SemanticValidator(instantiated) - try: - validator.validate() - except SDLValidationError as exc: - raise SDLInstantiationError(exc.errors) from exc - instantiated._set_advisories(validator.warnings) - instantiated._set_semantic_validated(True) + derived = derive_instantiated_explicitness(raw_scenario, content) + if derived.errors: + raise SDLInstantiationError(list(derived.errors)) + explicitness_by_path = {record.path: _portable_explicitness_record(record) for record in derived.records.values()} + constraints: tuple[CapabilityConstraint, ...] = local_constraints + if isinstance(raw_scenario, ExpandedScenario): + constraints = (*raw_scenario.expansion_provenance.capability_constraints, *local_constraints) + explicitness_by_path.update( + {record.model_path: record for record in raw_scenario.expansion_provenance.explicitness} + ) + + return _BoundScenarioResult( + content=content, + bindings=tuple( + ParameterBinding( + parameter=(name,), + origin=binding_origins[name], + value=value, + ) + for name, value in variable_values.items() + ), + capability_constraints=constraints, + explicitness=tuple(explicitness_by_path.values()), + ) + + +def _validate_authoring_scenario(scenario: Scenario | ExpandedScenario) -> None: + if isinstance(scenario, Scenario) and scenario.imports: + raise SDLInstantiationError(["Scenario imports must be resolved by file-backed parsing before instantiation."]) + validator = SemanticValidator(scenario) + try: + validator.validate() + except SDLValidationError as exc: + raise SDLInstantiationError(list(exc.errors)) from exc + scenario._set_advisories(validator.warnings) + scenario._set_semantic_validated(True) + + +def admit_instantiated_scenario( + artifact: InstantiatedScenario | Mapping[str, object], +) -> InstantiatedScenario: + """Structurally and semantically admit a portable instantiated artifact.""" + + if isinstance(artifact, InstantiatedScenario): + admitted = artifact else: - instantiated._set_advisories(list(raw_scenario.advisories)) - instantiated._set_semantic_validated(False) - instantiated._set_instantiation_context( - parameters=variable_values, - profile=profile, + try: + admitted = InstantiatedScenario.model_validate(artifact) + except ValidationError as exc: + raise SDLInstantiationError(_safe_model_validation_errors(exc, subject="Instantiated artifact")) from exc + validator = SemanticValidator(admitted) + validator.validate() + admitted._set_advisories(validator.warnings) + admitted._set_semantic_validated(True) + return admitted + + +def instantiate_scenario( + raw_scenario: Scenario | ExpandedScenario, + parameters: Mapping[str, JSONLike] | None = None, + profile: str | None = None, +) -> InstantiatedScenario: + """Return a fully concrete scenario ready for compilation. + + Instantiation applies parameter values and variable defaults, rejects + unresolved placeholders, rebuilds the Pydantic model, and reruns semantic + validation on the concrete result. + """ + + _validate_authoring_scenario(raw_scenario) + authored_digest = canonical_sdl_digest(raw_scenario) + bound = _bind_scenario_content(raw_scenario, parameters) + expansion = raw_scenario.expansion_provenance if isinstance(raw_scenario, ExpandedScenario) else None + provenance = InstantiationProvenance( + authored_digest=SemanticDigest(**authored_digest.as_dict()), + selected_profile=profile, + bindings=bound.bindings, + imports=expansion.imports if expansion is not None else (), + capability_constraints=bound.capability_constraints, + explicitness=bound.explicitness, ) - instantiated._set_node_variable_refs(node_variable_refs) - instantiated._set_module_variable_specs(module_variable_specs) - explicitness = derive_instantiated_explicitness(raw_scenario, instantiated) - if explicitness.errors: - raise SDLInstantiationError(list(explicitness.errors)) - instantiated._set_explicitness(explicitness.records) - return instantiated + payload = bound.content.model_dump(mode="python", by_alias=True) + payload["instantiation_provenance"] = provenance.model_dump(mode="python") + + try: + instantiated = InstantiatedScenario.model_validate(payload) + except ValidationError as exc: + raise SDLInstantiationError(_safe_model_validation_errors(exc)) from exc + try: + return admit_instantiated_scenario(instantiated) + except SDLValidationError as exc: + raise SDLInstantiationError(list(exc.errors)) from exc diff --git a/implementations/python/packages/aces_sdl/language_service.py b/implementations/python/packages/aces_sdl/language_service.py index 034a0cfb7..585844a8e 100644 --- a/implementations/python/packages/aces_sdl/language_service.py +++ b/implementations/python/packages/aces_sdl/language_service.py @@ -11,21 +11,25 @@ from typing import Any import yaml +from pydantic import ValidationError +from ._declarations import DeclarationIndex, build_declaration_index from ._errors import SDLParseError, SDLValidationError from ._language_diagnostics import diagnostic as _diagnostic from ._language_diagnostics import invalid as _invalid +from ._language_diagnostics import parse_error as _parse_error from ._language_edit import apply_edit from ._language_metadata import REFERENCE_COMPLETION_TARGETS, SECTION_FIELD_COMPLETIONS from ._language_references import find_references +from ._reference_targetability import is_targetable_section +from .formatting import format_sdl_source from .parser import _load_normalized_data, parse_sdl from .scenario import Scenario _MAX_INPUT_BYTES = 64 * 1024 -_CODE_PARSE = "sdl.parse" - _SCENARIO_METADATA_FIELDS = frozenset({"name", "version", "description", "module", "imports"}) _SECTION_FIELDS = tuple(field for field in Scenario.model_fields if field not in _SCENARIO_METADATA_FIELDS) +_TARGETABLE_SECTION_FIELDS = tuple(field for field in _SECTION_FIELDS if is_targetable_section(field)) _TOP_LEVEL_KEYS = tuple(Scenario.model_fields) @@ -43,11 +47,12 @@ def language_completions( data, error = _load_completion_data(sdl_content) if error is not None: return error + declaration_index = _declaration_index_from_data(data) pointer = _split_pointer_or_empty(cursor_path) target_section = _completion_target_section(pointer) if target_section is not None: - items = _reference_completion_items(data, target_section) + items = _reference_completion_items(data, target_section, declaration_index=declaration_index) context = f"reference:{target_section}" elif len(pointer) <= 1: existing = set(data) if isinstance(data, dict) else set() @@ -86,27 +91,46 @@ def language_references(sdl_content: str, symbol: str) -> dict[str, Any]: if size_error is not None: return size_error - return find_references(sdl_content, symbol, section_fields=_SECTION_FIELDS) + return find_references( + sdl_content, + symbol, + section_fields=_SECTION_FIELDS, + declaration_index=_try_declaration_index(sdl_content), + ) + + +def _try_declaration_index(sdl_content: str) -> DeclarationIndex | None: + """Return the authoritative index for a complete structural document.""" + + try: + data = _load_normalized_data(sdl_content) + except SDLParseError: + return None + return _declaration_index_from_data(data) + + +def _declaration_index_from_data(data: dict[str, Any]) -> DeclarationIndex | None: + try: + scenario = Scenario.model_validate(data) + except ValidationError: + return None + return build_declaration_index(scenario, raise_on_collision=False) def language_format(sdl_content: str) -> dict[str, Any]: - """Return normalized, consistently formatted SDL YAML.""" + """Migrate recognized legacy spellings and return canonical SDL YAML.""" size_error = _size_error(sdl_content) if size_error is not None: return size_error try: - data = _load_normalized_data(sdl_content) + result = format_sdl_source(sdl_content) except SDLParseError as exc: - return _invalid("parse", _CODE_PARSE, exc.details) + return _parse_error(exc) - formatted = yaml.safe_dump( - data, - allow_unicode=False, - default_flow_style=False, - sort_keys=False, - ) - diagnostics = language_diagnostics(formatted)["diagnostics"] + formatted = result.content + diagnostics = [item.as_dict() for item in result.diagnostics] + diagnostics.extend(language_diagnostics(formatted)["diagnostics"]) status = "formatted" if not diagnostics else "formatted_with_diagnostics" return {"status": status, "content": formatted, "diagnostics": diagnostics} @@ -127,7 +151,7 @@ def language_diagnostics( skip_semantic_validation=not semantic_validation, ) except SDLParseError as exc: - return _invalid("parse", _CODE_PARSE, exc.details) + return _parse_error(exc) except SDLValidationError as exc: return { "status": "invalid", @@ -160,7 +184,7 @@ def apply_structured_edit( try: data = _load_normalized_data(sdl_content) except SDLParseError as exc: - return _invalid("parse", _CODE_PARSE, exc.details) + return _parse_error(exc) try: tokens = _split_pointer(pointer) @@ -185,7 +209,7 @@ def _load_completion_data(sdl_content: str) -> tuple[dict[str, Any], dict[str, A try: return _load_normalized_data(sdl_content), None except SDLParseError as exc: - return {}, _invalid("parse", _CODE_PARSE, exc.details) + return {}, _parse_error(exc) def _completion_target_section(pointer: list[str]) -> str | None: @@ -204,9 +228,31 @@ def _completion_target_section(pointer: list[str]) -> str | None: return None -def _reference_completion_items(data: dict[str, Any], target_section: str) -> list[dict[str, str]]: +def _reference_completion_items( + data: dict[str, Any], + target_section: str, + *, + declaration_index: DeclarationIndex | None, +) -> list[dict[str, str]]: + if declaration_index is not None and target_section in {"any", "targetable"}: + return sorted( + ( + { + "label": spelling, + "kind": "reference", + "detail": declaration.address, + "insert_text": spelling, + } + for spelling, declaration in declaration_index.reference_completions( + targetable=target_section == "targetable" + ) + ), + key=lambda item: (item["detail"], item["label"]), + ) if target_section == "any": sections = _SECTION_FIELDS + elif target_section == "targetable": + sections = _TARGETABLE_SECTION_FIELDS elif target_section == "workflow_steps": return _workflow_step_completion_items(data) else: @@ -218,11 +264,14 @@ def _reference_completion_items(data: dict[str, Any], target_section: str) -> li if not isinstance(section_data, dict): continue for name in section_data: + detail = f"{section}.{name}" + if declaration_index is not None and declaration_index.declaration_for(detail) is None: + continue items.append( { "label": str(name), "kind": "reference", - "detail": f"{section}.{name}", + "detail": detail, "insert_text": str(name), } ) diff --git a/implementations/python/packages/aces_sdl/module_registry.py b/implementations/python/packages/aces_sdl/module_registry.py index d9cdd250c..bef4f476b 100644 --- a/implementations/python/packages/aces_sdl/module_registry.py +++ b/implementations/python/packages/aces_sdl/module_registry.py @@ -27,7 +27,8 @@ from pydantic import Field, ValidationError from ._base import SDLModel -from ._errors import SDLParseError +from ._errors import SDLParseDiagnostic, SDLParseError +from ._source_profile import DEFAULT_SOURCE_PARSE_OPTIONS, SDLSourceParseOptions from .scenario import ImportDecl, ModuleDescriptor, Scenario LOCKFILE_NAME = "aces.lock.json" @@ -113,36 +114,9 @@ class ResolvedModule: def _scenario_module_descriptor(scenario: Scenario, *, source_id: str) -> ModuleDescriptor: if scenario.module is not None: return scenario.module - normalized_source_id = source_id.replace("\\", "/") - if "/" not in normalized_source_id: - normalized_source_id = f"local/{normalized_source_id}" - return ModuleDescriptor( - id=normalized_source_id, - version=scenario.version, - parameters=sorted(scenario.variables.keys()), - exports={ - section: sorted(getattr(scenario, section).keys()) - for section in ( - "nodes", - "infrastructure", - "features", - "conditions", - "vulnerabilities", - "entities", - "injects", - "events", - "scripts", - "stories", - "content", - "accounts", - "relationships", - "agents", - "objectives", - "workflows", - ) - if getattr(scenario, section) - }, - description=scenario.description, + raise SDLParseError( + "Imported SDL units require an explicit module descriptor", + path=Path(source_id), ) @@ -550,6 +524,8 @@ def resolve_import( base_dir: Path, lockfile: Lockfile | None = None, trust_policy: TrustPolicy | None = None, + source_options: SDLSourceParseOptions = DEFAULT_SOURCE_PARSE_OPTIONS, + source_diagnostics: list[SDLParseDiagnostic] | None = None, ) -> ResolvedModule: trust_policy = trust_policy or TrustPolicy() source = import_decl.normalized_source @@ -579,6 +555,8 @@ def resolve_import( base_dir=base_dir, lockfile=lockfile, trust_policy=trust_policy, + source_options=source_options, + source_diagnostics=source_diagnostics, ) if source.startswith("local:"): relative = source.removeprefix("local:") @@ -592,6 +570,10 @@ def resolve_import( imported_raw = _load_normalized_data( import_path.read_text(encoding="utf-8"), path=import_path, + source_format=source_options.source_format, + migration_policy=source_options.migration_policy, + limits=source_options.limits, + source_diagnostics=source_diagnostics, ) imported_scenario = Scenario.model_validate(imported_raw) descriptor = _scenario_module_descriptor( @@ -750,7 +732,7 @@ def resolve_lock_records( records.append( LockRecord( source=import_decl.normalized_source, - namespace=import_decl.namespace or resolved.module_descriptor.id.split("/")[-1], + namespace=import_decl.namespace, requested_version=import_decl.version or "*", resolved_source=resolved.resolved_source, module_id=resolved.module_descriptor.id, diff --git a/implementations/python/packages/aces_sdl/nodes.py b/implementations/python/packages/aces_sdl/nodes.py index f54cf0380..5b2460e19 100644 --- a/implementations/python/packages/aces_sdl/nodes.py +++ b/implementations/python/packages/aces_sdl/nodes.py @@ -15,6 +15,7 @@ parse_enum_or_var, parse_int_or_var, ) +from ._identifiers import OptionalPortableIdentifier, PortableIdentifier from ._runtime_service_families import install_runtime_service_family_exports from ._source import Source from .image_provenance import ( @@ -254,7 +255,7 @@ class ServicePort(SDLModel): port: int | str protocol: str = "tcp" - name: str = "" + name: OptionalPortableIdentifier = "" description: str = "" @field_validator("port", mode="before") @@ -286,7 +287,7 @@ def normalize_type(cls, v: str) -> str: conditions: dict[str, str] = Field(default_factory=dict) injects: dict[str, str] = Field(default_factory=dict) vulnerabilities: list[str] = Field(default_factory=list) - roles: dict[str, Role] = Field(default_factory=dict) + roles: dict[PortableIdentifier, Role] = Field(default_factory=dict) services: list[ServicePort] = Field(default_factory=list) asset_value: AssetValue | None = None runtime: RuntimeConfiguration | None = None diff --git a/implementations/python/packages/aces_sdl/objectives.py b/implementations/python/packages/aces_sdl/objectives.py index 0e64b27cc..1ac8bc7bc 100644 --- a/implementations/python/packages/aces_sdl/objectives.py +++ b/implementations/python/packages/aces_sdl/objectives.py @@ -11,31 +11,35 @@ in runtime adapters. """ -from enum import Enum - from pydantic import Field, field_validator, model_validator from ._base import SDLModel, parse_enum_or_var +from .propositions import TruthCompositionMode - -class SuccessMode(str, Enum): - """How referenced success criteria combine.""" - - ALL_OF = "all_of" - ANY_OF = "any_of" +SuccessMode = TruthCompositionMode class ObjectiveSuccess(SDLModel): """Declarative success criteria for an objective. - Per ADR-073, objective success references observable state (``conditions``) - only. The OCR-inherited scoring pipeline (``metrics`` / ``evaluations`` / - ``tlos`` / ``goals``) was removed from the SDL; graded scoring and reward - live in the experiment/evaluator plane (ADR-055/064/069). + Success composes backend-neutral invariant or postcondition assertions. + Probe implementations, graded scoring, and reward remain outside this + construct. """ mode: SuccessMode | str = SuccessMode.ALL_OF - conditions: list[str] = Field(default_factory=list) + assertions: list[str] = Field(default_factory=list) + threshold: int | None = Field(default=None, ge=1) + + @model_validator(mode="before") + @classmethod + def reject_legacy_conditions(cls, value: object) -> object: + if isinstance(value, dict) and "conditions" in value: + raise ValueError( + "objective success.conditions cannot state backend-neutral truth; " + "declare propositions and assertions, then reference success.assertions" + ) + return value @field_validator("mode", mode="before") @classmethod @@ -44,9 +48,16 @@ def normalize_mode(cls, v: str) -> SuccessMode | str: @model_validator(mode="after") def validate_non_empty(self) -> "ObjectiveSuccess": - if self.conditions: - return self - raise ValueError("Objective success must reference at least one condition") + if not self.assertions: + raise ValueError("Objective success must reference at least one assertion") + if self.mode == SuccessMode.AT_LEAST: + if self.threshold is None: + raise ValueError("at_least objective success requires threshold") + if self.threshold > len(self.assertions): + raise ValueError("objective success threshold cannot exceed assertion count") + elif self.threshold is not None: + raise ValueError("objective success threshold is valid only for at_least mode") + return self class ObjectiveWindow(SDLModel): diff --git a/implementations/python/packages/aces_sdl/orchestration.py b/implementations/python/packages/aces_sdl/orchestration.py index a5e830615..f7fdf94dd 100644 --- a/implementations/python/packages/aces_sdl/orchestration.py +++ b/implementations/python/packages/aces_sdl/orchestration.py @@ -1,11 +1,4 @@ -"""Orchestration models — Injects, Events, Scripts, Stories. - -Implements the OCR SDL exercise orchestration pipeline: - Stories -> Scripts -> Events -> { Conditions, Injects } - -Scripts use OCR-compatible human-readable duration strings -(e.g., ``"10min 2 sec"``, ``"1 mon"``, ``"1 us"``). -""" +"""SDL orchestration and workflow models.""" import math import re @@ -21,6 +14,7 @@ parse_float_or_var, parse_int_or_var, ) +from ._identifiers import PortableIdentifier from ._source import Source # OCR uses duration-str's fixed calendar conversions: 30d/month, 365d/year. @@ -70,12 +64,7 @@ def parse_duration(value: str | int | float) -> int | str: - """Parse a human-readable duration string to seconds. - - Accepts integers/floats (treated as seconds) or strings like - ``"10min 2 sec"``, ``"1 week 1day 1h"``, ``"1 mon"``, ``"1 us"``, - ``"1m+30"``, ``"0"``. - """ + """Parse an OCR-compatible human-readable duration into seconds.""" if is_variable_ref(value): return value if isinstance(value, bool): @@ -154,20 +143,24 @@ def validate_entity_pairing(self) -> "Inject": class Event(SDLModel): - """A triggered action combining conditions and injects.""" + """A triggered action combining assertion preconditions and injects.""" name: str = "" source: Source | None = None - conditions: list[str] = Field(default_factory=list) + assertions: list[str] = Field(default_factory=list) injects: list[str] = Field(default_factory=list) description: str = "" + @model_validator(mode="before") + @classmethod + def reject_legacy_conditions(cls, value: object) -> object: + if isinstance(value, dict) and "conditions" in value: + raise ValueError("event conditions cannot state backend-neutral truth; reference precondition assertions") + return value -class Script(SDLModel): - """A timed sequence of events. - Time values are human-readable duration strings parsed to seconds. - """ +class Script(SDLModel): + """A timed sequence of human-readable durations parsed to seconds.""" name: str = "" start_time: int | str @@ -253,7 +246,7 @@ class WorkflowStepStateRef(SDLModel): step: str outcomes: list[WorkflowStepOutcome] = Field(min_length=1) - min_attempts: int | str | None = Field(default=None, alias="min-attempts") + min_attempts: int | str | None = None @field_validator("min_attempts", mode="before") @classmethod @@ -270,28 +263,32 @@ def validate_unique_outcomes(self) -> "WorkflowStepStateRef": class WorkflowPredicate(SDLModel): - """Branch predicate over observable state, objectives, and prior step state. + """Typed branch predicate over assertions, objectives, and prior step state.""" - Per ADR-073 the OCR scoring references (``metrics`` / ``evaluations`` / - ``tlos`` / ``goals``) were removed; a predicate branches on observable - ``conditions``, declared ``objectives``, and prior workflow ``steps``. - """ - - conditions: list[str] = Field(default_factory=list) + assertions: list[str] = Field(default_factory=list) objectives: list[str] = Field(default_factory=list) steps: list[WorkflowStepStateRef] = Field(default_factory=list) + @model_validator(mode="before") + @classmethod + def reject_legacy_conditions(cls, value: object) -> object: + if isinstance(value, dict) and "conditions" in value: + raise ValueError( + "workflow predicate conditions cannot state backend-neutral truth; reference precondition assertions" + ) + return value + @model_validator(mode="after") def validate_non_empty(self) -> "WorkflowPredicate": if any( ( - self.conditions, + self.assertions, self.objectives, self.steps, ) ): return self - raise ValueError("Workflow predicate must reference at least one condition, objective, or step state") + raise ValueError("Workflow predicate must reference at least one assertion, objective, or step state") class WorkflowSwitchCase(SDLModel): @@ -345,7 +342,6 @@ class WorkflowCompensationPolicy(SDLModel): on: list[WorkflowCompensationTrigger] = Field(default_factory=list) failure_policy: WorkflowCompensationFailurePolicy = Field( default=WorkflowCompensationFailurePolicy.FAIL_WORKFLOW, - alias="failure_policy", ) order: str = "reverse_completion" @@ -373,9 +369,9 @@ class WorkflowStep(SDLModel): type: WorkflowStepType = Field(alias="type") objective: str = "" next: str = "" - on_success: str = Field(default="", alias="on-success") - on_failure: str = Field(default="", alias="on-failure") - on_exhausted: str = Field(default="", alias="on-exhausted") + on_success: str = "" + on_failure: str = "" + on_exhausted: str = "" when: WorkflowPredicate | None = None then_step: str = Field(default="", alias="then") else_step: str = Field(default="", alias="else") @@ -384,8 +380,8 @@ class WorkflowStep(SDLModel): branches: list[str] = Field(default_factory=list) join: str = "" workflow: str = "" - compensate_with: str = Field(default="", alias="compensate-with") - max_attempts: int | str | None = Field(default=None, alias="max-attempts") + compensate_with: str = "" + max_attempts: int | str | None = None description: str = "" @field_validator("type", mode="before") @@ -411,7 +407,7 @@ def parse_max_attempts(cls, v: int | str | None) -> int | str | None: def validate_type_specific_fields(self) -> "WorkflowStep": if self.type == WorkflowStepType.OBJECTIVE: if not self.objective or not self.on_success: - raise ValueError("Objective workflow step requires 'objective' and 'on-success'") + raise ValueError("Objective workflow step requires 'objective' and 'on_success'") if ( self.next or self.on_exhausted @@ -579,7 +575,7 @@ class Workflow(SDLModel): start: str timeout: WorkflowTimeoutPolicy | None = None compensation: WorkflowCompensationPolicy | None = None - steps: dict[str, WorkflowStep] = Field(min_length=1) + steps: dict[PortableIdentifier, WorkflowStep] = Field(min_length=1) @field_validator("timeout", mode="before") @classmethod diff --git a/implementations/python/packages/aces_sdl/parser.py b/implementations/python/packages/aces_sdl/parser.py index 2a5bb81d8..36ab3aa9e 100644 --- a/implementations/python/packages/aces_sdl/parser.py +++ b/implementations/python/packages/aces_sdl/parser.py @@ -1,93 +1,68 @@ -"""SDL parser — YAML loading with key normalization and shorthand expansion. +"""SDL parser — canonical YAML loading and typed normalization. Provides ``parse_sdl()`` as the primary entry point. Handles: -- Case-insensitive key normalization (``Name`` → ``name``) -- Hyphen-to-underscore conversion (``min-score`` → ``min_score``) +- Exact canonical structural fields by default +- Explicitly requested migration of legacy field spellings - Shorthand expansion (``source: "pkg"`` → ``{name: "pkg", version: "*"}``) """ -import textwrap from pathlib import Path -from typing import Any +from typing import Any, Literal -import yaml from pydantic import ValidationError from ._base import contains_variable_token, is_variable_ref -from ._errors import SDLParseError, SDLValidationError +from ._errors import ( + SDLParseDiagnostic, + SDLParseError, + SDLSourcePosition, + SDLSourceRange, + SDLValidationError, +) +from ._mapping_scopes import ( + HASHMAP_SECTIONS, + NESTED_HASHMAP_FIELDS, + MappingScope, + is_literal_map_field, + normalize_field_key, +) +from ._model_diagnostics import ( + _dedupe_source_diagnostics, + _model_parse_error, +) +from ._source_profile import ( + DEFAULT_PARSER_LIMITS, + SDL_SOURCE_FORMAT, + SDLMigrationPolicy, + SDLParserLimits, + SDLSourceParseOptions, +) +from ._yaml_loader import load_sdl_yaml from .scenario import ExpandedScenario, Scenario from .validator import SemanticValidator # Top-level sections that are HashMaps of user-defined identifiers. # Keys inside these are scenario-author names (e.g., "web-server") # and must NOT be transformed. -_HASHMAP_SECTIONS = frozenset( - { - "nodes", - "infrastructure", - "features", - "conditions", - "vulnerabilities", - "entities", - "injects", - "events", - "scripts", - "stories", - "content", - "accounts", - "relationships", - "agents", - "action_contracts", - "observation_boundaries", - "outcome_interpretation_rules", - "behavior_specifications", - "evidence_requirements", - "objectives", - "workflows", - "variables", - } -) +_HASHMAP_SECTIONS = HASHMAP_SECTIONS # Fields within struct models that are also HashMaps of user-defined keys. -_NESTED_HASHMAP_FIELDS = frozenset( - { - "features", # VM.features (dict[str, str]) - "conditions", # VM.conditions (dict[str, str]) - "injects", # VM.injects (dict[str, str]) - "roles", # Node.roles (dict[str, Role]) - "log_options", # RuntimeContainerConfiguration.log_options preserves native engine option keys - "labels", # ImageConfig.labels preserves case-sensitive native image label keys - "driver_options", # RuntimeNetworkBackendDetail.driver_options preserves native network driver keys - "ipam_options", # RuntimeNetworkBackendDetail.ipam_options preserves native IPAM driver keys - "facts", # Entity.facts (dict[str, str]) - "entities", # Entity.entities (dict[str, Entity]) - "events", # Script.events (dict[str, int]) - "steps", # Workflow.steps (dict[str, WorkflowStep]) - # ParticipantBehaviorSpecification.extensions preserves governed x-owner:term keys. - "extensions", - } -) +_NESTED_HASHMAP_FIELDS = NESTED_HASHMAP_FIELDS def _child_is_hashmap_field(key: str, value: Any) -> bool: """Return whether the children of ``key`` are user-defined hashmap keys.""" - if key in _HASHMAP_SECTIONS: - return isinstance(value, dict) - if key in _NESTED_HASHMAP_FIELDS: - return True - # Complex properties use list items like ``[{switch-name: "10.0.0.10"}]``. - return key == "properties" and isinstance(value, list) + return is_literal_map_field( + key, + value_is_mapping=isinstance(value, dict), + value_is_sequence=isinstance(value, list), + ) def _normalize_field_key(k: Any) -> Any: - """Normalize a Pydantic field key: lowercase + hyphens to underscores.""" - # PyYAML's YAML 1.1 rules can coerce bare keys like ``on``/``off`` to bools. - # SDL field keys are schema-defined strings, so normalize those legacy bool - # coercions back into the field names we actually support. - if isinstance(k, bool): - return "on" if k else "off" + """Return the normalized representation of a structural field key.""" if isinstance(k, str): - return k.lower().replace("-", "_") + return normalize_field_key(k) return k @@ -120,6 +95,30 @@ def _normalize_keys(data: Any, is_hashmap: bool = False) -> Any: return data +def load_sdl_fragment( + content: str, + *, + mapping_keys: Literal["structural", "literal"] = "structural", + base_pointer: str = "", + source_format: str = SDL_SOURCE_FORMAT, + migration_policy: SDLMigrationPolicy | str = SDLMigrationPolicy.REJECT, + limits: SDLParserLimits = DEFAULT_PARSER_LIMITS, + source_diagnostics: list[SDLParseDiagnostic] | None = None, +) -> object: + """Safely load an SDL YAML fragment with the canonical key preflight.""" + return load_sdl_yaml( + content, + scope=MappingScope(mapping_keys), + base_pointer=base_pointer, + source_options=SDLSourceParseOptions( + source_format=source_format, + migration_policy=migration_policy, + limits=limits, + ), + source_diagnostics=source_diagnostics, + ) + + def _reject_variable_mapping_keys( data: Any, *, @@ -195,8 +194,8 @@ def _reject_removed_scoring_sections(data: dict[str, Any], *, path: Path | None) raise SDLParseError( "SDL scoring sections " f"{', '.join(present)} were removed from the language by ADR-073. " - "Express objective success against observable state via " - "'objectives.*.success.conditions', and route graded scoring, reward, " + "Express objective success through backend-neutral propositions and " + "'objectives.*.success.assertions', and route graded scoring, reward, " "and evaluation outputs to the experiment/evaluator plane " "(ADR-055/064/069). The CybORG 'agents.*.reward_calculator' label was " "removed for the same reason.", @@ -274,6 +273,9 @@ def parse_sdl( path: Path | None = None, *, skip_semantic_validation: bool = False, + source_format: str = SDL_SOURCE_FORMAT, + migration_policy: SDLMigrationPolicy | str = SDLMigrationPolicy.REJECT, + limits: SDLParserLimits = DEFAULT_PARSER_LIMITS, ) -> Scenario: """Parse an SDL YAML string into a validated Scenario. @@ -286,6 +288,10 @@ def parse_sdl( path: Optional file path for error messages. skip_semantic_validation: If True, only run Pydantic structural validation (useful for partial scenarios during development). + source_format: Versioned concrete-syntax profile identifier. + migration_policy: Strict rejection or explicit acceptance of recognized + legacy field/merge syntax with retained diagnostics. + limits: Source and alias-processing resource limits. Returns: Validated Scenario object. @@ -294,10 +300,18 @@ def parse_sdl( SDLParseError: If YAML parsing fails or the data isn't a dict. SDLValidationError: If semantic validation finds errors. """ - data = _load_normalized_data(content, path=path) + source_diagnostics: list[SDLParseDiagnostic] = [] + source_ranges: dict[str, SDLSourceRange] = {} + data = _load_normalized_data( + content, + path=path, + source_format=source_format, + migration_policy=migration_policy, + limits=limits, + source_diagnostics=source_diagnostics, + source_ranges=source_ranges, + ) _reject_removed_scoring_sections(data, path=path) - module_variable_specs: dict[str, dict[str, object]] = {} - module_node_variable_refs: dict[str, dict[str, str | None]] = {} if data.get("imports"): if path is None: raise SDLParseError( @@ -306,13 +320,15 @@ def parse_sdl( ) from .composition import expand_sdl_modules - ( + data, _expansion_provenance = expand_sdl_modules( data, - namespaces, - module_variable_specs, - module_node_variable_refs, - ) = expand_sdl_modules(data, path=path) - scenario_cls = ExpandedScenario if namespaces else Scenario + path=path, + source_format=source_format, + migration_policy=migration_policy, + limits=limits, + source_diagnostics=source_diagnostics, + ) + scenario_cls = ExpandedScenario else: scenario_cls = Scenario @@ -320,12 +336,11 @@ def parse_sdl( try: scenario = scenario_cls(**data) except ValidationError as e: - raise SDLParseError(str(e), path=path) from e + raise _model_parse_error(e, path=path, source_ranges=source_ranges) from e - # Attach the module-import capability-variable provenance BEFORE semantic - # validation so downstream `instantiate_scenario` can propagate it. - scenario._set_module_variable_specs(module_variable_specs) - scenario._set_module_node_variable_refs(module_node_variable_refs) + source_diagnostics = _dedupe_source_diagnostics(source_diagnostics) + + scenario._set_source_diagnostics(source_diagnostics) # Semantic validation if not skip_semantic_validation: @@ -340,9 +355,6 @@ def parse_sdl( else: scenario._set_advisories([]) scenario._set_semantic_validated(False) - if isinstance(scenario, ExpandedScenario): - scenario._set_module_namespaces(locals().get("namespaces", {})) - return scenario @@ -354,60 +366,42 @@ def parse_sdl_file(path: Path, **kwargs: Any) -> Scenario: if not path.exists(): raise FileNotFoundError(f"SDL file not found: {path}") - content = path.read_text(encoding="utf-8") - data = _load_normalized_data(content, path=path) - namespaces: dict[str, str] = {} - module_variable_specs: dict[str, dict[str, object]] = {} - module_node_variable_refs: dict[str, dict[str, str | None]] = {} - if data.get("imports"): - from .composition import expand_sdl_modules - - ( - data, - namespaces, - module_variable_specs, - module_node_variable_refs, - ) = expand_sdl_modules(data, path=path) - scenario_cls = ExpandedScenario if namespaces else Scenario try: - scenario = scenario_cls(**data) - except ValidationError as e: - raise SDLParseError(str(e), path=path) from e - - scenario._set_module_variable_specs(module_variable_specs) - scenario._set_module_node_variable_refs(module_node_variable_refs) - - skip_semantic_validation = bool(kwargs.pop("skip_semantic_validation", False)) - if not skip_semantic_validation: - validator = SemanticValidator(scenario) - try: - validator.validate() - except SDLValidationError as e: - e.path = path - raise - scenario._set_advisories(validator.warnings) - scenario._set_semantic_validated(True) - else: - scenario._set_advisories([]) - scenario._set_semantic_validated(False) - if isinstance(scenario, ExpandedScenario): - scenario._set_module_namespaces(namespaces) - return scenario + content = path.read_text(encoding="utf-8") + except UnicodeDecodeError as exc: + position = SDLSourcePosition(1, 1) + diagnostic = SDLParseDiagnostic( + code="sdl.utf8", + message="SDL source must be valid UTF-8.", + pointer="", + primary_range=SDLSourceRange(start=position, end=position), + source=str(path), + ) + raise SDLParseError(diagnostic.message, path=path, diagnostics=(diagnostic,)) from exc + return parse_sdl(content, path=path, **kwargs) def _load_normalized_data( content: str, *, path: Path | None = None, + source_format: str = SDL_SOURCE_FORMAT, + migration_policy: SDLMigrationPolicy | str = SDLMigrationPolicy.REJECT, + limits: SDLParserLimits = DEFAULT_PARSER_LIMITS, + source_diagnostics: list[SDLParseDiagnostic] | None = None, + source_ranges: dict[str, SDLSourceRange] | None = None, ) -> dict[str, Any]: - content = textwrap.dedent(content).strip() - if not content: - raise SDLParseError("SDL content is empty", path=path) - - try: - raw = yaml.safe_load(content) - except yaml.YAMLError as e: - raise SDLParseError(f"Invalid YAML: {e}", path=path) from e + raw = load_sdl_yaml( + content, + path=path, + source_options=SDLSourceParseOptions( + source_format=source_format, + migration_policy=migration_policy, + limits=limits, + ), + source_diagnostics=source_diagnostics, + source_ranges=source_ranges, + ) if not isinstance(raw, dict): raise SDLParseError("SDL must be a YAML mapping (not a scalar or list)", path=path) diff --git a/implementations/python/packages/aces_sdl/phase_contracts.py b/implementations/python/packages/aces_sdl/phase_contracts.py new file mode 100644 index 000000000..46c3d317c --- /dev/null +++ b/implementations/python/packages/aces_sdl/phase_contracts.py @@ -0,0 +1,301 @@ +"""Portable records for SDL phase derivation claims and replay context.""" + +from __future__ import annotations + +import re +from collections.abc import Hashable, Iterable +from enum import Enum +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field, model_validator + +from ._base import SDLModel +from ._identifiers import PortableIdentifier, QualifiedName, require_module_identifier +from .explicitness import ExplicitnessClass, ExplicitnessProvenance + +_DIGEST_PATTERN = r"^sha256:[a-f0-9]{64}$" +_JSON_POINTER_RE = re.compile(r"^(?:/(?:[^~/]|~[01])*)*$") +JSONScalar = str | int | float | bool + + +class FrozenPhaseModel(SDLModel): + """Closed immutable value object used inside portable phase artifacts.""" + + model_config = ConfigDict(extra="forbid", frozen=True, allow_inf_nan=False) + + +class SemanticDigest(FrozenPhaseModel): + """Profile-labelled digest of one expanded authoring scenario.""" + + profile: Literal["aces-sdl-semantic/v1"] + algorithm: Literal["sha256"] + value: Annotated[str, Field(pattern=_DIGEST_PATTERN)] + + +class BindingOrigin(str, Enum): + """Why a concrete parameter value was selected.""" + + PROVIDED = "provided" + DEFAULT = "default" + + +class ParameterBinding(FrozenPhaseModel): + """One selected parameter value with an unambiguous qualified identity.""" + + parameter: tuple[PortableIdentifier, ...] = Field(min_length=1, max_length=32) + origin: BindingOrigin + value: JSONScalar + + +class ResolvedImportProvenance(FrozenPhaseModel): + """Portable evidence emitted after one trusted module resolution.""" + + namespace: tuple[PortableIdentifier, ...] = Field(min_length=1, max_length=31) + requested_source: str = Field(min_length=1, max_length=2048) + requested_version: str = Field(default="*", min_length=1, max_length=256) + requested_digest: str = Field(default="", max_length=71) + module_id: str = Field(max_length=129) + module_version: str = Field(min_length=1, max_length=256) + resolved_source: str = Field(min_length=1, max_length=4096) + manifest_digest: str = Field(default="", max_length=71) + content_digest: str = Field(default="", max_length=71) + export_hash: str = Field(default="", max_length=71) + signer_id: str = Field(default="", max_length=256) + bindings: tuple[ParameterBinding, ...] = () + + @model_validator(mode="after") + def _validate_record(self) -> ResolvedImportProvenance: + QualifiedName.parse(".".join(self.namespace)) + require_module_identifier(self.module_id, field_name="module_id") + identities = [binding.parameter for binding in self.bindings] + if len(identities) != len(set(identities)): + raise ValueError("resolved import bindings must have unique parameter identities") + if any(len(identity) != 1 for identity in identities): + raise ValueError("resolved import bindings must use module-local parameter identities") + for field_name in ("requested_digest", "manifest_digest", "content_digest", "export_hash"): + digest = getattr(self, field_name) + if digest and re.fullmatch(_DIGEST_PATTERN, digest) is None: + raise ValueError(f"{field_name} must be empty or a sha256 digest") + for field_name in ("requested_source", "resolved_source"): + source = getattr(self, field_name) + if _is_absolute_host_source(source): + raise ValueError(f"{field_name} must not contain an absolute host path") + if _contains_registry_userinfo(source): + raise ValueError(f"{field_name} must not contain registry credentials") + return self + + +class CapabilityConstraint(FrozenPhaseModel): + """Pre-instantiation constraint retained for one concrete field.""" + + kind: Literal["allowed-values"] = "allowed-values" + field_pointer: str = Field(max_length=4096) + parameter: tuple[PortableIdentifier, ...] = Field(min_length=1, max_length=32) + allowed_values: tuple[JSONScalar, ...] = Field( + min_length=1, + json_schema_extra={"uniqueItems": True}, + ) + + @model_validator(mode="after") + def _validate_constraint(self) -> CapabilityConstraint: + if not self.field_pointer.startswith("/") or _JSON_POINTER_RE.fullmatch(self.field_pointer) is None: + raise ValueError("field_pointer must be a non-root RFC 6901 JSON Pointer") + parts = self.field_pointer.split("/") + if len(parts) != 4 or (parts[1], parts[3]) not in { + ("nodes", "os"), + ("infrastructure", "count"), + }: + raise ValueError("field_pointer must address /nodes//os or /infrastructure//count") + declaration = parts[2].replace("~1", "/").replace("~0", "~") + QualifiedName.parse(declaration) + for index, value in enumerate(self.allowed_values): + if any(_json_value_equal(value, prior) for prior in self.allowed_values[:index]): + raise ValueError("allowed_values must not contain duplicates") + return self + + +class ExplicitnessProvenanceRecord(FrozenPhaseModel): + """Serializable SEM-218 classification for one instantiated model path.""" + + model_path: str = Field(min_length=1, max_length=4096) + classification: ExplicitnessClass + provenance: ExplicitnessProvenance = ExplicitnessProvenance.AUTHOR_DECLARED + reason: str = Field(default="", max_length=1024) + parameters: tuple[tuple[PortableIdentifier, ...], ...] = () + + @model_validator(mode="after") + def _validate_parameters(self) -> ExplicitnessProvenanceRecord: + if len(self.parameters) != len(set(self.parameters)): + raise ValueError("explicitness parameters must be unique") + return self + + +class ExpansionProvenance(FrozenPhaseModel): + """Trusted import-resolution facts attached to an expanded authoring object.""" + + imports: tuple[ResolvedImportProvenance, ...] = () + capability_constraints: tuple[CapabilityConstraint, ...] = () + explicitness: tuple[ExplicitnessProvenanceRecord, ...] = () + + @model_validator(mode="after") + def _validate_unique_identities(self) -> ExpansionProvenance: + _validate_derivation_collections( + imports=self.imports, + constraints=self.capability_constraints, + explicitness=self.explicitness, + ) + return self + + +class InstantiationProvenance(FrozenPhaseModel): + """Complete portable derivation context for an instantiated scenario.""" + + authored_digest: SemanticDigest + selected_profile: str | None = Field(default=None, max_length=256) + bindings: tuple[ParameterBinding, ...] = () + imports: tuple[ResolvedImportProvenance, ...] = () + capability_constraints: tuple[CapabilityConstraint, ...] = () + explicitness: tuple[ExplicitnessProvenanceRecord, ...] = () + + @model_validator(mode="after") + def _validate_unique_identities(self) -> InstantiationProvenance: + binding_ids = [binding.parameter for binding in self.bindings] + if len(binding_ids) != len(set(binding_ids)): + raise ValueError("instantiation bindings must have unique parameter identities") + if any(len(identity) != 1 for identity in binding_ids): + raise ValueError("root bindings must use local parameter identities") + _validate_derivation_collections( + imports=self.imports, + constraints=self.capability_constraints, + explicitness=self.explicitness, + root_bindings=self.bindings, + ) + return self + + @property + def root_binding_values(self) -> dict[str, JSONScalar]: + """Selected root bindings; imported bindings remain on import records.""" + + return {".".join(binding.parameter): binding.value for binding in self.bindings} + + +def _validate_derivation_collections( + *, + imports: tuple[ResolvedImportProvenance, ...], + constraints: tuple[CapabilityConstraint, ...], + explicitness: tuple[ExplicitnessProvenanceRecord, ...], + root_bindings: tuple[ParameterBinding, ...] = (), +) -> None: + _require_unique( + (record.namespace for record in imports), + "resolved imports must have unique namespace paths", + ) + _require_unique( + (constraint.field_pointer for constraint in constraints), + "capability constraints must address unique concrete fields", + ) + _require_unique( + (record.model_path for record in explicitness), + "explicitness records must have unique model paths", + ) + + binding_values = _binding_environment(imports, root_bindings) + _validate_constraint_bindings(constraints, binding_values) + _validate_explicitness_bindings(explicitness, binding_values) + + +def _require_unique(values: Iterable[Hashable], message: str) -> None: + materialized = tuple(values) + if len(materialized) != len(set(materialized)): + raise ValueError(message) + + +def _binding_environment( + imports: tuple[ResolvedImportProvenance, ...], + root_bindings: tuple[ParameterBinding, ...], +) -> dict[tuple[str, ...], JSONScalar]: + binding_values = {binding.parameter: binding.value for binding in root_bindings} + for record in imports: + for binding in record.bindings: + identity = (*record.namespace, *binding.parameter) + if identity in binding_values: + raise ValueError("resolved parameter identities must be globally unique") + binding_values[identity] = binding.value + return binding_values + + +def _validate_constraint_bindings( + constraints: tuple[CapabilityConstraint, ...], + binding_values: dict[tuple[str, ...], JSONScalar], +) -> None: + constraints_by_parameter: dict[tuple[str, ...], tuple[JSONScalar, ...]] = {} + for constraint in constraints: + if constraint.parameter not in binding_values: + raise ValueError("capability constraint references an unresolved parameter identity") + if not any( + _json_value_equal(binding_values[constraint.parameter], allowed) for allowed in constraint.allowed_values + ): + raise ValueError("resolved parameter value does not satisfy its capability constraint") + prior_values = constraints_by_parameter.setdefault(constraint.parameter, constraint.allowed_values) + if not _json_domain_equal(prior_values, constraint.allowed_values): + raise ValueError("one parameter identity must not carry conflicting capability constraints") + + +def _validate_explicitness_bindings( + explicitness: tuple[ExplicitnessProvenanceRecord, ...], + binding_values: dict[tuple[str, ...], JSONScalar], +) -> None: + for record in explicitness: + unknown = [identity for identity in record.parameters if identity not in binding_values] + if unknown: + raise ValueError("explicitness record references an unresolved parameter identity") + + +def _json_value_equal(left: JSONScalar, right: JSONScalar) -> bool: + """Compare scalar JSON values without Python's ``True == 1`` collapse.""" + + if isinstance(left, bool) or isinstance(right, bool): + return isinstance(left, bool) and isinstance(right, bool) and left == right + if isinstance(left, (int, float)) and isinstance(right, (int, float)): + return left == right + return type(left) is type(right) and left == right + + +def _json_domain_equal( + left: tuple[JSONScalar, ...], + right: tuple[JSONScalar, ...], +) -> bool: + return len(left) == len(right) and all( + any(_json_value_equal(candidate, other) for other in right) for candidate in left + ) + + +def _is_absolute_host_source(source: str) -> bool: + candidate = source + for prefix in ("local:", "file:"): + if candidate.startswith(prefix): + candidate = candidate.removeprefix(prefix) + break + return candidate.startswith(("/", "\\", "~/", "~\\")) or re.match(r"^[A-Za-z]:[\\/]", candidate) is not None + + +def _contains_registry_userinfo(source: str) -> bool: + if not source.startswith("oci:") and "@sha256:" not in source: + return False + candidate = source.removeprefix("oci:") + if "://" in candidate: + candidate = candidate.split("://", 1)[1] + authority = candidate.split("/", 1)[0] + return "@" in authority + + +__all__ = [ + "BindingOrigin", + "CapabilityConstraint", + "ExpansionProvenance", + "ExplicitnessProvenanceRecord", + "InstantiationProvenance", + "ParameterBinding", + "ResolvedImportProvenance", + "SemanticDigest", +] diff --git a/implementations/python/packages/aces_sdl/propositions.py b/implementations/python/packages/aces_sdl/propositions.py new file mode 100644 index 000000000..ad642bb74 --- /dev/null +++ b/implementations/python/packages/aces_sdl/propositions.py @@ -0,0 +1,178 @@ +"""Backend-neutral proposition and assertion authoring models.""" + +from __future__ import annotations + +import math +from enum import Enum +from typing import Annotated, Literal + +from pydantic import Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator, model_validator + +from ._base import SDLModel + +ObservableProperty = Annotated[ + str, + Field( + min_length=1, + max_length=512, + pattern=r"^[a-z0-9][a-z0-9_-]{0,63}(?:\.[a-z0-9][a-z0-9_-]{0,63})*$", + ), +] +SemanticReference = Annotated[ + str, + Field( + min_length=1, + max_length=2048, + pattern=r"^(?:https://[^\s]+|urn:[A-Za-z0-9][A-Za-z0-9:._~-]*)$", + ), +] + + +class PropositionBasis(str, Enum): + """Source of facts that may decide a proposition.""" + + DECLARED_STATE = "declared_state" + OBSERVED_STATE = "observed_state" + + +class SubjectQuantifier(str, Enum): + """Quantification over a proposition's finite, resolved subject set.""" + + ALL = "all" + ANY = "any" + AT_LEAST = "at_least" + + +class AssertionRole(str, Enum): + """Boundary at which an assertion constrains its owning construct.""" + + PRECONDITION = "precondition" + INVARIANT = "invariant" + POSTCONDITION = "postcondition" + + +class AssertionPolarity(str, Enum): + """Expected polarity of the referenced proposition.""" + + POSITIVE = "positive" + NEGATIVE = "negative" + + +class TruthCompositionMode(str, Enum): + """Portable composition over assertion outcomes.""" + + ALL_OF = "all_of" + ANY_OF = "any_of" + AT_LEAST = "at_least" + + +class PresencePredicate(SDLModel): + """Test whether a governed property is present on a subject.""" + + kind: Literal["presence"] = "presence" + property: ObservableProperty + semantic_ref: SemanticReference + operator: Literal["exists"] = "exists" + + +class BooleanPredicate(SDLModel): + """Compare a governed Boolean property with an expected Boolean.""" + + kind: Literal["boolean"] = "boolean" + property: ObservableProperty + semantic_ref: SemanticReference + operator: Literal["equals", "not_equals"] = "equals" + expected: StrictBool + + +class StringPredicate(SDLModel): + """Compare a governed string property with typed string operands.""" + + kind: Literal["string"] = "string" + property: ObservableProperty + semantic_ref: SemanticReference + operator: Literal["equals", "not_equals", "in", "not_in"] = "equals" + expected: StrictStr | list[StrictStr] + + @model_validator(mode="after") + def validate_operand_shape(self) -> StringPredicate: + membership = self.operator in {"in", "not_in"} + if membership and not isinstance(self.expected, list): + raise ValueError(f"string operator {self.operator!r} requires a list operand") + if not membership and isinstance(self.expected, list): + raise ValueError(f"string operator {self.operator!r} requires a scalar operand") + if isinstance(self.expected, list) and (not self.expected or len(set(self.expected)) != len(self.expected)): + raise ValueError("string membership operands must be non-empty and unique") + return self + + +class NumericPredicate(SDLModel): + """Compare a governed numeric property using an explicit unit.""" + + kind: Literal["number"] = "number" + property: ObservableProperty + semantic_ref: SemanticReference + operator: Literal[ + "equals", + "not_equals", + "less_than", + "less_than_or_equal", + "greater_than", + "greater_than_or_equal", + ] = "equals" + expected: StrictInt | StrictFloat + unit: ObservableProperty + unit_semantic_ref: SemanticReference + + @field_validator("expected") + @classmethod + def reject_boolean_operand(cls, value: int | float) -> int | float: + if isinstance(value, bool): + raise ValueError("numeric predicate expected value must not be Boolean") + if not math.isfinite(value): + raise ValueError("numeric predicate expected value must be finite") + return value + + +TypedPredicate = Annotated[ + PresencePredicate | BooleanPredicate | StringPredicate | NumericPredicate, + Field(discriminator="kind"), +] + + +class Proposition(SDLModel): + """A side-effect-free claim over a finite set of stable SDL subjects.""" + + description: str = Field(min_length=1) + subjects: list[str] = Field(min_length=1) + basis: PropositionBasis + predicate: TypedPredicate + quantifier: SubjectQuantifier = SubjectQuantifier.ALL + threshold: int | None = Field(default=None, ge=1) + evidence_requirements: list[str] = Field(default_factory=list) + + @model_validator(mode="after") + def validate_semantic_shape(self) -> Proposition: + if len(set(self.subjects)) != len(self.subjects): + raise ValueError("proposition subjects must be unique") + if len(set(self.evidence_requirements)) != len(self.evidence_requirements): + raise ValueError("proposition evidence requirements must be unique") + if self.basis is PropositionBasis.OBSERVED_STATE and not self.evidence_requirements: + raise ValueError("observed-state propositions require at least one evidence requirement") + if self.quantifier is SubjectQuantifier.AT_LEAST: + if self.threshold is None: + raise ValueError("at_least quantification requires threshold") + if self.threshold > len(self.subjects): + raise ValueError("threshold cannot exceed the finite subject count") + elif self.threshold is not None: + raise ValueError("threshold is valid only for at_least quantification") + return self + + +class Assertion(SDLModel): + """A typed use of one proposition at a governed semantic boundary.""" + + description: str = "" + proposition: str + role: AssertionRole + polarity: AssertionPolarity = AssertionPolarity.POSITIVE diff --git a/implementations/python/packages/aces_sdl/realization_envelope.py b/implementations/python/packages/aces_sdl/realization_envelope.py new file mode 100644 index 000000000..649b85c3f --- /dev/null +++ b/implementations/python/packages/aces_sdl/realization_envelope.py @@ -0,0 +1,405 @@ +"""Realization-envelope relation: membership, subsumption, witness, negative probes. + +One deterministic semantic relation over :class:`RealizationEnvelopeModel` and +validated SDL instances (ADR-070 §2, ``specs/formal/realization/envelope-semantics.md`` +R1-R8). Membership, subsumption, witness generation, and negative-probe generation +share a single flattening/closure engine (:mod:`aces_sdl._realization_envelope_engine`) +and domain-kind dispatch (:mod:`aces_sdl._realization_envelope_domains`) — there are +no separate author-side, backend-side, or conformance-side interpretations. + +Guarantees: + +- **Membership (R1)** — a concrete instance is in an envelope only when it is + structurally and semantically valid SDL with no unresolved variables *and* every + effective binding and closed-world scope is satisfied. Invalid SDL is never a + member. +- **Subsumption (R4)** — ``subsumes(offered, requested)`` is set inclusion reduced + to bounded per-path domain-subset, closed-scope key-set, and closure-compatibility + checks. No sampling, approximation, solver, or backend probing. +- **Witness (R5)** — ``witness`` deterministically selects one candidate from the + envelope's own bindings (no externally supplied scenario) and then runs the + ordinary ``parse``/``instantiate``/``validate`` pipeline. A witness is one + executable in-envelope instance; it is not proof of subsumption or backend honesty. +- **Negative probes (R6)** — ``generate_negative_probes`` derives out-of-envelope + variants of a valid witness for the closed dimensions that can be varied, so + conformance can require refusal. + +Diagnostics name the relation kind, envelope id, SDL path, and domain kind — never +raw sensitive values (R8 / ADR-070 §7). +""" + +from __future__ import annotations + +from copy import deepcopy +from dataclasses import dataclass + +from aces_contracts.diagnostics import Diagnostic, Severity +from aces_contracts.realization_envelope import Posture, RealizationEnvelopeModel, WitnessPolicy, scalar_in_domain +from pydantic import ValidationError + +from ._errors import SDLInstantiationError, SDLValidationError +from ._realization_envelope_domains import _MISSING, domain_subset, out_of_domain_value +from ._realization_envelope_engine import ( + LeafConstraint, + assign_path, + effective_constraints, + fresh_extra_key, + navigate, + normalize_scalar, + overridability_violations, + present_children, + remove_path, + tokenize_path, + witness_value, +) +from .instantiate import instantiate_scenario +from .scenario import InstantiatedScenario, Scenario +from .validator import SemanticValidator + +__all__ = [ + "NegativeProbe", + "RelationKind", + "RelationResult", + "WitnessResult", + "generate_negative_probes", + "member", + "subsumes", + "witness", +] + +_DOMAIN = "realization-envelope" + + +class RelationKind: + """Relation kind labels used in diagnostic codes.""" + + MEMBERSHIP = "membership" + SUBSUMPTION = "subsumption" + WITNESS = "witness" + NEGATIVE_PROBE = "negative-probe" + + +@dataclass(frozen=True) +class RelationResult: + """Result of a membership or subsumption evaluation.""" + + holds: bool + diagnostics: tuple[Diagnostic, ...] = () + + +@dataclass(frozen=True) +class WitnessResult: + """A generated witness scenario, or diagnostics proving none can be generated.""" + + scenario: InstantiatedScenario | None + diagnostics: tuple[Diagnostic, ...] = () + + +@dataclass(frozen=True) +class NegativeProbe: + """An out-of-envelope variant of a valid witness for a closed dimension.""" + + path: str + domain_kind: str + variation: str + payload: dict[str, object] + + +def _diag(code: str, address: str, message: str, severity: Severity = Severity.ERROR) -> Diagnostic: + return Diagnostic(code=f"{_DOMAIN}.{code}", domain=_DOMAIN, address=address, message=message, severity=severity) + + +def _envelope_r2_diagnostics(envelope: RealizationEnvelopeModel) -> tuple[Diagnostic, ...]: + """R2 well-formedness: reject a more-specific binding that widens a fixed value. + + An envelope that violates the overridability rule is ill-formed; the relation + answers deny (not a member / no witness) rather than silently resolving against + a broader-than-authored set. + """ + + return tuple( + _diag( + "invalid.non-overrideable-widen", path, "a more-specific binding widens a non-overrideable inherited value" + ) + for path in overridability_violations(envelope) + ) + + +# --------------------------------------------------------------------------- # +# Membership (R1, R3) # +# --------------------------------------------------------------------------- # + + +def _member_sdl_invalid(instance: InstantiatedScenario, envelope: RealizationEnvelopeModel) -> RelationResult | None: + """Return a deny result when the instance is not semantically valid SDL (R1).""" + + if getattr(instance, "semantic_validated", False): + return None + try: + SemanticValidator(instance).validate() + except SDLValidationError: + return RelationResult( + False, + (_diag(f"{RelationKind.MEMBERSHIP}.invalid-sdl", envelope.id, "instance failed SDL semantic validation"),), + ) + return None + + +def _member_constraint_diagnostics( + instance: InstantiatedScenario, constraints: dict[str, LeafConstraint] +) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + for path, constraint in constraints.items(): + found, value = navigate(instance, tokenize_path(path)) + if not found or value is None: + # A constrained/exact dimension left unspecified (missing field, or an + # optional field defaulting to ``None``) is not a member. Domains never + # admit ``None`` (they range over scalars), so this is always absence. + diagnostics.append( + _diag(f"{RelationKind.MEMBERSHIP}.path-absent", path, "constrained path is unspecified in the instance") + ) + elif not scalar_in_domain(normalize_scalar(value), constraint.domain): + diagnostics.append( + _diag( + f"{RelationKind.MEMBERSHIP}.domain-mismatch", + path, + f"value is not in the {constraint.domain.kind} domain", + ) + ) + return diagnostics + + +_UNRESOLVED = object() + + +def _resolve_scope_value(instance: InstantiatedScenario, scope_path: str) -> object: + """Value at ``scope_path`` (the whole instance for the root), or ``_UNRESOLVED``.""" + + if not scope_path: + return instance + found, value = navigate(instance, tokenize_path(scope_path)) + return value if found else _UNRESOLVED + + +def _closed_extra_diagnostics(scope_path: str, scope_value: object, admitted: set[str]) -> list[Diagnostic]: + """Diagnostics for realizable child dimensions not admitted under a closed scope.""" + + diagnostics: list[Diagnostic] = [] + for child in sorted(present_children(scope_value)): + if child not in admitted: + address = f"{scope_path}.{child}" if scope_path else child + diagnostics.append( + _diag( + f"{RelationKind.MEMBERSHIP}.closed-world-extra", + address, + "closed-world scope admits no unspecified realizable dimension", + ) + ) + return diagnostics + + +def _member_closed_diagnostics(instance: InstantiatedScenario, closed: dict[str, set[str]]) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + for scope_path in sorted(closed): + scope_value = _resolve_scope_value(instance, scope_path) + if scope_value is not _UNRESOLVED: + diagnostics.extend(_closed_extra_diagnostics(scope_path, scope_value, closed[scope_path])) + return diagnostics + + +def member(instance: InstantiatedScenario, envelope: RealizationEnvelopeModel) -> RelationResult: + """Decide whether ``instance`` is a member of ``envelope`` (R1).""" + + invalid = _envelope_r2_diagnostics(envelope) + if invalid: + return RelationResult(False, invalid) + + sdl_invalid = _member_sdl_invalid(instance, envelope) + if sdl_invalid is not None: + return sdl_invalid + + constraints, closed = effective_constraints(envelope) + diagnostics = _member_constraint_diagnostics(instance, constraints) + _member_closed_diagnostics(instance, closed) + return RelationResult(not diagnostics, tuple(diagnostics)) + + +# --------------------------------------------------------------------------- # +# Subsumption (R4) # +# --------------------------------------------------------------------------- # + + +def _subsumption_domain_diagnostics( + offered: dict[str, LeafConstraint], requested: dict[str, LeafConstraint] +) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + for path in sorted(set(offered) | set(requested)): + offered_constraint = offered.get(path) + if offered_constraint is None: + # offered is open/universal here: it admits any requested value + continue + requested_constraint = requested.get(path) + if requested_constraint is None: + diagnostics.append( + _diag( + f"{RelationKind.SUBSUMPTION}.requested-unconstrained", + path, + "requested leaves a path open that offered constrains", + ) + ) + elif not domain_subset(requested_constraint.domain, offered_constraint.domain): + diagnostics.append( + _diag( + f"{RelationKind.SUBSUMPTION}.domain-not-subset", + path, + "requested domain is not a subset of the offered domain", + ) + ) + return diagnostics + + +def _subsumption_closure_diagnostics(offered: dict[str, set[str]], requested: dict[str, set[str]]) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + for scope_path in sorted(offered): + label = scope_path or "" + if scope_path not in requested: + diagnostics.append( + _diag( + f"{RelationKind.SUBSUMPTION}.closure-mismatch", + label, + "offered is closed-world where requested is open-world", + ) + ) + elif not requested[scope_path].issubset(offered[scope_path]): + diagnostics.append( + _diag( + f"{RelationKind.SUBSUMPTION}.closed-extra", + label, + "requested admits a closed dimension offered does not", + ) + ) + return diagnostics + + +def subsumes(offered: RealizationEnvelopeModel, requested: RealizationEnvelopeModel) -> RelationResult: + """Decide whether every scenario in ``requested`` is in ``offered`` (R4).""" + + invalid = _envelope_r2_diagnostics(offered) + _envelope_r2_diagnostics(requested) + if invalid: + return RelationResult(False, invalid) + + offered_constraints, offered_closed = effective_constraints(offered) + requested_constraints, requested_closed = effective_constraints(requested) + diagnostics = _subsumption_domain_diagnostics( + offered_constraints, requested_constraints + ) + _subsumption_closure_diagnostics(offered_closed, requested_closed) + return RelationResult(not diagnostics, tuple(diagnostics)) + + +# --------------------------------------------------------------------------- # +# Witness generation (R5) # +# --------------------------------------------------------------------------- # + + +def _build_witness_payload( + envelope: RealizationEnvelopeModel, policy: WitnessPolicy | None +) -> tuple[dict[str, object], list[Diagnostic]]: + effective_policy = policy if policy is not None else envelope.witness_policy + constraints, _closed = effective_constraints(envelope) + payload: dict[str, object] = {} + diagnostics: list[Diagnostic] = [] + for path in sorted(constraints): + value, error = witness_value(constraints[path], effective_policy) + if error is None: + error = assign_path(payload, tokenize_path(path), value) + if error is not None: + diagnostics.append(_diag(f"{RelationKind.WITNESS}.no-witness", path, error)) + return payload, diagnostics + + +def _validate_witness_payload(payload: dict[str, object], envelope: RealizationEnvelopeModel) -> WitnessResult: + try: + raw = Scenario.model_validate(payload) + raw._set_semantic_validated(False) + instantiated = instantiate_scenario(raw) + except (ValidationError, SDLValidationError, SDLInstantiationError): + return WitnessResult( + None, + ( + _diag( + f"{RelationKind.WITNESS}.invalid", + envelope.id, + "generated witness did not pass SDL structural/semantic validation " + "(the envelope does not fully determine a valid scenario instance)", + ), + ), + ) + return WitnessResult(instantiated, ()) + + +def witness(envelope: RealizationEnvelopeModel, policy: WitnessPolicy | None = None) -> WitnessResult: + """Deterministically derive one in-envelope scenario instance (R5).""" + + invalid = _envelope_r2_diagnostics(envelope) + if invalid: + return WitnessResult(None, invalid) + payload, diagnostics = _build_witness_payload(envelope, policy) + if diagnostics: + return WitnessResult(None, tuple(diagnostics)) + return _validate_witness_payload(payload, envelope) + + +# --------------------------------------------------------------------------- # +# Negative probes (R6) # +# --------------------------------------------------------------------------- # + + +def _value_probes_for(base_payload: dict[str, object], path: str, constraint: LeafConstraint) -> list[NegativeProbe]: + probes: list[NegativeProbe] = [] + variation_value = out_of_domain_value(constraint.domain) + if variation_value is not _MISSING: + payload = deepcopy(base_payload) + if assign_path(payload, tokenize_path(path), variation_value) is None: + probes.append(NegativeProbe(path, constraint.domain.kind, "value-outside-domain", payload)) + if constraint.posture is Posture.EXACT: + omitted = deepcopy(base_payload) + if remove_path(omitted, tokenize_path(path)): + probes.append(NegativeProbe(path, constraint.domain.kind, "omitted-required-exact", omitted)) + return probes + + +def _closed_scope_probes(base_payload: dict[str, object], closed: dict[str, set[str]]) -> list[NegativeProbe]: + probes: list[NegativeProbe] = [] + for scope_path in sorted(closed): + extra_key = fresh_extra_key(closed[scope_path]) + payload = deepcopy(base_payload) + tokens = tokenize_path(scope_path) + [extra_key] if scope_path else [extra_key] + if assign_path(payload, tokens, "out-of-envelope") is None: + address = f"{scope_path}.{extra_key}" if scope_path else extra_key + probes.append(NegativeProbe(address, "closed-scope", "extra-dimension", payload)) + return probes + + +def generate_negative_probes( + envelope: RealizationEnvelopeModel, +) -> tuple[tuple[NegativeProbe, ...], tuple[Diagnostic, ...]]: + """Derive out-of-envelope probes for the envelope's closed dimensions (R6).""" + + base = witness(envelope) + if base.scenario is None: + return (), ( + _diag( + f"{RelationKind.NEGATIVE_PROBE}.no-witness", + envelope.id, + "cannot derive a witness base for negative probes", + ), + ) + + # ``mode="json"`` yields plain JSON scalars (enums as their string value), so + # each probe payload is a portable, re-parseable scenario request. + base_payload = base.scenario.model_dump(mode="json", by_alias=True) + constraints, closed = effective_constraints(envelope) + probes: list[NegativeProbe] = [] + for path in sorted(constraints): + probes.extend(_value_probes_for(base_payload, path, constraints[path])) + probes.extend(_closed_scope_probes(base_payload, closed)) + return tuple(probes), () diff --git a/implementations/python/packages/aces_sdl/runtime_forwarding_agent.py b/implementations/python/packages/aces_sdl/runtime_forwarding_agent.py index eae006f05..b7e092082 100644 --- a/implementations/python/packages/aces_sdl/runtime_forwarding_agent.py +++ b/implementations/python/packages/aces_sdl/runtime_forwarding_agent.py @@ -26,6 +26,7 @@ from pydantic import Field, ValidationInfo, field_validator, model_validator from ._base import SDLModel, is_variable_ref, parse_int_or_var +from ._identifiers import require_qualified_identifier from .runtime_forwarding_agent_vocab import ( RuntimeForwardingAgentImplementation, RuntimeForwardingAgentKind, @@ -306,7 +307,7 @@ class RuntimeForwardingAgent(SDLModel): @field_validator("forwarding_agent_id") @classmethod def validate_forwarding_agent_id(cls, v: str) -> str: - return require_symbol(v, field_name="forwarding_agent_id") + return require_qualified_identifier(v, field_name="forwarding_agent_id") @field_validator("implementation", mode="before") @classmethod diff --git a/implementations/python/packages/aces_sdl/runtime_values.py b/implementations/python/packages/aces_sdl/runtime_values.py index 21f87f6e7..134e80f00 100644 --- a/implementations/python/packages/aces_sdl/runtime_values.py +++ b/implementations/python/packages/aces_sdl/runtime_values.py @@ -11,6 +11,7 @@ parse_bool_or_var, parse_enum_or_var, ) +from ._identifiers import require_portable_identifier _BYTE_UNITS = { "b": 1, @@ -181,11 +182,7 @@ def require_symbol(value: str, *, field_name: str) -> str: Symbol-defining ids are reference targets, so they must be concrete: empty values and ``${var}`` placeholders are rejected. """ - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"{field_name} must be a non-empty string") - if is_variable_ref(value): - raise ValueError(f"{field_name} must be a stable identifier, not a variable placeholder") - return value + return require_portable_identifier(value, field_name=field_name) def absolute_path_or_var(value: str, *, field_name: str) -> str: diff --git a/implementations/python/packages/aces_sdl/scenario.py b/implementations/python/packages/aces_sdl/scenario.py index 077e60d01..d16ec8bd6 100644 --- a/implementations/python/packages/aces_sdl/scenario.py +++ b/implementations/python/packages/aces_sdl/scenario.py @@ -13,11 +13,19 @@ """ from collections.abc import Mapping -from typing import Annotated +from typing import ClassVar -from pydantic import Field, PrivateAttr, StringConstraints, model_validator +from pydantic import ConfigDict, Field, PrivateAttr, model_validator -from ._base import VARIABLE_NAME_PATTERN, VARIABLE_TOKEN_RE, SDLModel +from ._base import VARIABLE_TOKEN_RE, SDLModel +from ._errors import SDLParseDiagnostic +from ._identifiers import ( + PortableIdentifier, + QualifiedName, + require_module_identifier, + require_portable_identifier, +) +from ._mapping_scopes import HASHMAP_SECTIONS from .accounts import Account from .agents import Agent from .conditions import Condition @@ -36,13 +44,57 @@ ) from .participant_behavior_specification import ParticipantBehaviorSpecification from .participant_outcome_semantics import OutcomeInterpretationRule +from .phase_contracts import ( + CapabilityConstraint, + ExpansionProvenance, + InstantiationProvenance, + _json_value_equal, +) +from .propositions import Assertion, Proposition from .relationships import Relationship from .runtime_forwarding_agent import RuntimeForwardingAgent from .variables import Variable from .vulnerabilities import Vulnerability -VariableName = Annotated[str, StringConstraints(pattern=f"^{VARIABLE_NAME_PATTERN}$")] -VariableDefinitions = dict[VariableName, Variable] +VariableName = PortableIdentifier +VariableDefinitions = dict[PortableIdentifier, Variable] + + +def _legacy_constraint_name(parameter: tuple[str, ...]) -> str: + """Project a typed identity into the pre-#724 private planner key.""" + + if len(parameter) == 1: + return parameter[0] + return ".".join((*parameter[:-1], "__private", parameter[-1])) + + +def _constraint_variable_specs( + constraints: tuple[CapabilityConstraint, ...], +) -> dict[str, dict[str, object]]: + return { + _legacy_constraint_name(constraint.parameter): { + "allowed_values": list(constraint.allowed_values), + } + for constraint in constraints + } + + +def _constraint_node_refs( + constraints: tuple[CapabilityConstraint, ...], +) -> dict[str, dict[str, str | None]]: + refs: dict[str, dict[str, str | None]] = {} + for constraint in constraints: + parts = constraint.field_pointer.split("/") + if len(parts) != 4 or parts[1] not in {"nodes", "infrastructure"}: + continue + field_name = parts[3] + if (parts[1], field_name) not in {("nodes", "os"), ("infrastructure", "count")}: + continue + node_name = parts[2].replace("~1", "/").replace("~0", "~") + refs.setdefault(node_name, {"os": None, "count": None})[field_name] = _legacy_constraint_name( + constraint.parameter + ) + return refs def _collect_variable_tokens(value: object) -> list[str]: @@ -64,19 +116,98 @@ def _collect_variable_tokens(value: object) -> list[str]: return found +def _validate_declaration_identifier( + identifier: object, + *, + section_name: str, + allow_qualified: bool, +) -> None: + if allow_qualified: + local_name = QualifiedName.parse(identifier).parts[-1] + else: + local_name = require_portable_identifier(identifier, field_name=f"{section_name} declaration key") + if section_name == "nodes" and len(local_name) > 35: + raise ValueError("nodes declaration key must be at most 35 characters") + + +def _validate_section_declaration_keys(value: Mapping[object, object], *, allow_qualified: bool) -> None: + for section_name in HASHMAP_SECTIONS: + declarations = value.get(section_name) + if not isinstance(declarations, Mapping): + continue + for identifier in declarations: + _validate_declaration_identifier( + identifier, + section_name=section_name, + allow_qualified=allow_qualified, + ) + + +def _forwarding_agent_identifier(agent: object) -> object: + if isinstance(agent, RuntimeForwardingAgent): + return agent.forwarding_agent_id + if isinstance(agent, Mapping): + return agent.get("forwarding_agent_id") + return None + + +def _validate_forwarding_agent_identifiers( + agents: object, + *, + allow_qualified: bool, + field_name: str, +) -> None: + if not isinstance(agents, (list, tuple)): + return + for agent in agents: + identifier = _forwarding_agent_identifier(agent) + if allow_qualified: + QualifiedName.parse(identifier) + else: + require_portable_identifier(identifier, field_name=field_name) + + +def _node_runtime(node: object) -> object | None: + if isinstance(node, Node): + return node.runtime + if isinstance(node, Mapping): + return node.get("runtime") + return None + + +def _runtime_forwarding_agents(runtime: object) -> object: + if isinstance(runtime, Mapping): + return runtime.get("forwarding_agents", ()) + return getattr(runtime, "forwarding_agents", ()) + + +def _validate_runtime_forwarding_agent_identifiers(value: Mapping[object, object]) -> None: + nodes = value.get("nodes") + if not isinstance(nodes, Mapping): + return + for node in nodes.values(): + runtime = _node_runtime(node) + if runtime is None: + continue + _validate_forwarding_agent_identifiers( + _runtime_forwarding_agents(runtime), + allow_qualified=False, + field_name="runtime forwarding_agent_id", + ) + + class ModuleDescriptor(SDLModel): """Published module metadata for SDL composition.""" id: str version: str - parameters: list[str] = Field(default_factory=list) + parameters: list[PortableIdentifier] = Field(default_factory=list) exports: dict[str, list[str]] = Field(default_factory=dict) description: str = "" @model_validator(mode="after") def validate_descriptor(self) -> "ModuleDescriptor": - if "/" not in self.id or self.id.startswith("/") or self.id.endswith("/"): - raise ValueError("module.id must use canonical 'publisher/name' format") + require_module_identifier(self.id) if len(self.parameters) != len(set(self.parameters)): raise ValueError("module.parameters must be unique") for section, names in self.exports.items(): @@ -92,7 +223,7 @@ class ImportDecl(SDLModel): path: str = "" namespace: str = "" version: str = "*" - parameters: dict[str, object] = Field(default_factory=dict) + parameters: dict[PortableIdentifier, object] = Field(default_factory=dict) digest: str = "" @model_validator(mode="after") @@ -101,11 +232,9 @@ def validate_source_fields(self) -> "ImportDecl": raise ValueError("Import requires either 'source' or deprecated 'path'") if self.source and self.path: raise ValueError("Import may specify only one of 'source' or 'path'") - if self.path and not self.namespace: - # Local path imports remain backward compatible and may derive namespace. - return self - if self.source.startswith("oci:") and not self.namespace: - raise ValueError("OCI imports require an explicit namespace") + if not self.namespace: + raise ValueError("Import requires an explicit namespace") + require_portable_identifier(self.namespace, field_name="namespace") return self @property @@ -115,26 +244,23 @@ def normalized_source(self) -> str: return f"local:{self.path}" -class Scenario(SDLModel): - """Top-level scenario specification. +class ScenarioContent(SDLModel): + """Executable SDL content shared by the closed document-phase types.""" - A YAML document with up to 23 named sections. Only ``name`` - is required. All sections are optional dicts keyed by - user-defined identifiers. - """ + _allows_qualified_declaration_keys: ClassVar[bool] = False # --- Identity --- - name: str + name: PortableIdentifier version: str = "*" description: str = "" - module: ModuleDescriptor | None = None - imports: list[ImportDecl] = Field(default_factory=list) - # --- OCR SDL: 14 sections --- + # OCR-derived topology and exercise-narrative sections. nodes: dict[str, Node] = Field(default_factory=dict) infrastructure: dict[str, InfraNode] = Field(default_factory=dict) features: dict[str, Feature] = Field(default_factory=dict) conditions: dict[str, Condition] = Field(default_factory=dict) + propositions: dict[str, Proposition] = Field(default_factory=dict) + assertions: dict[str, Assertion] = Field(default_factory=dict) vulnerabilities: dict[str, Vulnerability] = Field(default_factory=dict) entities: dict[str, Entity] = Field(default_factory=dict) injects: dict[str, Inject] = Field(default_factory=dict) @@ -155,25 +281,26 @@ class Scenario(SDLModel): evidence_requirements: dict[str, EvidenceRequirement] = Field(default_factory=dict) objectives: dict[str, Objective] = Field(default_factory=dict) workflows: dict[str, Workflow] = Field(default_factory=dict) - variables: VariableDefinitions = Field( - default_factory=dict, - json_schema_extra={"additionalProperties": False}, - ) - _advisories: list[str] = PrivateAttr(default_factory=list) + _source_diagnostics: tuple[SDLParseDiagnostic, ...] = PrivateAttr(default=()) _semantic_validated: bool = PrivateAttr(default=False) - # Capability-variable provenance carried across the SDL module-import - # composition boundary. The composition pass strips imported variables - # from the merged payload by design, so this side-channel preserves the - # imported variable specs (namespace-prefixed) and the imported nodes' - # `${name}` refs onto the outer scenario. The instantiation and runtime- - # compile paths read these to enforce backend `allowed_values` against - # imported modules even though their variables don't survive the merge. - # Inner mapping shape mirrors `InstantiatedScenario._node_variable_refs`. - _module_variable_specs: dict[str, dict[str, object]] = PrivateAttr(default_factory=dict) - _module_node_variable_refs: dict[str, dict[str, str | None]] = PrivateAttr(default_factory=dict) _explicitness: dict[str, ExplicitnessRecord] = PrivateAttr(default_factory=dict) + @model_validator(mode="before") + @classmethod + def _validate_declaration_keys(cls, value: object) -> object: + if not isinstance(value, Mapping): + return value + allow_qualified = cls._allows_qualified_declaration_keys + _validate_section_declaration_keys(value, allow_qualified=allow_qualified) + _validate_forwarding_agent_identifiers( + value.get("forwarding_agents", ()), + allow_qualified=allow_qualified, + field_name="forwarding_agent_id", + ) + _validate_runtime_forwarding_agent_identifiers(value) + return value + @property def advisories(self) -> list[str]: """Non-fatal SDL advisories gathered during semantic validation.""" @@ -182,6 +309,14 @@ def advisories(self) -> list[str]: def _set_advisories(self, advisories: list[str]) -> None: self._advisories = list(advisories) + @property + def source_diagnostics(self) -> tuple[SDLParseDiagnostic, ...]: + """Non-fatal source migration diagnostics retained after parsing.""" + return self._source_diagnostics + + def _set_source_diagnostics(self, diagnostics: list[SDLParseDiagnostic]) -> None: + self._source_diagnostics = tuple(diagnostics) + @property def semantic_validated(self) -> bool: """Whether full semantic validation has already run on this scenario.""" @@ -198,24 +333,73 @@ def explicitness(self) -> dict[str, ExplicitnessRecord]: def _set_explicitness(self, explicitness: dict[str, ExplicitnessRecord]) -> None: self._explicitness = dict(explicitness) + +class Scenario(ScenarioContent): + """Normalized SDL authoring object. + + This model applies after ``sdl-yaml/v1`` source-profile checks, structural + key canonicalization, shorthand expansion, enum normalization, and typed + construction, but before module expansion and instantiation. Its JSON + Schema does not validate YAML presentation details. + """ + + 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, + }, + ) + + module: ModuleDescriptor | None = None + imports: list[ImportDecl] = Field(default_factory=list) + variables: VariableDefinitions = Field( + default_factory=dict, + json_schema_extra={"additionalProperties": False}, + ) + @property def module_variable_specs(self) -> dict[str, dict[str, object]]: - """Namespace-prefixed variable specs preserved across module imports.""" - return {name: dict(spec) for name, spec in self._module_variable_specs.items()} + return {} @property def module_node_variable_refs(self) -> dict[str, dict[str, str | None]]: - """Refs on imported-module nodes that survived the composition merge.""" - return {name: dict(refs) for name, refs in self._module_node_variable_refs.items()} + return {} - def _set_module_variable_specs(self, specs: dict[str, dict[str, object]]) -> None: - self._module_variable_specs = {name: dict(spec) for name, spec in specs.items()} - def _set_module_node_variable_refs(self, refs: dict[str, dict[str, str | None]]) -> None: - self._module_node_variable_refs = {name: dict(entry) for name, entry in refs.items()} +class ExpandedScenario(ScenarioContent): + """Internal authoring object after trusted import expansion.""" + + _allows_qualified_declaration_keys: ClassVar[bool] = True + + model_config = ConfigDict( + title="SDL Expanded Authoring Object v1", + json_schema_extra={"x-aces-document-phase": "expanded-authoring-object"}, + ) + + variables: VariableDefinitions = Field( + default_factory=dict, + json_schema_extra={"additionalProperties": False}, + ) + expansion_provenance: ExpansionProvenance = Field(default_factory=ExpansionProvenance) + @property + def module_variable_specs(self) -> dict[str, dict[str, object]]: + """Compatibility projection of imported allowed-value constraints.""" + return _constraint_variable_specs(self.expansion_provenance.capability_constraints) -class InstantiatedScenario(Scenario): + @property + def module_node_variable_refs(self) -> dict[str, dict[str, str | None]]: + """Compatibility projection retained until compiler migration completes.""" + return _constraint_node_refs(self.expansion_provenance.capability_constraints) + + @property + def module_namespaces(self) -> dict[str, str]: + return {record.resolved_source: ".".join(record.namespace) for record in self.expansion_provenance.imports} + + +class InstantiatedScenario(ScenarioContent): """Scenario with all ``${var}`` references resolved to concrete values. Unlike the authoring-input contract, an instantiated scenario MUST NOT @@ -224,66 +408,74 @@ class InstantiatedScenario(Scenario): (``"host-${index}"``). The invariant is enforced both by the model validator below and by the published ``instantiated-scenario-v1`` JSON Schema, which forbids the token in every string field. The schema is - marginally stricter than the runtime instantiation engine in one - pathological case: if a resolved variable *value* itself re-introduces a - literal ``${name}`` sequence (single-pass substitution does not re-scan - it), the schema and this validator treat it as non-concrete and reject it. + If a resolved variable value itself introduces a literal ``${name}`` + sequence, the single-pass substitution step does not interpret it as a + second substitution request; final model admission still treats the result + as non-concrete and rejects the public instantiation. """ - _instantiation_parameters: dict[str, object] = PrivateAttr(default_factory=dict) - _instantiation_profile: str | None = PrivateAttr(default=None) - # Snapshot of pre-instantiation `${name}` refs on `nodes.os` and - # `infrastructure.count`, captured by `instantiate_scenario` before - # substitution. Carries downstream so the runtime processor / planner - # can reach each variable's `allowed_values` after the resolved values - # have been written onto the concrete scenario. - _node_variable_refs: dict[str, dict[str, str | None]] = PrivateAttr(default_factory=dict) + _allows_qualified_declaration_keys: ClassVar[bool] = True - @property - def instantiation_parameters(self) -> dict[str, object]: - """Concrete parameter values used during instantiation.""" - return dict(self._instantiation_parameters) + 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", + }, + ) - @property - def instantiation_profile(self) -> str | None: - """Optional instantiation profile name.""" - return self._instantiation_profile + instantiation_provenance: InstantiationProvenance = Field( + json_schema_extra={"x-aces-realization-dimension": False}, + ) @property - def node_variable_refs(self) -> dict[str, dict[str, str | None]]: - """Pre-instantiation `${var}` refs on `nodes.os` / `infrastructure.count`.""" - return {name: dict(refs) for name, refs in self._node_variable_refs.items()} - - def _set_instantiation_context( - self, - *, - parameters: dict[str, object], - profile: str | None, - ) -> None: - self._instantiation_parameters = dict(parameters) - self._instantiation_profile = profile - - def _set_node_variable_refs(self, refs: dict[str, dict[str, str | None]]) -> None: - self._node_variable_refs = {name: dict(entry) for name, entry in refs.items()} + def explicitness(self) -> dict[str, ExplicitnessRecord]: + """Portable SEM-218 records keyed by SDL model path.""" + + return { + record.model_path: ExplicitnessRecord( + path=record.model_path, + classification=record.classification, + provenance=record.provenance, + reason=record.reason, + variables=tuple(".".join(parameter) for parameter in record.parameters), + ) + for record in self.instantiation_provenance.explicitness + } @model_validator(mode="after") def _reject_unresolved_variable_references(self) -> "InstantiatedScenario": - payload = self.model_dump(mode="python", by_alias=True) + payload = self.model_dump(mode="json", by_alias=True) tokens = sorted(set(_collect_variable_tokens(payload))) if tokens: joined = ", ".join(tokens) raise ValueError(f"InstantiatedScenario must not contain unresolved variable references: {joined}") + binding_values = {binding.parameter: binding.value for binding in self.instantiation_provenance.bindings} + for imported in self.instantiation_provenance.imports: + for binding in imported.bindings: + binding_values[(*imported.namespace, *binding.parameter)] = binding.value + for constraint in self.instantiation_provenance.capability_constraints: + try: + concrete_value = _resolve_json_pointer(payload, constraint.field_pointer) + except (KeyError, IndexError, TypeError, ValueError) as exc: + raise ValueError( + f"Capability constraint field_pointer does not resolve: {constraint.field_pointer}" + ) from exc + if constraint.parameter not in binding_values: + raise ValueError("Capability constraint references an unresolved parameter identity") + if not _json_value_equal(concrete_value, binding_values[constraint.parameter]): + raise ValueError("Capability constraint binding does not match the concrete field value") return self -class ExpandedScenario(Scenario): - """Scenario produced by module/import expansion.""" - - _module_namespaces: dict[str, str] = PrivateAttr(default_factory=dict) - - @property - def module_namespaces(self) -> dict[str, str]: - return dict(self._module_namespaces) - - def _set_module_namespaces(self, namespaces: dict[str, str]) -> None: - self._module_namespaces = dict(namespaces) +def _resolve_json_pointer(payload: object, pointer: str) -> object: + current = payload + for raw_segment in pointer.split("/")[1:]: + segment = raw_segment.replace("~1", "/").replace("~0", "~") + if isinstance(current, Mapping): + current = current[segment] + elif isinstance(current, (list, tuple)): + current = current[int(segment)] + else: + raise TypeError("JSON Pointer traverses a scalar value") + return current diff --git a/implementations/python/packages/aces_sdl/scenarios.py b/implementations/python/packages/aces_sdl/scenarios.py index 873ed2a23..f6349321a 100644 --- a/implementations/python/packages/aces_sdl/scenarios.py +++ b/implementations/python/packages/aces_sdl/scenarios.py @@ -6,7 +6,8 @@ from pathlib import Path from ._errors import SDLParseError, SDLValidationError -from .parser import parse_sdl +from ._source_profile import SDLMigrationPolicy +from .parser import parse_sdl_file from .scenario import Scenario log = logging.getLogger("aces.scenarios") @@ -38,17 +39,17 @@ class ScenarioStateError(ScenarioError): """An invalid state transition was attempted.""" -def load_scenario(path: Path) -> Scenario: +def load_scenario( + path: Path, + *, + migration_policy: SDLMigrationPolicy | str = SDLMigrationPolicy.REJECT, +) -> Scenario: """Load and validate a scenario from a YAML file.""" if not path.exists(): raise FileNotFoundError(f"Scenario file not found: {path}") - raw = path.read_text(encoding="utf-8").strip() - if not raw: - raise ScenarioValidationError("Scenario file is empty", path=path) - try: - scenario = parse_sdl(raw, path=path) + scenario = parse_sdl_file(path, migration_policy=migration_policy) except SDLParseError as exc: raise ScenarioValidationError(str(exc), path=path) from exc except SDLValidationError as exc: @@ -56,6 +57,17 @@ def load_scenario(path: Path) -> Scenario: for advisory in scenario.advisories: log.warning("Scenario '%s' advisory: %s", scenario.name, advisory) + for diagnostic in scenario.source_diagnostics: + start = diagnostic.primary_range.start + log.warning( + "Scenario '%s' source advisory [%s] at %s:%d:%d (%s)", + scenario.name, + diagnostic.code, + diagnostic.source or path, + start.line, + start.column, + diagnostic.pointer or "/", + ) log.info("Loaded scenario '%s' from %s", scenario.name, path) return scenario diff --git a/implementations/python/packages/aces_sdl/schema_catalogs.py b/implementations/python/packages/aces_sdl/schema_catalogs.py new file mode 100644 index 000000000..89b5e3bd5 --- /dev/null +++ b/implementations/python/packages/aces_sdl/schema_catalogs.py @@ -0,0 +1,6 @@ +"""Public read-only views used to publish SDL contract schemas.""" + +from ._mapping_scopes import HASHMAP_SECTIONS +from ._runtime_service_families import RUNTIME_SERVICE_FAMILIES, RuntimeReferenceChild + +__all__ = ["HASHMAP_SECTIONS", "RUNTIME_SERVICE_FAMILIES", "RuntimeReferenceChild"] diff --git a/implementations/python/packages/aces_sdl/semantics/assessment.py b/implementations/python/packages/aces_sdl/semantics/assessment.py index 9b204b604..89d699df3 100644 --- a/implementations/python/packages/aces_sdl/semantics/assessment.py +++ b/implementations/python/packages/aces_sdl/semantics/assessment.py @@ -6,12 +6,8 @@ (ADR-055/064/069), not in authored SDL. What remains here is the resource-kind qualifier that objective success -references carry. Objective success references observable state only, so -``CONDITION`` is the sole member; the enum is retained as the kind-qualifier -seam so a future ADR that admits another observable-state carrier adds a member -here rather than reviving the removed scoring sections. Per ADR-015 this helper -lives with the SDL package and has no processor-runtime dependencies; per -ADR-016 it is part of the realized artifact set for SEM-206. +references carry. Objective success composes assertions over propositions; +executable conditions are probe realizations and are not success resources. """ from __future__ import annotations @@ -22,4 +18,4 @@ class AssessmentResourceKind(str, Enum): """Resource kinds an objective's success may reference (post ADR-073).""" - CONDITION = "condition" + ASSERTION = "assertion" diff --git a/implementations/python/packages/aces_sdl/semantics/objective_semantics.py b/implementations/python/packages/aces_sdl/semantics/objective_semantics.py index 5bafb77b5..b4da3f5d3 100644 --- a/implementations/python/packages/aces_sdl/semantics/objective_semantics.py +++ b/implementations/python/packages/aces_sdl/semantics/objective_semantics.py @@ -2,7 +2,7 @@ :func:`analyze_objective_semantics` is the single name-level source of truth for the SDL declarative-objective construct — actor binding, target resolution, -success interpretation (over ``conditions`` observable state, per ADR-073), the +success interpretation over backend-neutral assertions, the optional window (delegated to :func:`aces_sdl.semantics.objectives.analyze_objective_window`), and the acyclic ``depends_on`` ordering relation. It returns normalized references with their dependency-role tags, the per-objective ordering/refresh @@ -72,8 +72,7 @@ class ObjectiveReference: reference_kind: ObjectiveReferenceKind source_name: str dependency_roles: tuple[ObjectiveDependencyRole, ...] = () - #: Set on ``SUCCESS`` references to disambiguate the assessment-pipeline - #: namespace (a condition and a metric may legally share an SDL name). + #: Set on ``SUCCESS`` references to preserve the assertion namespace. success_resource_kind: AssessmentResourceKind | None = None window_reference_kind: ObjectiveWindowReferenceKind | None = None workflow_name: str | None = None @@ -140,13 +139,9 @@ def dependencies_for(self, name: str) -> ObjectiveResourceDependencies: @dataclass(frozen=True) class AssessmentResourceCatalog: - """The observable-state section an objective's success may name (post ADR-073). + """The backend-neutral assertion section objective success may name.""" - Objective success references ``conditions`` only; the OCR scoring sections - (metrics/evaluations/tlos/goals) were removed by ADR-073. - """ - - conditions: Mapping[str, object] + assertions: Mapping[str, object] @dataclass(frozen=True) @@ -221,7 +216,7 @@ def _has_cycle(graph: Mapping[str, list[str]]) -> bool: _SUCCESS_REFERENCE_SECTIONS: tuple[tuple[str, AssessmentResourceKind, str], ...] = ( - ("conditions", AssessmentResourceKind.CONDITION, "objective.success-condition-undeclared"), + ("assertions", AssessmentResourceKind.ASSERTION, "objective.success-assertion-undeclared"), ) @@ -349,7 +344,7 @@ def _analyze_success( assessment_resources: AssessmentResourceCatalog, unresolved: Callable[[object], bool], ) -> tuple[list[ObjectiveReference], list[ObjectiveIssue], list[str]]: - """Resolve ``success.conditions`` (observable state, per ADR-073). + """Resolve backend-neutral ``success.assertions``. Resolved names are kind-qualified before they enter the derived ordering/refresh tuples, preserving the kind-qualifier seam even though @@ -360,7 +355,7 @@ def _analyze_success( issues: list[ObjectiveIssue] = [] resolved: list[str] = [] success = getattr(objective, "success", None) - sections = ((assessment_resources.conditions, _SUCCESS_REFERENCE_SECTIONS[0]),) + sections = ((assessment_resources.assertions, _SUCCESS_REFERENCE_SECTIONS[0]),) for section, (attr, kind, code) in sections: for ref_name in getattr(success, attr, []) or []: if unresolved(ref_name): diff --git a/implementations/python/packages/aces_sdl/semantics/objectives.py b/implementations/python/packages/aces_sdl/semantics/objectives.py index 21b83b157..1d6be272a 100644 --- a/implementations/python/packages/aces_sdl/semantics/objectives.py +++ b/implementations/python/packages/aces_sdl/semantics/objectives.py @@ -6,6 +6,8 @@ from dataclasses import dataclass from enum import Enum +from .._identifiers import QualifiedName + class ObjectiveWindowReferenceKind(str, Enum): """Normalized objective-window reference kinds.""" @@ -107,17 +109,18 @@ def _ordered_unique(items: list[str]) -> tuple[str, ...]: def parse_workflow_step_ref(step_ref: str) -> ParsedWorkflowStepRef | None: - """Parse ``.`` syntax used by objective windows.""" + """Parse a qualified workflow name followed by one local step segment.""" - if "." not in step_ref: + try: + parts = QualifiedName.parse(step_ref).parts + except (TypeError, ValueError): return None - workflow_name, step_name = step_ref.rsplit(".", 1) - if not workflow_name or not step_name: + if len(parts) < 2: return None return ParsedWorkflowStepRef( raw=step_ref, - workflow_name=workflow_name, - step_name=step_name, + workflow_name=QualifiedName(parts[:-1]).render(), + step_name=parts[-1], ) diff --git a/implementations/python/packages/aces_sdl/semantics/propositions.py b/implementations/python/packages/aces_sdl/semantics/propositions.py new file mode 100644 index 000000000..c3a32a39d --- /dev/null +++ b/implementations/python/packages/aces_sdl/semantics/propositions.py @@ -0,0 +1,122 @@ +"""Pure truth algebra for backend-neutral SDL assertions.""" + +from __future__ import annotations + +from collections.abc import Sequence +from enum import Enum + +from ..propositions import AssertionPolarity, SubjectQuantifier, TruthCompositionMode + + +class TruthValue(str, Enum): + """Portable outcome domain for proposition evaluation. + + ``UNSUPPORTED`` is a realization disposition, not a logical truth value. + It is present in this wire-facing domain so inability to realize a + proposition cannot be collapsed into missing evidence or falsity. + """ + + TRUE = "true" + FALSE = "false" + UNKNOWN = "unknown" + UNSUPPORTED = "unsupported" + + +def negate_truth(value: TruthValue) -> TruthValue: + """Negate a decided truth value without inventing knowledge or support.""" + + if value is TruthValue.TRUE: + return TruthValue.FALSE + if value is TruthValue.FALSE: + return TruthValue.TRUE + return value + + +def evaluate_assertion_polarity(value: TruthValue, polarity: AssertionPolarity) -> TruthValue: + """Apply assertion polarity to one proposition outcome.""" + + return negate_truth(value) if polarity is AssertionPolarity.NEGATIVE else value + + +def _indeterminate(values: Sequence[TruthValue]) -> TruthValue: + return TruthValue.UNSUPPORTED if TruthValue.UNSUPPORTED in values else TruthValue.UNKNOWN + + +def _compose_at_least(values: Sequence[TruthValue], threshold: int) -> TruthValue: + true_count = values.count(TruthValue.TRUE) + if true_count >= threshold: + return TruthValue.TRUE + undecided_count = values.count(TruthValue.UNKNOWN) + values.count(TruthValue.UNSUPPORTED) + if true_count + undecided_count < threshold: + return TruthValue.FALSE + return _indeterminate(values) + + +def _validate_composition( + values: Sequence[TruthValue], + mode: TruthCompositionMode, + threshold: int | None, +) -> None: + if not values: + raise ValueError("truth composition requires at least one truth value") + if mode is TruthCompositionMode.AT_LEAST: + if threshold is None: + raise ValueError("at_least truth composition requires threshold") + if threshold < 1 or threshold > len(values): + raise ValueError("at_least threshold must be within the truth-value count") + elif threshold is not None: + raise ValueError(f"{mode.value} truth composition must not declare threshold") + + +def _compose_all(values: Sequence[TruthValue]) -> TruthValue: + if TruthValue.FALSE in values: + return TruthValue.FALSE + return TruthValue.TRUE if all(value is TruthValue.TRUE for value in values) else _indeterminate(values) + + +def _compose_any(values: Sequence[TruthValue]) -> TruthValue: + if TruthValue.TRUE in values: + return TruthValue.TRUE + return TruthValue.FALSE if all(value is TruthValue.FALSE for value in values) else _indeterminate(values) + + +def compose_truth( + values: Sequence[TruthValue], + *, + mode: TruthCompositionMode, + threshold: int | None = None, +) -> TruthValue: + """Compose non-empty assertion outcomes using the portable truth tables.""" + + _validate_composition(values, mode, threshold) + if mode is TruthCompositionMode.AT_LEAST: + assert threshold is not None + return _compose_at_least(values, threshold) + if mode is TruthCompositionMode.ALL_OF: + return _compose_all(values) + return _compose_any(values) + + +def quantify_subject_truth( + values: Sequence[TruthValue], + *, + quantifier: SubjectQuantifier, + threshold: int | None = None, +) -> TruthValue: + """Aggregate per-subject predicate outcomes for one finite proposition.""" + + mode = { + SubjectQuantifier.ALL: TruthCompositionMode.ALL_OF, + SubjectQuantifier.ANY: TruthCompositionMode.ANY_OF, + SubjectQuantifier.AT_LEAST: TruthCompositionMode.AT_LEAST, + }[quantifier] + return compose_truth(values, mode=mode, threshold=threshold) + + +__all__ = [ + "TruthValue", + "compose_truth", + "evaluate_assertion_polarity", + "negate_truth", + "quantify_subject_truth", +] diff --git a/implementations/python/packages/aces_sdl/validator/__init__.py b/implementations/python/packages/aces_sdl/validator/__init__.py index c18d46cb1..26613e9e4 100644 --- a/implementations/python/packages/aces_sdl/validator/__init__.py +++ b/implementations/python/packages/aces_sdl/validator/__init__.py @@ -6,6 +6,7 @@ from ._core import _ValidatorCore from ._evidence_requirements import _EvidenceRequirementsMixin from ._nodes_infra_network import _NodesInfraNetworkMixin +from ._propositions import _PropositionsMixin from ._relationships import _RelationshipsMixin from ._relationships_proxy import _RelationshipsProxyMixin from ._runtime_identity_data import _RuntimeIdentityDataMixin @@ -30,13 +31,14 @@ class SemanticValidator( _RelationshipsMixin, _RelationshipsProxyMixin, _ContentObjectivesMixin, + _PropositionsMixin, _EvidenceRequirementsMixin, _WorkflowAnalysisMixin, _WorkflowVerifyMixin, _SectionsMixin, _ValidatorCore, ): - """Validates a Scenario beyond structural Pydantic checks. + """Validates executable SDL content beyond structural Pydantic checks. Call ``validate()`` to run all passes. Raises ``SDLValidationError`` with all collected errors if any pass fails. diff --git a/implementations/python/packages/aces_sdl/validator/_content_objectives.py b/implementations/python/packages/aces_sdl/validator/_content_objectives.py index b86912cbc..13c56c48b 100644 --- a/implementations/python/packages/aces_sdl/validator/_content_objectives.py +++ b/implementations/python/packages/aces_sdl/validator/_content_objectives.py @@ -41,8 +41,8 @@ "objective.target-ambiguous": ( lambda i: f"Objective '{i.objective_name}' target '{i.ref}' is ambiguous; use one of: {', '.join(i.candidates)}" ), - "objective.success-condition-undeclared": ( - lambda i: f"Objective '{i.objective_name}' references undefined condition '{i.ref}' in success criteria" + "objective.success-assertion-undeclared": ( + lambda i: f"Objective '{i.objective_name}' references undefined assertion '{i.ref}' in success criteria" ), "objective.window.story-unbound": ( lambda i: f"Objective '{i.objective_name}' references undefined story '{i.ref}' in window" @@ -227,12 +227,14 @@ def _verify_relationships(self) -> None: rel.source, owner_label=f"Relationship '{name}'", ref_label="source", + targetable=True, ) if not self._is_unresolved_var(rel.target): self._validate_named_ref( rel.target, owner_label=f"Relationship '{name}'", ref_label="target", + targetable=True, ) def _verify_agents(self) -> None: @@ -257,7 +259,6 @@ def _verify_agent(self, name: str, agent: object, flat_entity_names: set[str], s ) if agent.initial_knowledge: self._verify_agent_initial_knowledge(name, agent.initial_knowledge, service_names) - self._verify_agent_starting_conditions(label, agent) for anchor in agent.authority_anchors: if not self._is_unresolved_var(anchor): self._validate_named_ref(anchor, owner_label=label, ref_label="authority_anchor", targetable=False) @@ -281,18 +282,6 @@ def _verify_agent_subnet_refs( elif not self._is_switch_node(subnet): self._err(not_switch(subnet)) - def _verify_agent_starting_conditions(self, label: str, agent: object) -> None: - for cond_name in agent.starting_conditions: - if self._is_unresolved_var(cond_name): - continue - # ADR-020 §6 publishes starting_conditions as accepting bare - # (`health`) or section-qualified (`conditions.health`) - # references. Strip the `conditions.` prefix when present so - # both forms resolve against the same dict. - bare_name = cond_name.removeprefix("conditions.") - if bare_name not in self._s.conditions: - self._err(f"{label} starting_condition '{cond_name}' not in conditions section") - def _verify_agent_initial_knowledge(self, name: str, initial_knowledge: object, service_names: set[str]) -> None: label = f"Agent '{name}'" self._verify_agent_ik_hosts(label, initial_knowledge) @@ -401,7 +390,7 @@ def _verify_objectives(self) -> None: agents_by_name=self._s.agents, entity_names=self._all_entity_names(), assessment_resources=AssessmentResourceCatalog( - conditions=self._s.conditions, + assertions=self._s.assertions, ), window_resources=WindowResourceCatalog( stories=self._s.stories, diff --git a/implementations/python/packages/aces_sdl/validator/_core.py b/implementations/python/packages/aces_sdl/validator/_core.py index 52f7daebb..74da8a583 100644 --- a/implementations/python/packages/aces_sdl/validator/_core.py +++ b/implementations/python/packages/aces_sdl/validator/_core.py @@ -6,19 +6,24 @@ from collections import defaultdict from .._base import is_variable_ref +from .._declarations import DeclarationIndex, build_declaration_index from .._errors import SDLValidationError -from .._runtime_service_families import collect_qualified_runtime_family_refs +from .._runtime_service_families import ( + RuntimeFamilyReference, + iter_runtime_family_references, +) from ..entities import flatten_entities from ..nodes import NodeType -from ..scenario import Scenario -from ._support import _NODES_PREFIX +from ..scenario import ScenarioContent class _ValidatorCore: - def __init__(self, scenario: Scenario) -> None: + def __init__(self, scenario: ScenarioContent) -> None: self._s = scenario self._errors: list[str] = [] self._warnings: list[str] = [] + self._declaration_index: DeclarationIndex | None = None + self._runtime_references: dict[str, RuntimeFamilyReference] | None = None def _err(self, msg: str) -> None: self._errors.append(msg) @@ -43,46 +48,16 @@ def _is_vm_node(self, node_name: str) -> bool: def _all_entity_names(self) -> set[str]: return set(flatten_entities(self._s.entities).keys()) - def _qualified_service_refs(self) -> set[str]: - refs: set[str] = set() - for node_name, node in self._s.nodes.items(): - for service in node.services: - if service.name: - refs.add(f"nodes.{node_name}.services.{service.name}") - return refs - - @staticmethod - def _split_node_service_ref(ref: object) -> tuple[str, str] | None: - """Split ``nodes..services.`` into node/service parts. + def _split_node_service_ref(self, ref: object) -> tuple[str, str] | None: + """Resolve a qualified service ref without parsing rendered delimiters.""" - Node names may contain dots (for example ``wazuh.manager``), so service - refs must be partitioned on the ``.services.`` marker instead of split - by position. - """ - if not isinstance(ref, str) or not ref.startswith(_NODES_PREFIX): - return None - node_name, sep, service_name = ref[len(_NODES_PREFIX) :].partition(".services.") - if not sep or not node_name or not service_name: + if not isinstance(ref, str): return None - return node_name, service_name - - def _qualified_runtime_refs(self) -> set[str]: - """Qualified refs for node-scoped runtime inventories. - - These let a top-level relationship endpoint resolve to a runtime - service family or stable child record. This keeps runtime-observed - logical state targetable without promoting those records to top-level - SDL sections. - """ - return collect_qualified_runtime_family_refs(self._s) - - def _qualified_acl_refs(self) -> set[str]: - refs: set[str] = set() - for infra_name, infra in self._s.infrastructure.items(): - for acl in infra.acls: - if acl.name: - refs.add(f"infrastructure.{infra_name}.acls.{acl.name}") - return refs + for node_name, node in self._s.nodes.items(): + for service in node.services: + if service.name and ref == f"nodes.{node_name}.services.{service.name}": + return node_name, service.name + return None def _workflow_step_refs(self) -> set[str]: refs: set[str] = set() @@ -99,89 +74,9 @@ def _named_ref_index(self, *, targetable: bool = False) -> dict[str, set[str]]: and are required for infrastructure entries because those keys intentionally mirror node names. """ - index: dict[str, set[str]] = defaultdict(set) - self._populate_named_ref_index(index) - if not targetable: - return {alias: set(candidates) for alias, candidates in index.items()} - return self._filter_targetable_aliases(index) - - _NAMED_REF_TOP_LEVEL_SECTIONS = ( - ("nodes", True), - ("features", True), - ("conditions", True), - ("vulnerabilities", True), - ("infrastructure", False), - ("content", True), - ("accounts", True), - ("agents", True), - ("action_contracts", True), - ("observation_boundaries", True), - ("behavior_specifications", True), - ("evidence_requirements", True), - ("objectives", True), - ("workflows", True), - ("relationships", True), - ("variables", True), - ("injects", True), - ("events", True), - ("scripts", True), - ("stories", True), - ) - - _TARGETABLE_DISALLOWED_PREFIXES = ( - "variables.", - "evidence_requirements.", - "objectives.", - "workflows.", - ) - - def _populate_named_ref_index(self, index: dict[str, set[str]]) -> None: - self._add_top_level_section_aliases(index) - self._add_entity_aliases(index) - self._add_content_item_aliases(index) - self._add_qualified_aliases(index) - - def _add_top_level_section_aliases(self, index: dict[str, set[str]]) -> None: - for section_name, allow_bare in self._NAMED_REF_TOP_LEVEL_SECTIONS: - for name in getattr(self._s, section_name): - canonical = f"{section_name}.{name}" - index[canonical].add(canonical) - if allow_bare: - index[name].add(canonical) - - def _add_entity_aliases(self, index: dict[str, set[str]]) -> None: - for entity_name in self._all_entity_names(): - canonical = f"entities.{entity_name}" - index[canonical].add(canonical) - index[entity_name].add(canonical) - - def _add_content_item_aliases(self, index: dict[str, set[str]]) -> None: - for content_name, content in self._s.content.items(): - for item in content.items: - if not item.name: - continue - canonical = f"content.{content_name}.items.{item.name}" - index[canonical].add(canonical) - index[item.name].add(canonical) - - def _add_qualified_aliases(self, index: dict[str, set[str]]) -> None: - for qualified_refs in ( - self._qualified_service_refs(), - self._qualified_acl_refs(), - self._qualified_runtime_refs(), - ): - for ref in qualified_refs: - index[ref].add(ref) - - def _filter_targetable_aliases(self, index: dict[str, set[str]]) -> dict[str, set[str]]: - filtered: dict[str, set[str]] = {} - for alias, candidates in index.items(): - keep = { - candidate for candidate in candidates if not candidate.startswith(self._TARGETABLE_DISALLOWED_PREFIXES) - } - if keep: - filtered[alias] = keep - return filtered + if self._declaration_index is None: + raise RuntimeError("declaration index must be built before reference validation") + return self._declaration_index.reference_aliases(targetable=targetable) def _operating_scope_ref_index(self) -> dict[str, set[str]]: """Build the alias map for ACT-601 ``Agent.operating_scope``. @@ -235,11 +130,13 @@ def _add_operating_scope_service_aliases(self, index: dict[str, set[str]]) -> No # service names. The service-ref helper only emits names declared # on VM nodes (a service on a switch is meaningless), so no extra # filtering is needed here. - for ref in self._qualified_service_refs(): - index[ref].add(ref) - tail = ref.rsplit(".", 1)[-1] - if tail: - index[tail].add(ref) + for node_name, node in self._s.nodes.items(): + for service in node.services: + if not service.name: + continue + ref = f"nodes.{node_name}.services.{service.name}" + index[ref].add(ref) + index[service.name].add(ref) def _add_operating_scope_content_aliases(self, index: dict[str, set[str]]) -> None: # Content: sections and items keep the unrestricted aliasing from @@ -291,6 +188,8 @@ def validate(self) -> None: """Run all validation passes and raise on errors.""" self._errors = [] self._warnings = [] + self._declaration_index = build_declaration_index(self._s, raise_on_collision=False) + self._errors.extend(self._declaration_index.collision_errors) # OCR passes self._verify_nodes() @@ -335,7 +234,10 @@ def validate(self) -> None: self._verify_relationship_proxy_upstreams() self._verify_agents() self._verify_participant_behavior() + self._verify_propositions_and_assertions() self._verify_objectives() + self._verify_objective_success_assertions() + self._verify_precondition_assertion_uses() self._verify_workflows() self._verify_participant_outcomes() self._verify_evidence_requirements() @@ -365,35 +267,17 @@ def _warn_missing_vm_resources(self) -> None: "supplies defaults." ) - @staticmethod - def _split_runtime_ref(ref: object, *, surface: str) -> tuple[str, str] | None: - """Split ``nodes..runtime..`` into (node, rest). + def _runtime_reference(self, ref: object) -> RuntimeFamilyReference | None: + """Resolve an exact registered runtime address without delimiter parsing.""" - Module composition rewrites the node segment to a dotted namespaced - form (``shared.web``), so we cannot split on ``.`` and index by - position. Partition on the surface marker instead so the node name - survives an arbitrary number of namespace prefixes. - """ - if not isinstance(ref, str) or not ref.startswith(_NODES_PREFIX): + if not isinstance(ref, str): return None - marker = f".runtime.{surface}." - head, sep, tail = ref[len(_NODES_PREFIX) :].partition(marker) - if not sep or not head or not tail: - return None - return head, tail - - def _node_runtime(self, node_name: str) -> object | None: - """Return the ``runtime`` surface for ``node_name``, or None.""" - node = self._s.nodes.get(node_name) - return getattr(node, "runtime", None) if node is not None else None - - @staticmethod - def _database_service_id_from_tail(tail: str) -> str | None: - """Service id from a ```` (1 part) or ``.databases.`` (3) tail.""" - tail_parts = tail.split(".") - if len(tail_parts) == 1 or (len(tail_parts) == 3 and tail_parts[1] == "databases"): - return tail_parts[0] - return None + if self._runtime_references is None: + references: dict[str, RuntimeFamilyReference] = {} + for reference in iter_runtime_family_references(self._s): + references.setdefault(reference.address, reference) + self._runtime_references = references + return self._runtime_references.get(ref) def _resolve_database_service_ref(self, ref: object) -> object | None: """Resolve a qualified ``nodes..runtime.database_services.`` ref. @@ -402,15 +286,12 @@ def _resolve_database_service_ref(self, ref: object) -> object | None: resolve to the owning :class:`RuntimeDatabaseService` so a relationship's ``database_access`` can be checked against it. """ - split = self._split_runtime_ref(ref, surface="database_services") - if split is None: + reference = self._runtime_reference(ref) + if reference is None or reference.family.collection_name != "database_services": return None - node_name, tail = split - svc_id = self._database_service_id_from_tail(tail) - runtime = self._node_runtime(node_name) - if svc_id is None or runtime is None: + if reference.collection_path not in {(), ("databases",)}: return None - return next((s for s in runtime.database_services if s.database_service_id == svc_id), None) + return reference.owning_item def _resolve_application_ref(self, ref: object) -> object | None: """Resolve a qualified ``nodes..runtime.applications.`` ref. @@ -419,11 +300,11 @@ def _resolve_application_ref(self, ref: object) -> object | None: ``database_access`` source endpoint can be confirmed to be a runtime application (ADR-029 §4). """ - split = self._split_runtime_ref(ref, surface="applications") - if split is None: - return None - node_name, tail = split - runtime = self._node_runtime(node_name) - if "." in tail or runtime is None: + reference = self._runtime_reference(ref) + if ( + reference is None + or reference.family.collection_name != "applications" + or reference.item is not reference.owning_item + ): return None - return next((a for a in runtime.applications if a.application_id == tail), None) + return reference.item diff --git a/implementations/python/packages/aces_sdl/validator/_propositions.py b/implementations/python/packages/aces_sdl/validator/_propositions.py new file mode 100644 index 000000000..9f1234a73 --- /dev/null +++ b/implementations/python/packages/aces_sdl/validator/_propositions.py @@ -0,0 +1,73 @@ +"""Semantic validation for proposition, assertion, and probe references.""" + +from ..propositions import AssertionRole + + +class _PropositionsMixin: + def _verify_propositions_and_assertions(self) -> None: + self._verify_proposition_refs() + self._verify_assertion_refs() + self._verify_condition_proposition_refs() + + def _verify_proposition_refs(self) -> None: + for name, proposition in self._s.propositions.items(): + label = f"Proposition '{name}'" + for subject in proposition.subjects: + if not self._is_unresolved_var(subject): + self._validate_named_ref( + subject, + owner_label=label, + ref_label="subject", + targetable=True, + ) + self._verify_membership_refs( + proposition.evidence_requirements, + self._s.evidence_requirements, + lambda ref, proposition_label=label: ( + f"{proposition_label} evidence_requirement '{ref}' not in evidence_requirements section" + ), + ) + + def _verify_assertion_refs(self) -> None: + for name, assertion in self._s.assertions.items(): + if not self._is_unresolved_var(assertion.proposition) and assertion.proposition not in self._s.propositions: + self._err(f"Assertion '{name}' proposition '{assertion.proposition}' not in propositions section") + + def _verify_condition_proposition_refs(self) -> None: + for name, condition in self._s.conditions.items(): + if condition.proposition and not self._is_unresolved_var(condition.proposition): + if condition.proposition not in self._s.propositions: + self._err(f"Condition '{name}' proposition '{condition.proposition}' not in propositions section") + + def _verify_objective_success_assertions(self) -> None: + allowed_roles = {AssertionRole.INVARIANT, AssertionRole.POSTCONDITION} + for objective_name, objective in self._s.objectives.items(): + for assertion_name in objective.success.assertions: + if self._is_unresolved_var(assertion_name): + continue + assertion = self._s.assertions.get(assertion_name) + if assertion is None: + self._err( + f"Objective '{objective_name}' success assertion '{assertion_name}' not in assertions section" + ) + elif assertion.role not in allowed_roles: + self._err( + f"Objective '{objective_name}' success assertion '{assertion_name}' " + "must be an invariant or postcondition" + ) + + def _verify_precondition_assertion_uses(self) -> None: + uses = [(f"Event '{name}'", event.assertions, "event trigger") for name, event in self._s.events.items()] + uses.extend( + (f"Agent '{name}'", agent.starting_assertions, "agent starting state") + for name, agent in self._s.agents.items() + ) + for owner, refs, use_label in uses: + for assertion_name in refs: + if self._is_unresolved_var(assertion_name): + continue + assertion = self._s.assertions.get(assertion_name) + if assertion is None: + self._err(f"{owner} assertion '{assertion_name}' not in assertions section") + elif assertion.role is not AssertionRole.PRECONDITION: + self._err(f"{owner} assertion '{assertion_name}' {use_label} must be a precondition") diff --git a/implementations/python/packages/aces_sdl/validator/_relationships_proxy.py b/implementations/python/packages/aces_sdl/validator/_relationships_proxy.py index bb5dd34be..23a425053 100644 --- a/implementations/python/packages/aces_sdl/validator/_relationships_proxy.py +++ b/implementations/python/packages/aces_sdl/validator/_relationships_proxy.py @@ -3,8 +3,6 @@ Part of the SemanticValidator mixin composition; see __init__.py. """ -from ._support import _NODES_PREFIX - class _RelationshipsProxyMixin: def _verify_relationship_proxy_upstreams(self) -> None: @@ -153,10 +151,9 @@ def _node_name_from_qualified_target(self, target: str) -> str | None: if service_split is not None: node_name = service_split[0] return node_name if node_name in self._s.nodes else None - if target.startswith(_NODES_PREFIX): - node_name, sep, _tail = target[len(_NODES_PREFIX) :].partition(".runtime.") - if sep and node_name in self._s.nodes: - return node_name + runtime_reference = self._runtime_reference(target) + if runtime_reference is not None: + return runtime_reference.node_name return None def _check_proxy_upstream_route_ref(self, route_ref: str, source: str, label: str) -> object | None: diff --git a/implementations/python/packages/aces_sdl/validator/_runtime_mail.py b/implementations/python/packages/aces_sdl/validator/_runtime_mail.py index afbf99757..92b75ad64 100644 --- a/implementations/python/packages/aces_sdl/validator/_runtime_mail.py +++ b/implementations/python/packages/aces_sdl/validator/_runtime_mail.py @@ -17,13 +17,6 @@ class _MailServiceLocalIds: routing_refs: set[str] -@dataclass(frozen=True) -class _MailRefTail: - service_id: str - collection_name: str - child_id: str - - _MailChildIdReader = Callable[[object], Iterable[str]] _MAIL_CHILD_ID_READERS: dict[str, _MailChildIdReader] = { "components": lambda service: (component.component_id for component in service.components), @@ -43,11 +36,6 @@ def _mail_services_for_node(node: object) -> Sequence[object]: return () if runtime is None else runtime.mail_services -def _mail_services_for_node_name(scenario: object, node_name: str) -> Sequence[object]: - node = scenario.nodes.get(node_name) - return () if node is None else _mail_services_for_node(node) - - def _collect_mail_service_local_ids(service: object) -> _MailServiceLocalIds: mailbox_ids = {mailbox.mailbox_id for mailbox in service.mailboxes} alias_ids = {alias.alias_id for alias in service.aliases} @@ -62,35 +50,6 @@ def _collect_mail_service_local_ids(service: object) -> _MailServiceLocalIds: ) -def _parse_mail_ref_tail(tail: str) -> _MailRefTail | None: - tail_parts = tail.split(".") - if len(tail_parts) == 1: - return _MailRefTail(tail_parts[0], "", "") - if len(tail_parts) == 3: - return _MailRefTail(*tail_parts) - return None - - -def _resolve_mail_service_tail( - mail_services: Sequence[object], - parsed_tail: _MailRefTail, -) -> object | None: - for service in mail_services: - if service.mail_service_id != parsed_tail.service_id: - continue - return _matched_service_for_tail(service, parsed_tail) - return None - - -def _matched_service_for_tail(service: object, parsed_tail: _MailRefTail) -> object | None: - matches_service = not parsed_tail.collection_name - matches_child = bool( - parsed_tail.collection_name - and _mail_child_ref_exists(service, parsed_tail.collection_name, parsed_tail.child_id) - ) - return service if matches_service or matches_child else None - - def _mail_child_ref_exists(service: object, collection_name: str, child_id: str) -> bool: read_child_ids = _MAIL_CHILD_ID_READERS.get(collection_name) return read_child_ids is not None and child_id in read_child_ids(service) @@ -294,14 +253,7 @@ def _check_mail_access_target(self, target: str, label: str) -> object | None: def _resolve_mail_service_ref(self, ref: object) -> object | None: """Resolve a qualified runtime mail-service or child ref to the service.""" - split = self._split_runtime_ref(ref, surface="mail_services") - if split is None: - return None - node_name, tail = split - parsed_tail = _parse_mail_ref_tail(tail) - if parsed_tail is None: + reference = self._runtime_reference(ref) + if reference is None or reference.family.collection_name != "mail_services": return None - return _resolve_mail_service_tail( - _mail_services_for_node_name(self._s, node_name), - parsed_tail, - ) + return reference.owning_item diff --git a/implementations/python/packages/aces_sdl/validator/_runtime_services.py b/implementations/python/packages/aces_sdl/validator/_runtime_services.py index d0755a504..f3815a236 100644 --- a/implementations/python/packages/aces_sdl/validator/_runtime_services.py +++ b/implementations/python/packages/aces_sdl/validator/_runtime_services.py @@ -350,20 +350,20 @@ def _verify_ssh_server_service( server_id = server.ssh_server_id service_name = ref if ref.startswith("nodes."): - parts = ref.split(".") - if len(parts) != 4 or parts[2] != "services": + split = self._split_node_service_ref(ref) + if split is None: self._err( f"Node '{node_name}' runtime ssh_server '{server_id}' service ref '{ref}' " f"must be a bare service name or 'nodes..services.'" ) return - if parts[1] != node_name: + service_node_name, service_name = split + if service_node_name != node_name: self._err( f"Node '{node_name}' runtime ssh_server '{server_id}' service ref '{ref}' " f"must reference a service on the same node" ) return - service_name = parts[3] if service_name not in service_names: self._err( f"Node '{node_name}' runtime ssh_server '{server_id}' references undefined service '{service_name}'" diff --git a/implementations/python/packages/aces_sdl/validator/_sections.py b/implementations/python/packages/aces_sdl/validator/_sections.py index c4fe671aa..1433c81bc 100644 --- a/implementations/python/packages/aces_sdl/validator/_sections.py +++ b/implementations/python/packages/aces_sdl/validator/_sections.py @@ -14,7 +14,7 @@ class _SectionsMixin: def _verify_variables(self) -> None: - defined = set(self._s.variables.keys()) + defined = set(getattr(self._s, "variables", {})) self._check_variable_refs(self._s, "", defined) def _check_variable_refs(self, value: object, path: str, defined: set[str]) -> None: @@ -136,9 +136,6 @@ def _verify_events(self) -> None: self._verify_event_refs(name, event) def _verify_event_refs(self, name: str, event: object) -> None: - self._verify_membership_refs( - event.conditions, self._s.conditions, lambda ref: f"Event '{name}' references undefined condition '{ref}'" - ) self._verify_membership_refs( event.injects, self._s.injects, lambda ref: f"Event '{name}' references undefined inject '{ref}'" ) diff --git a/implementations/python/packages/aces_sdl/validator/_workflows_analysis.py b/implementations/python/packages/aces_sdl/validator/_workflows_analysis.py index f59d893ad..82d53e795 100644 --- a/implementations/python/packages/aces_sdl/validator/_workflows_analysis.py +++ b/implementations/python/packages/aces_sdl/validator/_workflows_analysis.py @@ -4,6 +4,7 @@ """ from ..orchestration import Workflow, WorkflowPredicate, WorkflowStep, WorkflowStepType +from ..propositions import AssertionRole from ..semantics.workflow import workflow_step_semantic_contract from ._support import _AvailableStateContext, _CompensationState, _WorkflowBuildState @@ -24,7 +25,7 @@ def _validate_predicate_section_refs( self, workflow_name: str, step_name: str, predicate: WorkflowPredicate ) -> None: predicate_sections = ( - ("condition", predicate.conditions, self._s.conditions), + ("assertion", predicate.assertions, self._s.assertions), ("objective", predicate.objectives, self._s.objectives), ) for label, refs, section in predicate_sections: @@ -37,6 +38,11 @@ def _validate_predicate_section_refs( f"'{step_name}' references undefined " f"{label} '{ref}' in predicate" ) + elif label == "assertion" and section[ref].role is not AssertionRole.PRECONDITION: + self._err( + f"Workflow '{workflow_name}' step '{step_name}' assertion '{ref}' " + "in predicate must be a precondition" + ) def _validate_predicate_step_states( self, diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index d29b34c2a..ce1f9ef3b 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -14,12 +14,15 @@ dependencies = [ "rich>=13.0.0", "PyYAML>=6.0", "cryptography>=46.0.7", + "defusedxml>=0.7.1", "fastapi>=0.115.0", "uvicorn[standard]>=0.34.0", "sse-starlette>=2.0.0", "asyncssh>=2.23.0", "mcp>=1.0.0", "packaging>=23.0", + "rfc8785>=0.1.4,<0.2", + "blake3>=1.0.8,<2", ] [project.optional-dependencies] @@ -86,6 +89,7 @@ path = "hatch_build.py" [tool.hatch.build.targets.sdist.force-include] "../../contracts" = "_corpus" "../../README.md" = "README.md" +"../../THIRD_PARTY_NOTICES.md" = "THIRD_PARTY_NOTICES.md" [tool.pytest.ini_options] testpaths = ["tests"] @@ -156,3 +160,4 @@ ignore = [ "packages/aces_runtime/control_plane_api.py" = ["B008"] # fastapi Depends() pattern "packages/aces_runtime/control_plane.py" = ["S112"] # intentional exception suppression "packages/aces_sdl/module_registry.py" = ["S310", "S202"] # explicit OCI URL fetch and tarball extract +"packages/aces_contracts/realization_envelope.py" = ["E402"] # late import breaks a carrier dependency cycle diff --git a/implementations/python/src/aces/__init__.py b/implementations/python/src/aces/__init__.py index a86952363..b7359bf3d 100644 --- a/implementations/python/src/aces/__init__.py +++ b/implementations/python/src/aces/__init__.py @@ -3,7 +3,9 @@ from aces._compat import package_version # Derived from the installed distribution metadata. The version source of truth -# is `[project] version` in pyproject.toml, bumped by release-please (#684). -__version__ = package_version("aces-sdl", default="0.1.0") +# is `[project] version` in pyproject.toml, bumped by release-please (#684). The +# not-installed fallback is the honest PEP 440 sentinel `0.0.0+unknown` +# (GOV-901): an uninstalled namespace must not imply a real released version. +__version__ = package_version("aces-sdl", default="0.0.0+unknown") __all__ = ["__version__"] diff --git a/implementations/python/src/aces/core/sdl/__init__.py b/implementations/python/src/aces/core/sdl/__init__.py index c2990cf82..bf7f712f1 100644 --- a/implementations/python/src/aces/core/sdl/__init__.py +++ b/implementations/python/src/aces/core/sdl/__init__.py @@ -1,23 +1,7 @@ """Backward-compatible SDL namespace.""" -from aces.core.sdl._errors import ( - SDLError, - SDLInstantiationError, - SDLParseError, - SDLValidationError, -) -from aces.core.sdl.instantiate import instantiate_scenario -from aces.core.sdl.parser import parse_sdl, parse_sdl_file -from aces.core.sdl.scenario import InstantiatedScenario, Scenario +from aces._compat import reexport as _reexport -__all__ = [ - "instantiate_scenario", - "InstantiatedScenario", - "parse_sdl", - "parse_sdl_file", - "Scenario", - "SDLError", - "SDLInstantiationError", - "SDLParseError", - "SDLValidationError", -] +_reexport(globals(), "aces_sdl") + +del _reexport diff --git a/implementations/python/tests/data/realization_envelope/invalid/empty-interval.json b/implementations/python/tests/data/realization_envelope/invalid/empty-interval.json new file mode 100644 index 000000000..99a6bb252 --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/invalid/empty-interval.json @@ -0,0 +1,3 @@ +{"id": "bad-interval", "scope": "node", + "domains": {"cpu": {"kind": "numeric-interval", "numeric_type": "integer", "lower": 8, "upper": 1}}, + "bindings": []} diff --git a/implementations/python/tests/data/realization_envelope/invalid/exact-nonsingleton.json b/implementations/python/tests/data/realization_envelope/invalid/exact-nonsingleton.json new file mode 100644 index 000000000..e487ee723 --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/invalid/exact-nonsingleton.json @@ -0,0 +1,3 @@ +{"id": "bad-exact", "scope": "scenario", + "domains": {"os": {"kind": "enum", "values": ["linux", "windows"]}}, + "bindings": [{"path": "nodes.web.os", "scope": "field", "posture": "exact", "domain": "os"}]} diff --git a/implementations/python/tests/data/realization_envelope/invalid/open-with-domain.json b/implementations/python/tests/data/realization_envelope/invalid/open-with-domain.json new file mode 100644 index 000000000..e6c57fc99 --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/invalid/open-with-domain.json @@ -0,0 +1,3 @@ +{"id": "bad-open", "scope": "scenario", + "domains": {"os": {"kind": "enum", "values": ["linux"]}}, + "bindings": [{"path": "nodes.web.os", "scope": "field", "posture": "open", "domain": "os"}]} diff --git a/implementations/python/tests/data/realization_envelope/invalid/record-cycle.json b/implementations/python/tests/data/realization_envelope/invalid/record-cycle.json new file mode 100644 index 000000000..9cdee410f --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/invalid/record-cycle.json @@ -0,0 +1,4 @@ +{"id": "bad-cycle", "scope": "node", + "domains": {"a": {"kind": "record", "fields": {"x": "b"}}, + "b": {"kind": "record", "fields": {"y": "a"}}}, + "bindings": []} diff --git a/implementations/python/tests/data/realization_envelope/invalid/unknown-domain-ref.json b/implementations/python/tests/data/realization_envelope/invalid/unknown-domain-ref.json new file mode 100644 index 000000000..0ae1faa9b --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/invalid/unknown-domain-ref.json @@ -0,0 +1,2 @@ +{"id": "bad-ref", "scope": "scenario", "domains": {}, + "bindings": [{"path": "name", "scope": "scenario", "posture": "constrained", "domain": "missing"}]} diff --git a/implementations/python/tests/data/realization_envelope/valid/closed-scenario.json b/implementations/python/tests/data/realization_envelope/valid/closed-scenario.json new file mode 100644 index 000000000..32d7efacb --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/valid/closed-scenario.json @@ -0,0 +1,13 @@ +{ + "id": "closed-scenario-v1", + "scope": "scenario", + "domains": { + "scenario_name": {"kind": "exact", "value": "sealed"}, + "node_type": {"kind": "exact", "value": "vm"} + }, + "bindings": [ + {"path": "name", "scope": "scenario", "posture": "exact", "domain": "scenario_name"}, + {"path": "nodes.only.type", "scope": "node", "posture": "exact", "domain": "node_type"} + ], + "closure": [{"path": "", "scope": "scenario", "closure": "closed-world"}] +} diff --git a/implementations/python/tests/data/realization_envelope/valid/record-node.json b/implementations/python/tests/data/realization_envelope/valid/record-node.json new file mode 100644 index 000000000..bf0518098 --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/valid/record-node.json @@ -0,0 +1,12 @@ +{ + "id": "record-node-v1", + "scope": "node", + "domains": { + "os": {"kind": "enum", "values": ["linux", "windows"]}, + "vm": {"kind": "exact", "value": "vm"}, + "node": {"kind": "record", "fields": {"type": "vm", "os": "os"}, "extra": false} + }, + "bindings": [ + {"path": "nodes.web", "scope": "node", "posture": "constrained", "domain": "node"} + ] +} diff --git a/implementations/python/tests/data/realization_envelope/valid/resource-bounds.json b/implementations/python/tests/data/realization_envelope/valid/resource-bounds.json new file mode 100644 index 000000000..6a76597ca --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/valid/resource-bounds.json @@ -0,0 +1,16 @@ +{ + "id": "resource-bounds-v1", + "scope": "node", + "domains": { + "cpu": {"kind": "numeric-interval", "numeric_type": "integer", "lower": 1, "upper": 8}, + "ratio": {"kind": "numeric-interval", "numeric_type": "number", "lower": 0.0, "upper": 1.0, "upper_closed": false}, + "enabled": {"kind": "boolean", "value": true}, + "image": {"kind": "governed-reference", "authority": "image-registry", "allowed_refs": ["base/linux", "base/windows"]} + }, + "bindings": [ + {"path": "nodes.host.resources.cpu", "scope": "field", "posture": "constrained", "domain": "cpu"}, + {"path": "nodes.host.resources.ratio", "scope": "field", "posture": "constrained", "domain": "ratio"}, + {"path": "nodes.host.enabled", "scope": "field", "posture": "exact", "domain": "enabled"}, + {"path": "nodes.host.image", "scope": "field", "posture": "constrained", "domain": "image"} + ] +} diff --git a/implementations/python/tests/data/realization_envelope/valid/scenario-web-family.json b/implementations/python/tests/data/realization_envelope/valid/scenario-web-family.json new file mode 100644 index 000000000..96531e722 --- /dev/null +++ b/implementations/python/tests/data/realization_envelope/valid/scenario-web-family.json @@ -0,0 +1,15 @@ +{ + "id": "web-family-v1", + "scope": "scenario", + "domains": { + "scenario_name": {"kind": "exact", "value": "web-family"}, + "node_type": {"kind": "exact", "value": "vm"}, + "os": {"kind": "enum", "values": ["linux", "windows"]} + }, + "bindings": [ + {"path": "name", "scope": "scenario", "posture": "exact", "domain": "scenario_name"}, + {"path": "nodes.web.type", "scope": "node", "posture": "exact", "domain": "node_type"}, + {"path": "nodes.web.os", "scope": "field", "posture": "constrained", "domain": "os"} + ], + "witness_policy": {"seed": "linux-first", "selections": {"os": "linux"}} +} diff --git a/implementations/python/tests/data/sdl/invalid/mapping-key-cycle.yaml b/implementations/python/tests/data/sdl/invalid/mapping-key-cycle.yaml new file mode 100644 index 000000000..580433c0f --- /dev/null +++ b/implementations/python/tests/data/sdl/invalid/mapping-key-cycle.yaml @@ -0,0 +1,5 @@ +name: cyclic-alias +nodes: &nodes + sw: + type: switch + roles: *nodes diff --git a/implementations/python/tests/data/sdl/invalid/mapping-key-exact-root.yaml b/implementations/python/tests/data/sdl/invalid/mapping-key-exact-root.yaml new file mode 100644 index 000000000..b9c430b9e --- /dev/null +++ b/implementations/python/tests/data/sdl/invalid/mapping-key-exact-root.yaml @@ -0,0 +1,2 @@ +name: first +name: second diff --git a/implementations/python/tests/data/sdl/invalid/mapping-key-merge.yaml b/implementations/python/tests/data/sdl/invalid/mapping-key-merge.yaml new file mode 100644 index 000000000..638bd9f81 --- /dev/null +++ b/implementations/python/tests/data/sdl/invalid/mapping-key-merge.yaml @@ -0,0 +1,12 @@ +name: merge-conflict +nodes: + first: + type: vm + resources: &resources + ram: 1 gib + cpu: 1 + second: + type: vm + resources: + <<: *resources + cpu: 2 diff --git a/implementations/python/tests/data/sdl/invalid/mapping-key-normalized-nested.yaml b/implementations/python/tests/data/sdl/invalid/mapping-key-normalized-nested.yaml new file mode 100644 index 000000000..5194be127 --- /dev/null +++ b/implementations/python/tests/data/sdl/invalid/mapping-key-normalized-nested.yaml @@ -0,0 +1,5 @@ +name: normalized-nested +nodes: + sw: + Type: switch + type: switch diff --git a/implementations/python/tests/libvirt_conformance_fixtures.py b/implementations/python/tests/libvirt_conformance_fixtures.py index 6f01cbd0e..9a5aed60d 100644 --- a/implementations/python/tests/libvirt_conformance_fixtures.py +++ b/implementations/python/tests/libvirt_conformance_fixtures.py @@ -41,6 +41,8 @@ class RecordedOp: class RecordingLibvirtDriver: """Hermetic libvirt driver that confirms realization and records ops.""" + driver_mode = "generic" + recorded_ops: list[RecordedOp] = field(default_factory=list) _realized: set[str] = field(default_factory=set) diff --git a/implementations/python/tests/libvirt_participant_fixtures.py b/implementations/python/tests/libvirt_participant_fixtures.py index 91f195fec..c597e6a14 100644 --- a/implementations/python/tests/libvirt_participant_fixtures.py +++ b/implementations/python/tests/libvirt_participant_fixtures.py @@ -40,6 +40,7 @@ class NullLibvirtDriver: + driver_mode = "generic" """No-op libvirt driver for structural tests that never call realize().""" def realize(self, *, networks, domains): diff --git a/implementations/python/tests/paths.py b/implementations/python/tests/paths.py index b1b0ab156..ea5a50d01 100644 --- a/implementations/python/tests/paths.py +++ b/implementations/python/tests/paths.py @@ -27,3 +27,4 @@ def _find_repo_root() -> Path: REPO_ROOT = _find_repo_root() EXAMPLES_DIR = REPO_ROOT / "examples" / "scenarios" +EXPERIMENTS_DIR = REPO_ROOT / "examples" / "experiments" diff --git a/implementations/python/tests/test_associated_artifact_manifests.py b/implementations/python/tests/test_associated_artifact_manifests.py new file mode 100644 index 000000000..838895df9 --- /dev/null +++ b/implementations/python/tests/test_associated_artifact_manifests.py @@ -0,0 +1,389 @@ +"""Associated-artifact manifest contract and byte-binding tests (issue #738).""" + +from __future__ import annotations + +import hashlib +import json +from io import BytesIO +from pathlib import Path + +import pytest +from aces_conformance.conformance import _MODEL_VALIDATORS, _fixture_case_diagnostics +from aces_contracts.associated_artifacts import ( + AssociatedArtifactValidationLimits, + associated_artifact_set_digest, + load_associated_artifact_manifest_json, + validate_associated_artifact_manifest, +) +from aces_contracts.contracts import ( + REUSABLE_ASSET_FAMILIES, + AssociatedArtifactManifestModel, + ExperimentApparatusContextModel, + ExperimentRunModel, + ExperimentSpecModel, + ExperimentStudyModel, + ExperimentTaskModel, + schema_bundle, +) +from aces_contracts.versions import ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION +from aces_sdl import canonical_sdl_digest, parse_sdl +from pydantic import ValidationError + +PAYLOAD = b"operator guide\n" +PAYLOAD_SHA256 = hashlib.sha256(PAYLOAD).hexdigest() +REPO_ROOT = Path(__file__).resolve().parents[3] +SCHEMA_PATH = REPO_ROOT / "contracts" / "schemas" / "associated-artifacts" / "associated-artifact-manifest-v1.json" +FIXTURES_ROOT = REPO_ROOT / "contracts" / "fixtures" / "associated-artifacts" / "associated-artifact-manifest-v1" + + +def _manifest_payload(**overrides: object) -> dict[str, object]: + payload: dict[str, object] = { + "schema_version": "associated-artifact-manifest/v1", + "manifest_id": "scenario-guidance", + "manifest_version": "1.0.0", + "canonicalization_profile": "associated-artifact-set/v1", + "scope": "scenario", + "parent_ref": {"ref_kind": "scenario", "ref_id": "training-range"}, + "artifacts": { + "operator-guide": { + "artifact_id": "operator-guide", + "role": "operator-guide", + "media_type": "text/markdown", + "uri": "urn:sha256:" + PAYLOAD_SHA256, + "checksum": {"algorithm": "sha256", "value": PAYLOAD_SHA256}, + "size_bytes": len(PAYLOAD), + "created_at": "2026-07-12T00:00:00Z", + "source": "scenario-author", + "sensitivity": "internal", + } + }, + "set_digest": "sha256:" + ("0" * 64), + } + payload.update(overrides) + return payload + + +def _manifest(**overrides: object) -> AssociatedArtifactManifestModel: + manifest = AssociatedArtifactManifestModel.model_validate(_manifest_payload(**overrides)) + return manifest.model_copy(update={"set_digest": associated_artifact_set_digest(manifest)}) + + +def _codes(diagnostics: tuple[object, ...]) -> set[str]: + return {diagnostic.code for diagnostic in diagnostics} # type: ignore[attr-defined] + + +def test_manifest_set_identity_is_distinct_from_sdl_semantic_identity() -> None: + scenario = parse_sdl("name: training-range\nversion: 1.0.0\n") + manifest = _manifest() + + assert manifest.set_digest == associated_artifact_set_digest(manifest) + assert manifest.set_digest != canonical_sdl_digest(scenario).value + + +def test_validator_binds_every_checksum_to_concrete_bytes() -> None: + scenario = parse_sdl("name: training-range\nversion: 1.0.0\n") + diagnostics = validate_associated_artifact_manifest( + _manifest(), + parent=scenario, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + + assert diagnostics == () + + +def test_validator_rejects_missing_and_digest_only_bindings() -> None: + scenario = parse_sdl("name: training-range\n") + + missing = validate_associated_artifact_manifest(_manifest(), parent=scenario, artifact_readers={}) + digest_only = validate_associated_artifact_manifest( + _manifest(), + parent=scenario, + artifact_readers={"operator-guide": PAYLOAD_SHA256}, + ) + + assert "associated-artifact.payload-binding-missing" in _codes(missing) + assert "associated-artifact.payload-binding-invalid" in _codes(digest_only) + + +def test_validator_rejects_unexpected_and_failing_byte_readers() -> None: + class FailingReader: + def read(self, _size: int) -> bytes: + raise OSError("simulated bounded-reader failure") + + scenario = parse_sdl("name: training-range\n") + unexpected = validate_associated_artifact_manifest( + _manifest(), + parent=scenario, + artifact_readers={ + "operator-guide": BytesIO(PAYLOAD), + "undeclared": BytesIO(b""), + }, + ) + failing = validate_associated_artifact_manifest( + _manifest(), + parent=scenario, + artifact_readers={"operator-guide": FailingReader()}, # type: ignore[dict-item] + ) + + assert "associated-artifact.payload-binding-unexpected" in _codes(unexpected) + assert "associated-artifact.payload-binding-invalid" in _codes(failing) + + +def test_validator_reports_checksum_size_set_and_parent_mismatches() -> None: + scenario = parse_sdl("name: training-range\n") + original = _manifest() + changed_bytes = validate_associated_artifact_manifest( + _manifest(), + parent=scenario, + artifact_readers={"operator-guide": BytesIO(b"changed")}, + ) + changed_set = validate_associated_artifact_manifest( + original.model_copy(update={"set_digest": "sha256:" + ("f" * 64)}), + parent=scenario, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + changed_artifact = original.artifacts["operator-guide"].model_copy(update={"role": "documentation"}) + changed_artifact_set = validate_associated_artifact_manifest( + original.model_copy(update={"artifacts": {"operator-guide": changed_artifact}}), + parent=scenario, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + wrong_parent = validate_associated_artifact_manifest( + _manifest(), + parent=parse_sdl("name: another-scenario\n"), + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + + assert { + "associated-artifact.payload-checksum-mismatch", + "associated-artifact.payload-size-mismatch", + } <= _codes(changed_bytes) + assert "associated-artifact.set-digest-mismatch" in _codes(changed_set) + assert "associated-artifact.set-digest-mismatch" in _codes(changed_artifact_set) + assert "associated-artifact.parent-mismatch" in _codes(wrong_parent) + + +def test_scope_key_and_collision_rules_are_closed() -> None: + experiment_parent = {"ref_kind": "run", "ref_id": "run-1", "ref_version": "1"} + with pytest.raises(ValidationError, match="scope|parent"): + AssociatedArtifactManifestModel.model_validate(_manifest_payload(parent_ref=experiment_parent)) + + keyed_mismatch = _manifest_payload() + keyed_mismatch["artifacts"]["operator-guide"]["artifact_id"] = "different" # type: ignore[index] + with pytest.raises(ValidationError, match="key|artifact_id"): + AssociatedArtifactManifestModel.model_validate(keyed_mismatch) + + alias = _manifest_payload() + artifacts = alias["artifacts"] # type: ignore[assignment] + artifacts["guide-copy"] = dict(artifacts["operator-guide"], artifact_id="guide-copy") # type: ignore[index] + with pytest.raises(ValidationError, match="alias|duplicate"): + AssociatedArtifactManifestModel.model_validate(alias) + + +def test_json_ingress_rejects_duplicate_members_and_secret_bearing_locators() -> None: + duplicate = '{"schema_version":"associated-artifact-manifest/v1","manifest_id":"a","manifest_id":"b"}' + with pytest.raises(ValueError, match="duplicate JSON member"): + load_associated_artifact_manifest_json(duplicate) + + secret_uri = _manifest_payload() + secret_uri["artifacts"]["operator-guide"]["uri"] = "https://user:secret@example.test/guide" # type: ignore[index] + with pytest.raises(ValidationError, match="credential|userinfo|secret"): + AssociatedArtifactManifestModel.model_validate(secret_uri) + + signed_uri = _manifest_payload() + signed_uri["artifacts"]["operator-guide"]["uri"] = ( # type: ignore[index] + "https://example.test/guide?X-Amz-Credential=temporary&X-Amz-Signature=secret" + ) + with pytest.raises(ValidationError, match="secret"): + AssociatedArtifactManifestModel.model_validate(signed_uri) + + +def test_streaming_limits_reject_before_or_during_reads() -> None: + scenario = parse_sdl("name: training-range\n") + limits = AssociatedArtifactValidationLimits(max_artifacts=1, max_artifact_bytes=4, max_total_bytes=4) + + diagnostics = validate_associated_artifact_manifest( + _manifest(), + parent=scenario, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + limits=limits, + ) + + assert "associated-artifact.resource-limit-exceeded" in _codes(diagnostics) + + +def test_artifact_count_limit_is_enforced_independently_of_byte_limits() -> None: + payload = _manifest_payload() + artifacts = payload["artifacts"] # type: ignore[assignment] + artifacts["guide-copy"] = { # type: ignore[index] + **artifacts["operator-guide"], # type: ignore[index] + "artifact_id": "guide-copy", + "role": "documentation", + "uri": "urn:example:guide-copy", + } + manifest = AssociatedArtifactManifestModel.model_validate(payload) + manifest = manifest.model_copy(update={"set_digest": associated_artifact_set_digest(manifest)}) + + diagnostics = validate_associated_artifact_manifest( + manifest, + parent=parse_sdl("name: training-range\n"), + artifact_readers={}, + limits=AssociatedArtifactValidationLimits( + max_artifacts=1, + max_artifact_bytes=len(PAYLOAD), + max_total_bytes=2 * len(PAYLOAD), + ), + ) + + assert _codes(diagnostics) == {"associated-artifact.resource-limit-exceeded"} + + +def test_blake3_payloads_supported_by_the_public_checksum_contract_can_conform() -> None: + empty_blake3 = "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" + payload = _manifest_payload() + artifact = payload["artifacts"]["operator-guide"] # type: ignore[index] + artifact["uri"] = "urn:blake3:" + empty_blake3 + artifact["checksum"] = {"algorithm": "blake3", "value": empty_blake3} + artifact["size_bytes"] = 0 + manifest = AssociatedArtifactManifestModel.model_validate(payload) + manifest = manifest.model_copy(update={"set_digest": associated_artifact_set_digest(manifest)}) + + assert ( + validate_associated_artifact_manifest( + manifest, + parent=parse_sdl("name: training-range\n"), + artifact_readers={"operator-guide": BytesIO(b"")}, + ) + == () + ) + + +def test_contract_is_registered_with_fixed_schema_and_trust_family() -> None: + assert ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION == "associated-artifact-manifest/v1" + assert "associated-artifact-manifest-v1" in schema_bundle() + assert "associated_artifact_set" in REUSABLE_ASSET_FAMILIES + + +def test_generic_conformance_runner_reports_required_external_semantic_context() -> None: + manifest = _manifest() + + assert "associated-artifact-manifest-v1" not in _MODEL_VALIDATORS + diagnostics = _fixture_case_diagnostics("associated-artifact-manifest-v1", manifest.model_dump(mode="json")) + assert _codes(tuple(diagnostics)) == {"conformance.semantic-context-required"} + + +@pytest.mark.parametrize( + ("ref_kind", "fixture", "model", "id_field", "version_field"), + [ + ("task", "experiment-task-v1", ExperimentTaskModel, "task_id", "task_version"), + ( + "authoring-input", + "experiment-authoring-input-v1", + ExperimentSpecModel, + "spec_id", + "spec_version", + ), + ( + "apparatus-context", + "experiment-apparatus-context-v1", + ExperimentApparatusContextModel, + "apparatus_context_id", + "context_version", + ), + ("run", "experiment-run-v1", ExperimentRunModel, "run_id", "run_version"), + ("study", "experiment-study-v1", ExperimentStudyModel, "study_id", "study_version"), + ], +) +def test_every_experiment_attachment_scope_binds_its_concrete_parent( + ref_kind: str, + fixture: str, + model: type, + id_field: str, + version_field: str, +) -> None: + path = REPO_ROOT / "contracts" / "fixtures" / "experiment-core" / fixture / "valid" / "reference.json" + parent = model.model_validate(json.loads(path.read_text(encoding="utf-8"))) + parent_ref = { + "ref_kind": ref_kind, + "ref_id": getattr(parent, id_field), + "ref_version": getattr(parent, version_field), + } + manifest = _manifest(scope="experiment", parent_ref=parent_ref) + + assert ( + validate_associated_artifact_manifest( + manifest, + parent=parent, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + == () + ) + wrong_version_manifest = _manifest( + scope="experiment", + parent_ref={**parent_ref, "ref_version": "definitely-not-the-parent-version"}, + ) + assert "associated-artifact.parent-mismatch" in _codes( + validate_associated_artifact_manifest( + wrong_version_manifest, + parent=parent, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + ) + + +def test_snapshot_parent_digest_is_checked_without_changing_sdl_identity() -> None: + scenario = parse_sdl("name: training-range\nversion: 1.0.0\n") + parent_ref = { + "ref_kind": "scenario-snapshot", + "ref_id": scenario.name, + "ref_version": scenario.version, + "ref_digest": canonical_sdl_digest(scenario).value, + } + manifest = _manifest(parent_ref=parent_ref) + + assert ( + validate_associated_artifact_manifest( + manifest, + parent=scenario, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + == () + ) + other_parent = _manifest(parent_ref={"ref_kind": "scenario", "ref_id": scenario.name}) + assert manifest.set_digest != other_parent.set_digest + + wrong_digest = _manifest(parent_ref={**parent_ref, "ref_digest": "sha256:" + ("0" * 64)}) + wrong_version = _manifest(parent_ref={**parent_ref, "ref_version": "2.0.0"}) + for mismatched_manifest in (wrong_digest, wrong_version): + assert "associated-artifact.parent-mismatch" in _codes( + validate_associated_artifact_manifest( + mismatched_manifest, + parent=scenario, + artifact_readers={"operator-guide": BytesIO(PAYLOAD)}, + ) + ) + + +def test_published_schema_and_fixture_corpus_cover_both_attachment_scopes() -> None: + import jsonschema + + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + valid_paths = sorted((FIXTURES_ROOT / "valid").glob("*.json")) + invalid_paths = sorted((FIXTURES_ROOT / "invalid").glob("*.json")) + assert {path.stem for path in valid_paths} == { + "apparatus-context", + "authoring-input", + "run", + "scenario", + "scenario-snapshot", + "study", + "task", + } + assert invalid_paths + for path in valid_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + jsonschema.validate(payload, schema) + AssociatedArtifactManifestModel.model_validate(payload) + for path in invalid_paths: + with pytest.raises(ValidationError): + AssociatedArtifactManifestModel.model_validate(json.loads(path.read_text(encoding="utf-8"))) diff --git a/implementations/python/tests/test_authority_boundary.py b/implementations/python/tests/test_authority_boundary.py index eb98c99f0..a90dec6dc 100644 --- a/implementations/python/tests/test_authority_boundary.py +++ b/implementations/python/tests/test_authority_boundary.py @@ -4,12 +4,17 @@ from pathlib import Path import pytest +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 tools.check_authority_boundary import ( # noqa: E402 + _REQUIRED_ARTIFACT_FAMILY_FIELDS, + _REQUIRED_AUTHORITY_ROOT_FIELDS, + _REQUIRED_NON_NORMATIVE_ROOT_FIELDS, + _REQUIRED_SCHEMA_AUTHORITY_FIELDS, ADR_AUTHORITY_RELATIVE_PATH, ADR_REFS, ADR_SEAM_REF, @@ -26,6 +31,26 @@ evaluate_authority_boundary, ) + +def _policy_without_entry_field(section: str, index: int, field: str) -> str: + """Return `_GOOD_POLICY` with one field deleted from a list entry. + + Parses the canonical YAML, drops `section[index][field]`, and re-dumps. + Used to exercise every field of a `_REQUIRED_*_FIELDS` tuple (rather than a + single hand-picked field) so dropping a field from the tuple in production + leaves a failing test. + """ + data = yaml.safe_load(_GOOD_POLICY) + del data[section][index][field] + return yaml.safe_dump(data, sort_keys=False) + + +def _policy_without_schema_authority_field(field: str) -> str: + data = yaml.safe_load(_GOOD_POLICY) + del data["schema_authority"][field] + return yaml.safe_dump(data, sort_keys=False) + + # --------------------------------------------------------------------------- # # Canonical, well-formed authority-boundary YAML used as the positive case # # and as the starting point for every mutation test. Mirrors the real # @@ -55,10 +80,18 @@ root: contracts/profiles/ authority: capability profile declarations family: profiles + - id: normative_realization_envelopes + root: contracts/realization-envelopes/ + authority: configuration-bound backend realization envelope declarations + family: realization-envelopes - id: normative_concept_authority root: contracts/concept-authority/ authority: concept-family and controlled-vocabulary authority artifacts family: concept-authority + - id: normative_provenance + root: contracts/provenance/ + authority: revision-pinned lineage and derivation records + family: provenance non_normative_roots: - id: reference_implementations root: implementations/ @@ -78,9 +111,6 @@ - id: tooling root: tools/ note: tooling - - id: changelog_fragments - root: changelog.d/ - note: towncrier fragments legacy_top_level_dirs: - schemas - conformance @@ -99,11 +129,19 @@ family: governance-guidance requirement_refs: - AUT-811 + - id: deprecation_records + artifact: specs/evolution/deprecation-records.yaml + authority: ecosystem deprecation and lifecycle records + family: deprecation-records + requirement_refs: + - GOV-902 """ # ADR-009 is immutable; the drift guard requires every authority-root token # (specs/, contracts/schemas/, contracts/fixtures/, contracts/profiles/, -# contracts/concept-authority/) to appear in its text. ADR-019 is the +# contracts/realization-envelopes/, contracts/concept-authority/, +# contracts/provenance/) to appear +# across the immutable ADR pair. ADR-019 is the # canonical-seam decision — required by adr_refs and by the drift guard. _GOOD_ADR_AUTHORITY = """# ADR-009: Normative Artifact Authority and Repository Structure @@ -144,7 +182,9 @@ "contracts/schemas", "contracts/fixtures", "contracts/profiles", + "contracts/realization-envelopes", "contracts/concept-authority", + "contracts/provenance", ) _GOOD_NON_NORMATIVE_ROOTS: tuple[str, ...] = ( "implementations", @@ -153,7 +193,6 @@ "research", "notes", "tools", - "changelog.d", ) @@ -175,7 +214,7 @@ def _seed_repo( *, policy_body: str | None = _GOOD_POLICY, adr_body: str | None = _GOOD_ADR_AUTHORITY, - seam_body: str | None = "ADR-019 stub mentioning concept-authority.\n", + seam_body: str | None = "ADR-019 stub mentioning concept-authority, realization-envelopes, and provenance.\n", contracts_readme: str | None = _GOOD_CONTRACTS_README, specs_readme: str | None = _GOOD_SPECS_README, authority_roots: tuple[str, ...] = _GOOD_AUTHORITY_ROOTS, @@ -200,7 +239,8 @@ def _seed_repo( if seam_body is not None: # ADR-019 governs the manifest YAML; the drift guard unions it with # ADR-009. The seed writes a stub that mentions `concept-authority` - # so the canonical positive case clears the drift check. + # and `realization-envelopes` so the canonical positive case clears + # the drift check. seam_relative = "docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md" seam_path = tmp_path / seam_relative seam_path.parent.mkdir(parents=True, exist_ok=True) @@ -225,6 +265,13 @@ def _seed_repo( if not guidance_path.exists(): guidance_path.write_text("profile: aces-agent-guidance\n", encoding="utf-8") + # The canonical deprecation-records ledger (GOV-902) is likewise classified + # in _GOOD_POLICY and must exist on disk for the positive case. + deprecation_path = tmp_path / "specs" / "evolution" / "deprecation-records.yaml" + deprecation_path.parent.mkdir(parents=True, exist_ok=True) + if not deprecation_path.exists(): + deprecation_path.write_text("policy: ecosystem-deprecation-records\n", encoding="utf-8") + for root in authority_roots: _materialise_root(tmp_path, root) for root in non_normative_roots: @@ -290,15 +337,17 @@ def test_policy_value_is_normative_artifact_authority() -> None: assert POLICY_VALUE == "normative-artifact-authority" -def test_canonical_authority_root_ids_cover_all_five_families() -> None: - # The five families ADR-009 names: prose, schemas, fixtures, profiles, - # concept-authority. A YAML that drops any of these fails the gate. +def test_canonical_authority_root_ids_cover_every_family() -> None: + # ADR-009 and ADR-019 name these authority families. A YAML that drops any + # of them fails the gate. assert set(CANONICAL_AUTHORITY_ROOT_IDS) == { "normative_prose", "normative_schemas", "normative_fixtures", "normative_profiles", + "normative_realization_envelopes", "normative_concept_authority", + "normative_provenance", } @@ -371,6 +420,12 @@ def test_policy_root_must_be_mapping(tmp_path: Path) -> None: " family: governance-guidance\n" " requirement_refs:\n" " - AUT-811\n" + " - id: deprecation_records\n" + " artifact: specs/evolution/deprecation-records.yaml\n" + " authority: ecosystem deprecation and lifecycle records\n" + " family: deprecation-records\n" + " requirement_refs:\n" + " - GOV-902\n" ), ), ] @@ -476,15 +531,16 @@ def test_legacy_top_level_dirs_non_list_value_is_rejected(tmp_path: Path) -> Non assert _flagged(failures, "authority-boundary-field-type") -def test_authority_root_required_id_is_rejected(tmp_path: Path) -> None: - # An authority_roots entry missing `id` is a malformation, not a default. - body = _GOOD_POLICY.replace( - " - id: normative_prose\n root: specs/\n authority: normative prose specifications\n family: prose\n", - " - root: specs/\n authority: normative prose specifications\n family: prose\n", - 1, - ) +@pytest.mark.parametrize("field", _REQUIRED_AUTHORITY_ROOT_FIELDS) +def test_authority_root_required_fields_are_rejected(tmp_path: Path, field: str) -> None: + # An authority_roots entry missing ANY required field is a malformation, not + # a default. Parametrised over the full tuple so dropping a field from + # _REQUIRED_AUTHORITY_ROOT_FIELDS in production leaves a failing test. + body = _policy_without_entry_field("authority_roots", 0, field) failures = evaluate_authority_boundary(_seed_repo(tmp_path, policy_body=body)) - assert _flagged(failures, "authority-boundary-entry-field") + assert any( + f.rule_id == "authority-boundary-entry-field" and f"required field: {field}" in f.message for f in failures + ), f"expected an entry-field failure naming {field!r}; got: {[f.render() for f in failures]}" def test_authority_root_must_end_with_slash(tmp_path: Path) -> None: @@ -518,7 +574,7 @@ def test_authority_root_must_not_traverse_parent(tmp_path: Path) -> None: # --------------------------------------------------------------------------- # -# Canonical family coverage -- ADR-009's five families MUST each have an # +# Canonical family coverage -- every governed family MUST have an entry. # # entry. Reordering or renaming an id is a drift. # # --------------------------------------------------------------------------- # @@ -624,18 +680,15 @@ def test_authority_root_with_only_keep_marker_is_flagged_as_empty(tmp_path: Path ) -def test_non_normative_root_required_id_is_rejected(tmp_path: Path) -> None: - # Symmetric coverage to test_authority_root_required_id_is_rejected: - # a non_normative_roots entry missing `id` must fail the entry-field - # check. Without this test the entry-field validation loop in - # _check_non_normative_roots is entirely untested. - body = _GOOD_POLICY.replace( - " - id: reference_implementations\n root: implementations/\n note: reference code only\n", - " - root: implementations/\n note: reference code only\n", - 1, - ) +@pytest.mark.parametrize("field", _REQUIRED_NON_NORMATIVE_ROOT_FIELDS) +def test_non_normative_root_required_fields_are_rejected(tmp_path: Path, field: str) -> None: + # Symmetric full-tuple coverage for _check_non_normative_roots: every + # required field of a non_normative_roots entry must be enforced. + body = _policy_without_entry_field("non_normative_roots", 0, field) failures = evaluate_authority_boundary(_seed_repo(tmp_path, policy_body=body)) - assert _flagged(failures, "authority-boundary-entry-field") + assert any( + f.rule_id == "authority-boundary-entry-field" and f"required field: {field}" in f.message for f in failures + ), f"expected an entry-field failure naming {field!r}; got: {[f.render() for f in failures]}" def test_legacy_dir_present_at_root_is_flagged(tmp_path: Path) -> None: @@ -820,6 +873,18 @@ def test_contracts_readme_missing_adr_019_reference_is_flagged(tmp_path: Path) - # --------------------------------------------------------------------------- # +@pytest.mark.parametrize("field", _REQUIRED_SCHEMA_AUTHORITY_FIELDS) +def test_schema_authority_required_subfields_are_rejected(tmp_path: Path, field: str) -> None: + # Dropping any required sub-field from an otherwise-present schema_authority + # block must fail — the whole-block-absence case (_TOP_LEVEL_FIELD_CASES) + # does not cover per-sub-field requiredness. + body = _policy_without_schema_authority_field(field) + failures = evaluate_authority_boundary(_seed_repo(tmp_path, policy_body=body)) + assert any( + f.rule_id == "authority-boundary-entry-field" and f"required field: {field}" in f.message for f in failures + ), f"expected a schema_authority entry-field failure naming {field!r}; got: {[f.render() for f in failures]}" + + def test_schema_authority_normative_root_must_match_authority_root(tmp_path: Path) -> None: body = _GOOD_POLICY.replace( " normative_root: contracts/schemas/\n", @@ -1182,15 +1247,22 @@ def test_invalid_forbidden_root_does_not_walk_unrelated_tree(tmp_path: Path) -> " family: governance-guidance\n" " requirement_refs:\n" " - AUT-811\n" + " - id: deprecation_records\n" + " artifact: specs/evolution/deprecation-records.yaml\n" + " authority: ecosystem deprecation and lifecycle records\n" + " family: deprecation-records\n" + " requirement_refs:\n" + " - GOV-902\n" ) def test_canonical_artifact_family_binding_pins_agent_guidance() -> None: - # The agent-guidance AUT-811 profile is the pinned governance-guidance - # artifact; a rename of the constant surfaces in the suite rather than - # silently relaxing the gate. + # The agent-guidance AUT-811 profile and the GOV-902 deprecation-records + # ledger are the pinned artifact families; a rename of either constant + # surfaces in the suite rather than silently relaxing the gate. assert CANONICAL_ARTIFACT_FAMILY_BINDING == { "specs/agent-guidance/agent-guidance.yaml": "governance-guidance", + "specs/evolution/deprecation-records.yaml": "deprecation-records", } @@ -1215,11 +1287,16 @@ def test_artifact_family_relabel_is_flagged(tmp_path: Path) -> None: assert _flagged(failures, "authority-boundary-artifact-family-canonical-binding") -def test_artifact_family_missing_required_field_is_flagged(tmp_path: Path) -> None: - # An entry missing `family` is a malformation, not a default. - body = _GOOD_POLICY.replace(" family: governance-guidance\n", "", 1) +@pytest.mark.parametrize("field", _REQUIRED_ARTIFACT_FAMILY_FIELDS) +def test_artifact_family_missing_required_field_is_flagged(tmp_path: Path, field: str) -> None: + # An artifact-family entry missing ANY required field is a malformation, not + # a default — full-tuple coverage over _REQUIRED_ARTIFACT_FAMILY_FIELDS. + body = _policy_without_entry_field("normative_artifact_families", 0, field) failures = evaluate_authority_boundary(_seed_repo(tmp_path, policy_body=body)) - assert _flagged(failures, "authority-boundary-artifact-family-entry-field") + assert any( + f.rule_id == "authority-boundary-artifact-family-entry-field" and f"required field: {field}" in f.message + for f in failures + ), f"expected an artifact-family entry-field failure naming {field!r}; got: {[f.render() for f in failures]}" def test_artifact_family_entry_must_be_mapping(tmp_path: Path) -> None: diff --git a/implementations/python/tests/test_backend_manifest.py b/implementations/python/tests/test_backend_manifest.py index d1ff822f1..8d5038c7f 100644 --- a/implementations/python/tests/test_backend_manifest.py +++ b/implementations/python/tests/test_backend_manifest.py @@ -38,7 +38,9 @@ FIXTURES_ROOT = Path(__file__).resolve().parents[3] / "contracts" / "fixtures" 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 = list(BACKEND_SUPPORTED_CONTRACT_IDS) +EXPECTED_SUPPORTED_CONTRACT_VERSIONS_V2 = [ + contract_id for contract_id in BACKEND_SUPPORTED_CONTRACT_IDS if contract_id != "realization-envelope-v1" +] def test_backend_workflow_vocab_enum_values(): @@ -71,6 +73,11 @@ def test_backend_manifest_rejects_hollow_defaults(): ) +def test_backend_manifest_rejects_unknown_keywords(): + with pytest.raises(TypeError, match="unexpected keyword argument.*unknown"): + BackendManifest(unknown=True) + + def test_provisioner_capabilities_reject_hollow_declaration(): with pytest.raises(ValueError): ProvisionerCapabilities( @@ -103,7 +110,9 @@ def test_backend_manifest_v2_roundtrip_from_stub_manifest(): WorkflowFeature.TIMEOUTS, ] assert model.realization_support[0].support_mode.value == "constrained" - assert model.model_dump(mode="json") == payload + roundtrip = model.model_dump(mode="json") + roundtrip.pop("realization_envelope", None) + assert roundtrip == payload def test_backend_manifest_v2_declares_participant_capability_dimensions(): diff --git a/implementations/python/tests/test_corpus_packaging.py b/implementations/python/tests/test_corpus_packaging.py index 5067345c2..aa9bcd87d 100644 --- a/implementations/python/tests/test_corpus_packaging.py +++ b/implementations/python/tests/test_corpus_packaging.py @@ -33,11 +33,15 @@ _CORPUS_PREFIX = "aces_contracts/_corpus/" _FAMILY_PROBES = { "profiles": "aces_contracts/_corpus/profiles/backend/provisioning-only.json", + "scientific-completeness": "aces_contracts/_corpus/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json", "fixtures": "aces_contracts/_corpus/fixtures/", "concept-authority": "aces_contracts/_corpus/concept-authority/controlled-vocabularies-v1.json", "schemas": "aces_contracts/_corpus/schemas/", + "provenance": "aces_contracts/_corpus/provenance/sdl-lineage-ledger-v1.json", } +_NOTICE_PATH = "aces_contracts/_corpus/provenance/THIRD_PARTY_NOTICES.md" + _UV = shutil.which("uv") requires_uv = pytest.mark.skipif(_UV is None, reason="uv toolchain not available") @@ -98,6 +102,14 @@ def test_built_wheel_bundles_each_corpus_family(built_wheel: Path, family: str): ) +@requires_uv +def test_built_wheel_includes_third_party_notice(built_wheel: Path): + with zipfile.ZipFile(built_wheel) as zf: + notice = zf.read(_NOTICE_PATH).decode("utf-8") + assert "Copyright (c) 2022 CR14" in notice + assert "fe83e8281fc4b954967fbaa5a0d099007ddcb06c" in notice + + @requires_uv def test_corpus_discoverable_via_importlib_resources_from_installed_wheel(installed_python: Path, tmp_path: Path): """Acceptance: the corpus is discoverable via ``importlib.resources`` from @@ -111,6 +123,7 @@ def test_corpus_discoverable_via_importlib_resources_from_installed_wheel(instal "print(json.dumps({\n" " 'root': str(root),\n" " 'backend_profile': (corpus_family_root('profiles')/'backend'/'provisioning-only.json').exists(),\n" + " 'scientific_completeness': (corpus_family_root('profiles')/'scientific-completeness'/'scientific-scenario-completeness-rev1.json').exists(),\n" " 'controlled_vocab': (corpus_family_root('concept-authority')/'controlled-vocabularies-v1.json').exists(),\n" " 'fixtures_dir': corpus_family_root('fixtures').is_dir(),\n" " 'schemas_dir': corpus_family_root('schemas').is_dir(),\n" @@ -127,6 +140,7 @@ def test_corpus_discoverable_via_importlib_resources_from_installed_wheel(instal assert "site-packages" in resolved.parts, f"corpus resolved outside site-packages: {resolved}" assert resolved != REPO_CONTRACTS.resolve(), "installed dist fell back to the repo contracts/ tree" assert payload["backend_profile"] is True + assert payload["scientific_completeness"] is True assert payload["controlled_vocab"] is True assert payload["fixtures_dir"] is True assert payload["schemas_dir"] is True diff --git a/implementations/python/tests/test_deprecation_lifecycle.py b/implementations/python/tests/test_deprecation_lifecycle.py new file mode 100644 index 000000000..c43a9f661 --- /dev/null +++ b/implementations/python/tests/test_deprecation_lifecycle.py @@ -0,0 +1,376 @@ +from __future__ import annotations + +import copy +import sys +from pathlib import Path + +import pytest +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 tools.check_deprecation_lifecycle import ( # noqa: E402 + _REQUIRED_RECORD_FIELDS, + _REQUIRED_TOP_LEVEL_FIELDS, + ADR_REF, + CANONICAL_DEPRECATION_RECORD_IDS, + DEPRECATION_RECORDS_RELATIVE_PATH, + POLICY_VALUE, + REQUIREMENT_REF, + SCHEMA_PUBLICATION_MANIFEST, + SPEC_RELATIVE_PATH, + STATUSES, + SURFACE_CLASSES, + evaluate_deprecation_records, +) + +# --------------------------------------------------------------------------- # +# A minimal, well-formed deprecation-records ledger used as the positive case # +# and as the starting point for every mutation test. It mirrors the real # +# specs/evolution/deprecation-records.yaml shape. # +# --------------------------------------------------------------------------- # + + +def _good_ledger() -> dict: + # Includes both 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 { + "policy": POLICY_VALUE, + "requirement_refs": [REQUIREMENT_REF], + "adr_refs": [ADR_REF], + "spec": SPEC_RELATIVE_PATH, + "records": [ + { + "id": "aces-compat-namespace", + "surface_class": "python-distribution", + "identifier": "implementations/python/src/aces/ (legacy aces.* namespace)", + "status": "deprecated", + "first_notice": "ADR-010 (example)", + "replacement": "the owning packages under implementations/python/packages/", + "migration_reference": "docs/decisions/adrs/adr-010-...md", + "notice_window": "supported through the pre-1.0 series", + "verification_evidence": "check_repo_policy.py enforces wrapper-only + import ban", + }, + { + "id": "sdl-import-path-field", + "surface_class": "sdl-scenario-module", + "identifier": "ImportDecl.path (module import 'path:' field)", + "status": "deprecated", + "first_notice": "ADR-053 (example)", + "replacement": "the 'source:' field", + "migration_reference": "docs/explain/sdl/parser.md", + "notice_window": "supported indefinitely as a backward-compatible alias", + "verification_evidence": "parser normalises the legacy field; tests exercise both forms", + }, + ], + } + + +def _write_repo( + tmp_path: Path, + ledger: object, + *, + write_spec: bool = True, + manifest: dict | None = None, +) -> Path: + """Materialise a minimal repo layout under ``tmp_path`` and return it. + + ``ledger`` is dumped to the canonical records path. The spec file the ledger + points at is written unless ``write_spec`` is False, and a schema + publication manifest is written when ``manifest`` is provided. + """ + records_path = tmp_path / DEPRECATION_RECORDS_RELATIVE_PATH + records_path.parent.mkdir(parents=True, exist_ok=True) + records_path.write_text(yaml.safe_dump(ledger, sort_keys=False), encoding="utf-8") + + if write_spec: + spec_path = tmp_path / SPEC_RELATIVE_PATH + spec_path.parent.mkdir(parents=True, exist_ok=True) + spec_path.write_text("# evolution policy (test stub)\n", encoding="utf-8") + + if manifest is not None: + manifest_path = tmp_path / SCHEMA_PUBLICATION_MANIFEST + manifest_path.parent.mkdir(parents=True, exist_ok=True) + import json + + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + + return tmp_path + + +def _rule_ids(tmp_path: Path) -> set[str]: + return {failure.rule_id for failure in evaluate_deprecation_records(tmp_path)} + + +# --------------------------------------------------------------------------- # +# Positive cases. # +# --------------------------------------------------------------------------- # + + +def test_real_repo_ledger_conforms() -> None: + """The checked-in ledger must pass its own gate.""" + assert evaluate_deprecation_records(REPO_ROOT) == [] + + +def test_good_ledger_passes(tmp_path: Path) -> None: + _write_repo(tmp_path, _good_ledger()) + assert evaluate_deprecation_records(tmp_path) == [] + + +def test_no_replacement_rationale_is_accepted(tmp_path: Path) -> None: + ledger = _good_ledger() + record = ledger["records"][0] + del record["replacement"] + record["no_replacement_rationale"] = "the surface is withdrawn with no successor by design" + _write_repo(tmp_path, ledger) + assert evaluate_deprecation_records(tmp_path) == [] + + +def test_complete_security_exception_is_accepted(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0]["security_exception"] = { + "affected_versions": "0.1.0 through 0.19.1", + "impact": "example impact", + "mitigation": "example mitigation", + "migration": "example migration path", + "review_authority": "maintainers", + } + _write_repo(tmp_path, ledger) + assert evaluate_deprecation_records(tmp_path) == [] + + +def test_removed_schema_with_tombstone_is_accepted(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0].update( + { + "surface_class": "published-json-schema", + "status": "removed", + "removal_record": "release note X; ADR-061 tombstone", + "removal_tombstone": "contracts/schemas/old/legacy-v1.json", + } + ) + manifest = { + "schema_version": "schema-publication-manifest/v1", + "hash_algorithm": "sha256", + "schemas": [], + "removed_schemas": [{"schema_path": "contracts/schemas/old/legacy-v1.json", "summary": "removed"}], + } + _write_repo(tmp_path, ledger, manifest=manifest) + assert evaluate_deprecation_records(tmp_path) == [] + + +# --------------------------------------------------------------------------- # +# Negative cases — one per rule_id. # +# --------------------------------------------------------------------------- # + + +def test_missing_file(tmp_path: Path) -> None: + assert "deprecation-records-missing" in _rule_ids(tmp_path) + + +def test_parse_error(tmp_path: Path) -> None: + records_path = tmp_path / DEPRECATION_RECORDS_RELATIVE_PATH + records_path.parent.mkdir(parents=True, exist_ok=True) + records_path.write_text("policy: [unterminated\n", encoding="utf-8") + assert "deprecation-records-parse" in _rule_ids(tmp_path) + + +def test_top_level_not_mapping(tmp_path: Path) -> None: + _write_repo(tmp_path, ["not", "a", "mapping"]) + assert "deprecation-records-shape" in _rule_ids(tmp_path) + + +@pytest.mark.parametrize("field", _REQUIRED_TOP_LEVEL_FIELDS) +def test_missing_top_level_field(tmp_path: Path, field: str) -> None: + # Parametrised over the FULL required-tuple so dropping any field from the + # tuple leaves a failing test, not a silent no-op. + ledger = _good_ledger() + del ledger[field] + _write_repo(tmp_path, ledger) + failures = evaluate_deprecation_records(tmp_path) + assert any(f.rule_id == "deprecation-records-field" and field in f.message for f in failures), ( + f"expected a missing-field failure naming {field!r}; got: {[f.render() for f in failures]}" + ) + + +def test_records_wrong_type(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"] = "not-a-list" + _write_repo(tmp_path, ledger) + assert "deprecation-records-field-type" in _rule_ids(tmp_path) + + +def test_wrong_policy_value(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["policy"] = "something-else" + _write_repo(tmp_path, ledger) + assert "deprecation-records-policy-value" in _rule_ids(tmp_path) + + +def test_missing_requirement_ref(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["requirement_refs"] = ["OTHER-001"] + _write_repo(tmp_path, ledger) + assert "deprecation-records-requirement-ref" in _rule_ids(tmp_path) + + +def test_missing_adr_ref(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["adr_refs"] = ["ADR-999"] + _write_repo(tmp_path, ledger) + assert "deprecation-records-adr-ref" in _rule_ids(tmp_path) + + +def test_spec_missing_on_disk(tmp_path: Path) -> None: + _write_repo(tmp_path, _good_ledger(), write_spec=False) + assert "deprecation-records-spec-missing" in _rule_ids(tmp_path) + + +@pytest.mark.parametrize("field", _REQUIRED_RECORD_FIELDS) +def test_record_missing_required_field(tmp_path: Path, field: str) -> None: + # Full-tuple coverage: every required record field is exercised, and the + # failure must name the dropped field (not merely fire the generic rule). + ledger = _good_ledger() + del ledger["records"][0][field] + _write_repo(tmp_path, ledger) + failures = evaluate_deprecation_records(tmp_path) + assert any(f.rule_id == "deprecation-records-entry-field" and field in f.message for f in failures), ( + f"expected a missing-field failure naming {field!r}; got: {[f.render() for f in failures]}" + ) + + +def test_record_empty_field(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0]["identifier"] = " " + _write_repo(tmp_path, ledger) + assert "deprecation-records-entry-field" in _rule_ids(tmp_path) + + +def test_duplicate_record_id(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"].append(copy.deepcopy(ledger["records"][0])) + _write_repo(tmp_path, ledger) + assert "deprecation-records-entry-duplicate" in _rule_ids(tmp_path) + + +def test_unknown_surface_class(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0]["surface_class"] = "not-a-real-surface" + _write_repo(tmp_path, ledger) + assert "deprecation-records-surface-class" in _rule_ids(tmp_path) + + +def test_bad_status(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0]["status"] = "sunset" + _write_repo(tmp_path, ledger) + assert "deprecation-records-status" in _rule_ids(tmp_path) + + +def test_both_replacement_and_no_replacement(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0]["no_replacement_rationale"] = "conflicting" + _write_repo(tmp_path, ledger) + assert "deprecation-records-replacement" in _rule_ids(tmp_path) + + +def test_neither_replacement_nor_rationale(tmp_path: Path) -> None: + ledger = _good_ledger() + del ledger["records"][0]["replacement"] + _write_repo(tmp_path, ledger) + assert "deprecation-records-replacement" in _rule_ids(tmp_path) + + +def test_incomplete_security_exception(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0]["security_exception"] = {"impact": "only one field"} + _write_repo(tmp_path, ledger) + assert "deprecation-records-security-exception" in _rule_ids(tmp_path) + + +def test_removed_without_removal_record(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0]["status"] = "removed" + _write_repo(tmp_path, ledger) + assert "deprecation-records-removal-record" in _rule_ids(tmp_path) + + +def test_removed_schema_without_tombstone(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0].update( + { + "surface_class": "published-json-schema", + "status": "removed", + "removal_record": "release note", + } + ) + _write_repo(tmp_path, ledger) + assert "deprecation-records-removal-tombstone" in _rule_ids(tmp_path) + + +def test_removed_schema_tombstone_not_in_manifest(tmp_path: Path) -> None: + ledger = _good_ledger() + ledger["records"][0].update( + { + "surface_class": "published-json-schema", + "status": "removed", + "removal_record": "release note", + "removal_tombstone": "contracts/schemas/absent.json", + } + ) + manifest = { + "schema_version": "schema-publication-manifest/v1", + "hash_algorithm": "sha256", + "schemas": [], + "removed_schemas": [], + } + _write_repo(tmp_path, ledger, manifest=manifest) + assert "deprecation-records-removal-tombstone" in _rule_ids(tmp_path) + + +# --------------------------------------------------------------------------- # +# Retention floor — lifecycle history may not silently disappear. # +# --------------------------------------------------------------------------- # + + +def test_empty_ledger_is_rejected(tmp_path: Path) -> None: + # An authoritative record surface that permits `records: []` would let all + # lifecycle history be erased while the gate stays green. + ledger = _good_ledger() + ledger["records"] = [] + _write_repo(tmp_path, ledger) + assert "deprecation-records-canonical-record-missing" in _rule_ids(tmp_path) + + +def test_dropping_a_canonical_record_is_rejected(tmp_path: Path) -> None: + # Deleting an established record (leaving the other) 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 + _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) + assert any(dropped["id"] in f.message for f in failures), ( + f"expected the dropped record id to be named; got: {[f.render() for f in failures]}" + ) + + +# --------------------------------------------------------------------------- # +# Guard the pinned constants so a rename surfaces here. # +# --------------------------------------------------------------------------- # + + +def test_canonical_record_ids_are_pinned() -> None: + assert {"aces-compat-namespace", "sdl-import-path-field"} == CANONICAL_DEPRECATION_RECORD_IDS + + +def test_surface_classes_cover_known_matrix_rows() -> None: + assert {"python-distribution", "sdl-scenario-module", "published-json-schema"} <= SURFACE_CLASSES + + +def test_statuses_are_deprecated_and_removed() -> None: + assert {"deprecated", "removed"} == STATUSES diff --git a/implementations/python/tests/test_dsl_115_authoring_specificity.py b/implementations/python/tests/test_dsl_115_authoring_specificity.py new file mode 100644 index 000000000..62da683ce --- /dev/null +++ b/implementations/python/tests/test_dsl_115_authoring_specificity.py @@ -0,0 +1,113 @@ +"""DSL-115 author-selectable specificity over owned concern surfaces.""" + +from __future__ import annotations + +import textwrap + +from aces_contracts.contracts import ExperimentReferenceModel +from aces_sdl import parse_sdl +from aces_sdl.explicitness import ExplicitnessClass, classify_authoring_specificity + + +def _scenario_with_specificity_levels(): + return parse_sdl( + textwrap.dedent(""" + name: dsl-115-specificity + version: ${scenario_version} + variables: + scenario_version: + type: string + default: 1.0.0 + allowed_values: [1.0.0, 1.1.0] + participant_label: + type: string + default: red operator + allowed_values: [red operator, autonomous red team] + entities: + red: + role: red + agents: + red-agent: + entity: red + description: ${participant_label} + propositions: + objective-complete: + description: The red participant has completed the governed assessment activity. + subjects: [entities.red] + basis: declared_state + predicate: + kind: boolean + property: assessment-complete + semantic_ref: urn:aces:declared-property:assessment-complete + operator: equals + expected: true + assertions: + objective-complete: + description: Assessment completion is required at the objective boundary. + proposition: objective-complete + role: postcondition + polarity: positive + objectives: + assess: + agent: red-agent + success: + assertions: [objective-complete] + """) + ) + + +def test_specificity_does_not_treat_missing_fields_as_open_by_default(): + result = classify_authoring_specificity(_scenario_with_specificity_levels()) + + assert "agents.red-agent.initial_knowledge" not in result.records + assert "objectives.assess.window" not in result.records + + +def test_specificity_classifies_scenario_participant_and_evaluation_concerns(): + result = classify_authoring_specificity( + _scenario_with_specificity_levels(), + admitted_open_paths=( + "version", + "objectives.assess.success.assertions[0]", + "agents.red-agent.initial_knowledge", + "objectives.assess.window", + ), + ) + + assert result.records["version"].classification is ExplicitnessClass.CONSTRAINED + assert result.records["agents.red-agent.description"].classification is ExplicitnessClass.CONSTRAINED + assert result.records["objectives.assess.success.assertions[0]"].classification is ExplicitnessClass.EXACT + assert result.records["agents.red-agent.initial_knowledge"].classification is ExplicitnessClass.OPEN + assert result.records["objectives.assess.window"].classification is ExplicitnessClass.OPEN + + +def test_specificity_rejects_admitted_open_paths_outside_model_surface(): + result = classify_authoring_specificity( + _scenario_with_specificity_levels(), + admitted_open_paths=( + "agents.red-agent.initialKnowlege", + "agents.ghost.initial_knowledge", + "objectives.assess.window", + ), + ) + + assert "agents.red-agent.initialKnowlege" not in result.records + assert "agents.ghost.initial_knowledge" not in result.records + assert result.records["objectives.assess.window"].classification is ExplicitnessClass.OPEN + assert result.errors == ( + "Cannot classify open specificity for 'agents.red-agent.initialKnowlege': " + "path does not resolve to a model surface", + "Cannot classify open specificity for 'agents.ghost.initial_knowledge': " + "path does not resolve to a model surface", + ) + + +def test_specificity_classifies_experiment_contract_concerns_without_new_sdl_syntax(): + reference = ExperimentReferenceModel(ref_kind="scenario", ref_id="benchmark-a") + + result = classify_authoring_specificity(reference, admitted_open_paths=("ref_version",)) + + assert result.records["ref_kind"].classification is ExplicitnessClass.EXACT + assert result.records["ref_id"].classification is ExplicitnessClass.EXACT + assert result.records["ref_version"].classification is ExplicitnessClass.OPEN + assert "ref_digest" not in result.records 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 223d056c1..43603637b 100644 --- a/implementations/python/tests/test_dsl_123_scenario_native_observability.py +++ b/implementations/python/tests/test_dsl_123_scenario_native_observability.py @@ -43,7 +43,52 @@ def _scenario(*, objective_target: str = OBSERVABILITY_REF, interaction_target: } }, entities={"blue": {"role": "blue"}}, - conditions={"observability-ready": {"command": "/bin/true", "interval": 15}}, + conditions={ + "observability-ready": { + "command": "/bin/true", + "interval": 15, + "proposition": "observability-inspected", + } + }, + evidence_requirements={ + "inspection-record": { + "description": "Record whether the participant inspected the governed observability endpoint.", + "source_refs": [OBSERVABILITY_REF], + "scope_refs": ["nodes.siem"], + "trigger_ref": "conditions.observability-ready", + "channel": "log", + "artifact_role": "proposition_truth_evidence", + "media_types": ["application/json"], + "sensitivity": "plain", + "redaction": "none", + "integrity": "checksum", + "retention": "study_lifetime", + "loss_disclosure": "required", + } + }, + propositions={ + "observability-inspected": { + "description": "The participant inspected the governed observability endpoint.", + "subjects": [OBSERVABILITY_REF], + "basis": "observed_state", + "predicate": { + "kind": "boolean", + "property": "participant-inspected", + "semantic_ref": "urn:aces:observable:participant-inspected", + "operator": "equals", + "expected": True, + }, + "evidence_requirements": ["inspection-record"], + } + }, + assertions={ + "observability-inspected": { + "description": "Inspection must be observed at the objective boundary.", + "proposition": "observability-inspected", + "role": "postcondition", + "polarity": "positive", + } + }, relationships={ "dashboard-depends-on-listener": { "type": "depends_on", @@ -91,7 +136,7 @@ def _scenario(*, objective_target: str = OBSERVABILITY_REF, interaction_target: "entity": "blue", "actions": ["inspect-dashboard"], "targets": [objective_target], - "success": {"conditions": ["observability-ready"]}, + "success": {"assertions": ["observability-inspected"]}, } }, ) 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 e5c26251b..816a4d14e 100644 --- a/implementations/python/tests/test_dsl_124_authored_evidence_requirements.py +++ b/implementations/python/tests/test_dsl_124_authored_evidence_requirements.py @@ -88,8 +88,28 @@ def test_dsl_124_evidence_requirements_are_not_objective_targets() -> None: targets: - evidence_requirements.network-trace success: - conditions: - - capture-open + assertions: + - trace-captured + propositions: + trace-captured: + description: The governed network trace was captured. + subjects: + - nodes.siem + basis: observed_state + predicate: + kind: boolean + property: network-trace-captured + semantic_ref: urn:aces:observable:network-trace-captured + operator: equals + expected: true + evidence_requirements: + - network-trace + assertions: + trace-captured: + description: Trace capture must be observed at the objective boundary. + proposition: trace-captured + role: postcondition + polarity: positive """ ) diff --git a/implementations/python/tests/test_example_library_policy.py b/implementations/python/tests/test_example_library_policy.py index c60d59a52..759d264a8 100644 --- a/implementations/python/tests/test_example_library_policy.py +++ b/implementations/python/tests/test_example_library_policy.py @@ -28,17 +28,57 @@ }, "conditions": { "app-healthy": { + "proposition": "app-healthy", "command": "curl -fsS https://app/health", "interval": 30, } }, + "evidence_requirements": { + "app-health-evidence": { + "description": "Capture the governed application health observation.", + "source_refs": ["nodes.app.services.https"], + "scope_refs": ["nodes.app"], + "trigger_ref": "conditions.app-healthy", + "channel": "api_response", + "artifact_role": "proposition_truth_evidence", + "media_types": ["application/json"], + "sensitivity": "plain", + "redaction": "redact_secrets", + "integrity": "checksum", + "retention": "study_lifetime", + "loss_disclosure": "required", + } + }, + "propositions": { + "app-healthy": { + "description": "The governed application service responds successfully.", + "subjects": ["nodes.app.services.https"], + "basis": "observed_state", + "predicate": { + "kind": "boolean", + "property": "service-healthy", + "semantic_ref": "urn:aces:observable:service-healthy", + "operator": "equals", + "expected": True, + }, + "evidence_requirements": ["app-health-evidence"], + } + }, + "assertions": { + "app-healthy": { + "description": "Application health must hold at the objective boundary.", + "proposition": "app-healthy", + "role": "postcondition", + "polarity": "positive", + } + }, "entities": {"blue-team": {"role": "blue"}}, "agents": {"operator": {"entity": "blue-team"}}, "objectives": { "verify-app-health": { "agent": "operator", "targets": ["nodes.app.services.https"], - "success": {"conditions": ["app-healthy"]}, + "success": {"assertions": ["app-healthy"]}, } }, "workflows": { @@ -48,7 +88,7 @@ "verify": { "type": "objective", "objective": "verify-app-health", - "on-success": "done", + "on_success": "done", }, "done": {"type": "end"}, }, diff --git a/implementations/python/tests/test_example_schema_conformance.py b/implementations/python/tests/test_example_schema_conformance.py index eb6e87c4e..ea1794e99 100644 --- a/implementations/python/tests/test_example_schema_conformance.py +++ b/implementations/python/tests/test_example_schema_conformance.py @@ -24,19 +24,32 @@ from dataclasses import dataclass, field from functools import cache from pathlib import Path +from typing import Any, Protocol import pytest from aces_contracts.corpus import corpus_family_root -from aces_sdl.scenario import Scenario +from aces_contracts.experiment_spec import load_experiment_spec +from aces_sdl import load_sdl_fragment from aces_sdl.scenarios import load_scenario from jsonschema import Draft202012Validator -from paths import EXAMPLES_DIR +from paths import EXAMPLES_DIR, EXPERIMENTS_DIR -# ``by_alias=True`` is load-bearing: the published schema is generated from the model with -# ``model_json_schema()`` (aliases on), so a field-name dump fails on YAML-facing aliases -# such as ``class``, ``on-success``, and ``max-attempts``. ``mode="json"`` yields JSON-native -# scalars (enum values, not enum members). These are the same flags the runtime compiler -# uses for its contract-shaped serialization. + +class SupportsModelDump(Protocol): + """Any loaded contract model that can be serialized for publication comparison. + + The corpus spans more than one loaded model type (``Scenario`` for SDL, + ``ExperimentSpecModel`` for authored experiments), so the loader is typed by + the one capability the suite uses — ``model_dump`` — rather than a single + concrete model class. + """ + + def model_dump(self, **kwargs: Any) -> dict: ... + + +# ``by_alias=True`` is load-bearing for intentional language keywords such as +# ``class``, ``type``, ``then``, and ``else``. Ordinary structural wire fields +# use canonical snake_case. ``mode="json"`` yields JSON-native enum values. _PUBLICATION_DUMP_KWARGS = {"mode": "json", "by_alias": True} @@ -52,7 +65,7 @@ class CorpusEntry: contract_id: str root: Path glob: str - loader: Callable[[Path], Scenario] + loader: Callable[[Path], SupportsModelDump] schema_path: Path dump_kwargs: dict = field(default_factory=lambda: dict(_PUBLICATION_DUMP_KWARGS)) @@ -71,6 +84,13 @@ def _validator_for(schema_path: Path) -> Draft202012Validator: _SDL_SCHEMA_DIR = corpus_family_root("schemas") / "sdl" +_EXP_SCHEMA_DIR = corpus_family_root("schemas") / "experiment-core" + +# The experiment authoring-input reference models (task / capture-spec) forbid +# ``ref_digest``/``ref_path`` in their published sub-schemas, so an id-only +# reference publishes without those keys. ``exclude_none`` yields exactly that +# schema-conformant publication shape (unset optionals are omitted, not null). +_EXPERIMENT_DUMP_KWARGS = {"mode": "json", "by_alias": True, "exclude_none": True} # Today the table has exactly one leg: the authoring example corpus against the published # authoring-input contract. No instantiated-scenario *example* artifacts exist under @@ -85,6 +105,14 @@ def _validator_for(schema_path: Path) -> Draft202012Validator: loader=load_scenario, schema_path=_SDL_SCHEMA_DIR / "sdl-authoring-input-v1.json", ), + CorpusEntry( + contract_id="experiment-authoring-input-v1", + root=EXPERIMENTS_DIR, + glob="*.exp.yaml", + loader=load_experiment_spec, + schema_path=_EXP_SCHEMA_DIR / "experiment-authoring-input-v1.json", + dump_kwargs=dict(_EXPERIMENT_DUMP_KWARGS), + ), ] @@ -118,6 +146,17 @@ def test_example_conforms_to_published_schema(entry: CorpusEntry, path: Path) -> ) +@pytest.mark.parametrize("path", sorted(EXAMPLES_DIR.glob("*.sdl.yaml")), ids=lambda path: path.name) +def test_sdl_example_is_canonical_source_and_direct_normalized_schema_input(path: Path) -> None: + """Canonical examples pass strict source decoding and the advertised schema directly.""" + payload = load_sdl_fragment(path.read_text(encoding="utf-8")) + errors = sorted(_AUTHORING_ENTRY.validator().iter_errors(payload), key=lambda error: error.json_path) + + assert not errors, f"{path.name} is not direct normalized authoring-schema input:\n" + "\n".join( + f" {error.json_path}: {error.message}" for error in errors + ) + + @pytest.mark.parametrize("entry", VALIDATION_CORPUS, ids=_case_id) def test_corpus_leg_is_nonempty(entry: CorpusEntry) -> None: """Non-vacuity guard: a stale/relocated corpus root must fail loudly, not collect zero cases.""" diff --git a/implementations/python/tests/test_experiment_authoring.py b/implementations/python/tests/test_experiment_authoring.py new file mode 100644 index 000000000..284a7afd7 --- /dev/null +++ b/implementations/python/tests/test_experiment_authoring.py @@ -0,0 +1,167 @@ +"""Tests for the experiment authoring-input loader and MCP authoring tools (issue #675).""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from aces_contracts.contracts import ExperimentSpecModel +from aces_contracts.experiment_spec import ( + ExperimentSpecValidationError, + find_experiment_specs, + load_experiment_spec, + parse_experiment_spec, +) +from aces_mcp.tools.experiment_authoring import ( + _MAX_INPUT_BYTES, + _run_experiment_get_example, + _run_experiment_scaffold, + _run_experiment_validate, +) +from paths import EXPERIMENTS_DIR, REPO_ROOT + +_VALID_FIXTURE = ( + REPO_ROOT + / "contracts" + / "fixtures" + / "experiment-core" + / "experiment-authoring-input-v1" + / "valid" + / "reference.json" +) + + +def _valid_payload() -> dict: + return json.loads(_VALID_FIXTURE.read_text(encoding="utf-8")) + + +def _valid_yaml() -> str: + # YAML is a JSON superset; the JSON fixture is valid YAML. + return _VALID_FIXTURE.read_text(encoding="utf-8") + + +# --- loader / parser ------------------------------------------------------ + + +def test_parse_experiment_spec_accepts_valid_yaml() -> None: + spec = parse_experiment_spec(_valid_yaml()) + assert spec.spec_id == "spec-techvault-red-tactic-sweep-v1" + assert spec.run_plan.allocation is not None + + +def test_parse_experiment_spec_rejects_empty() -> None: + with pytest.raises(ExperimentSpecValidationError): + parse_experiment_spec(" \n") + + +def test_parse_experiment_spec_rejects_non_mapping() -> None: + with pytest.raises(ExperimentSpecValidationError): + parse_experiment_spec("- a\n- b\n") + + +def test_parse_experiment_spec_rejects_bad_yaml() -> None: + with pytest.raises(ExperimentSpecValidationError): + parse_experiment_spec("spec_id: [unterminated\n") + + +def test_parse_experiment_spec_rejects_schema_invalid() -> None: + with pytest.raises(ExperimentSpecValidationError): + parse_experiment_spec("schema_version: experiment-authoring-input/v1\nspec_id: x\n") + + +def test_load_experiment_spec_reads_file(tmp_path: Path) -> None: + path = tmp_path / "demo.exp.yaml" + path.write_text(_valid_yaml(), encoding="utf-8") + spec = load_experiment_spec(path) + assert isinstance(spec, ExperimentSpecModel) + + +def test_load_experiment_spec_missing_file(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError): + load_experiment_spec(tmp_path / "nope.exp.yaml") + + +def test_find_experiment_specs_finds_shipped_examples() -> None: + found = find_experiment_specs(EXPERIMENTS_DIR) + assert found, "expected shipped *.exp.yaml examples" + assert all(p.name.endswith(".exp.yaml") for p in found) + + +def test_find_experiment_specs_missing_dir(tmp_path: Path) -> None: + assert find_experiment_specs(tmp_path / "absent") == [] + + +# --- model validators ----------------------------------------------------- + + +def test_run_plan_rejects_both_run_count_sources() -> None: + payload = _valid_payload() + payload["run_plan"]["target_run_count"] = 10 # allocation already present + with pytest.raises(ValueError, match="exactly one"): + ExperimentSpecModel.model_validate(payload) + + +def test_run_plan_rejects_no_run_count_source() -> None: + payload = _valid_payload() + del payload["run_plan"]["allocation"] + with pytest.raises(ValueError, match="exactly one"): + ExperimentSpecModel.model_validate(payload) + + +def test_run_plan_rejects_red_variant_key_mismatch() -> None: + payload = _valid_payload() + payload["run_plan"]["red_variant_selections"] = { + "aggressive": {"variant_id": "not-aggressive", "agent_ref": "red-agent"} + } + with pytest.raises(ValueError, match="variant_id"): + ExperimentSpecModel.model_validate(payload) + + +def test_target_run_count_path_is_valid() -> None: + payload = _valid_payload() + del payload["run_plan"]["allocation"] + del payload["factors"] + payload["run_plan"]["target_run_count"] = 5 + spec = ExperimentSpecModel.model_validate(payload) + assert spec.run_plan.target_run_count == 5 + + +# --- MCP tool helpers ----------------------------------------------------- + + +def test_tool_validate_valid() -> None: + out = _run_experiment_validate(_valid_yaml()) + assert out.startswith("VALID —") + assert "run-count source: allocation" in out + + +def test_tool_validate_invalid() -> None: + out = _run_experiment_validate("schema_version: experiment-authoring-input/v1\nspec_id: x\n") + assert out.startswith("VALIDATION ERROR") + + +def test_tool_validate_too_large() -> None: + out = _run_experiment_validate("x" * (_MAX_INPUT_BYTES + 1)) + assert "INPUT TOO LARGE" in out + + +@pytest.mark.parametrize("complexity", ["minimal", "sweep"]) +def test_tool_scaffold_outputs_are_valid(complexity: str) -> None: + out = _run_experiment_scaffold(complexity, "demo-spec", "task-demo-v1") + spec = parse_experiment_spec(out) + assert spec.spec_id == "demo-spec" + + +def test_tool_scaffold_rejects_unknown_complexity() -> None: + assert "Invalid complexity" in _run_experiment_scaffold("ultra", "x", "y") + + +@pytest.mark.parametrize("name", ["sweep", "smoke"]) +def test_tool_get_example_returns_shipped(name: str) -> None: + out = _run_experiment_get_example(name) + assert "schema_version: experiment-authoring-input/v1" in out + + +def test_tool_get_example_unknown() -> None: + assert "Unknown example" in _run_experiment_get_example("nope") diff --git a/implementations/python/tests/test_fm2_semantics.py b/implementations/python/tests/test_fm2_semantics.py index a876cb3ff..4d04adf29 100644 --- a/implementations/python/tests/test_fm2_semantics.py +++ b/implementations/python/tests/test_fm2_semantics.py @@ -10,7 +10,7 @@ from aces.core.runtime.compiler import compile_runtime_model from aces.core.runtime.models import RuntimeDomain, RuntimeSnapshot, SnapshotEntry from aces.core.runtime.planner import plan -from aces.core.sdl import SDLValidationError, parse_sdl +from aces.core.sdl import SDLInstantiationError, SDLValidationError, parse_sdl def _scenario(yaml_str: str): @@ -52,20 +52,29 @@ def test_validator_and_compiler_agree_on_window_errors(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} entities: blue: {role: blue} events: - kickoff: {conditions: [health]} - cleanup: {conditions: [health]} + kickoff: {assertions: [pre-health]} + cleanup: {assertions: [pre-health]} scripts: - timeline: {start-time: 0, end-time: 60, speed: 1, events: {kickoff: 10}} - side: {start-time: 0, end-time: 60, speed: 1, events: {cleanup: 20}} + timeline: {start_time: 0, end_time: 60, speed: 1, events: {kickoff: 10}} + side: {start_time: 0, end_time: 60, speed: 1, events: {cleanup: 20}} stories: main: {scripts: [timeline]} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} window: stories: [main] scripts: [side] @@ -91,11 +100,9 @@ def test_validator_and_compiler_agree_on_window_errors(self): assert any("window event 'kickoff' is not included by the referenced scripts" in error for error in errors) assert any("window step 'other.finish' is not part of the referenced workflows" in error for error in errors) - model = compile_runtime_model(parse_sdl(raw, skip_semantic_validation=True)) - codes = {diag.code for diag in model.diagnostics} - assert "evaluation.script-ref-outside-window-stories" in codes - assert "evaluation.event-ref-outside-window-scripts" in codes - assert "evaluation.workflow-step-ref-workflow-outside-window" in codes + with pytest.raises(SDLInstantiationError) as compiler_exc: + compile_runtime_model(parse_sdl(raw, skip_semantic_validation=True)) + assert compiler_exc.value.errors == errors def test_compiler_and_planner_agree_on_window_refresh_semantics(self): raw = _scenario(""" @@ -109,12 +116,21 @@ def test_compiler_and_planner_agree_on_window_refresh_semantics(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} entities: blue: {role: blue} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} window: workflows: [flow] steps: [flow.branch] @@ -124,7 +140,7 @@ def test_compiler_and_planner_agree_on_window_refresh_semantics(self): steps: branch: type: decision - when: {conditions: [health]} + when: {assertions: [pre-health]} then: finish else: finish finish: {type: end} @@ -137,7 +153,10 @@ def test_compiler_and_planner_agree_on_window_refresh_semantics(self): mutated = compile_runtime_model( parse_sdl( - raw.replace("/bin/true", "/bin/false"), + raw.replace("/bin/true", "/bin/false").replace( + "urn:aces:declared-property:runtime", + "urn:aces:declared-property:runtime-v2", + ), skip_semantic_validation=False, ) ) @@ -164,7 +183,7 @@ def test_validator_and_compiler_agree_on_objective_reference_errors(self): objectives: base: entity: blue - success: {conditions: [missing-condition]} + success: {assertions: [missing-assertion]} depends_on: [missing-objective] """) @@ -173,15 +192,14 @@ def test_validator_and_compiler_agree_on_objective_reference_errors(self): errors = exc_info.value.errors assert any( - "Objective 'base' references undefined condition 'missing-condition' in success criteria" in e + "Objective 'base' references undefined assertion 'missing-assertion' in success criteria" in e for e in errors ) assert any("Objective 'base' depends on undefined objective 'missing-objective'" in e for e in errors) - model = compile_runtime_model(parse_sdl(raw, skip_semantic_validation=True)) - codes = {diag.code for diag in model.diagnostics} - assert "evaluation.condition-ref-unbound" in codes - assert "evaluation.objective-ref-unbound" in codes + with pytest.raises(SDLInstantiationError) as compiler_exc: + compile_runtime_model(parse_sdl(raw, skip_semantic_validation=True)) + assert compiler_exc.value.errors == errors def test_compiler_and_planner_agree_on_objective_dependency_ordering_and_refresh(self): raw = _scenario(""" @@ -196,15 +214,29 @@ def test_compiler_and_planner_agree_on_objective_dependency_ordering_and_refresh conditions: ready: {command: /bin/true, interval: 15} gate: {command: /bin/echo, interval: 15} +propositions: + ready: + 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} + 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} +assertions: + ready: {proposition: ready, role: postcondition, polarity: positive} + gate: {proposition: gate, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: base: entity: blue - success: {conditions: [ready]} + success: {assertions: [ready]} dependent: entity: blue - success: {conditions: [gate]} + success: {assertions: [gate]} depends_on: [base] """) compiled = compile_runtime_model(parse_sdl(raw)) @@ -215,7 +247,14 @@ def test_compiler_and_planner_agree_on_objective_dependency_ordering_and_refresh baseline = plan(compiled, create_stub_manifest()) snapshot = _snapshot_from_plan(baseline) - mutated = compile_runtime_model(parse_sdl(raw.replace("/bin/true", "/bin/false"))) + mutated = compile_runtime_model( + parse_sdl( + raw.replace("/bin/true", "/bin/false").replace( + "urn:aces:declared-property:ready", + "urn:aces:declared-property:ready-v2", + ) + ) + ) updated = plan(mutated, create_stub_manifest(), snapshot=snapshot) actions = {op.address: op.action.value for op in updated.evaluation.operations} diff --git a/implementations/python/tests/test_instantiated_scenario_schema.py b/implementations/python/tests/test_instantiated_scenario_schema.py index b27f2e369..6742c53f4 100644 --- a/implementations/python/tests/test_instantiated_scenario_schema.py +++ b/implementations/python/tests/test_instantiated_scenario_schema.py @@ -1,12 +1,10 @@ -"""Issue #500 — instantiated-scenario contract differentiated from authoring-input. +"""Issues #500/#724: concrete tokens and closed instantiated phase shape. `sdl-authoring-input-v1` and `instantiated-scenario-v1` were byte-identical -apart from `$id`/title/description: the authoring-vs-instantiated distinction -lived only in `InstantiatedScenario` private attributes that never reached the -JSON Schema. These tests pin the differentiation: the instantiated contract -rejects unresolved ``${var}`` tokens (embedded and full-string) at both the -Pydantic model boundary and the published JSON Schema boundary, while the -authoring contract still accepts them. +apart from metadata, and the authoring-vs-instantiated distinction relied on +private Python state. These tests pin both repairs: the instantiated contract +rejects unresolved ``${var}`` tokens and all authoring machinery, requires +portable provenance, and has a separately profiled snapshot contract. """ from __future__ import annotations @@ -24,8 +22,20 @@ REPO_ROOT = Path(__file__).resolve().parents[3] SDL_SCHEMA_DIR = REPO_ROOT / "contracts" / "schemas" / "sdl" FIXTURE_DIR = REPO_ROOT / "contracts" / "fixtures" / "sdl" / "instantiated-scenario-v1" - -_CONCRETE = {"name": "concrete-scenario", "description": "a fully concrete scenario"} +SNAPSHOT_FIXTURE_DIR = REPO_ROOT / "contracts" / "fixtures" / "sdl" / "instantiated-scenario-snapshot-v1" + +_PROVENANCE = { + "authored_digest": { + "profile": "aces-sdl-semantic/v1", + "algorithm": "sha256", + "value": "sha256:" + "a" * 64, + } +} +_CONCRETE = { + "name": "concrete-scenario", + "description": "a fully concrete scenario", + "instantiation_provenance": _PROVENANCE, +} _EMBEDDED_VAR = {"name": "concrete-scenario", "description": "deploy ${region} cluster"} _FULL_VAR = {"name": "concrete-scenario", "description": "${environment}"} _COUNT_VAR = {"name": "concrete-scenario", "infrastructure": {"net": {"count": "${replicas}"}}} @@ -77,7 +87,16 @@ def test_authoring_model_rejects_invalid_variable_name() -> None: @pytest.mark.parametrize("payload", _VAR_PAYLOADS) def test_instantiated_model_rejects_unresolved_variables(payload: dict) -> None: with pytest.raises(ValidationError): - InstantiatedScenario.model_validate(payload) + InstantiatedScenario.model_validate({**payload, "instantiation_provenance": _PROVENANCE}) + + +@pytest.mark.parametrize( + ("field", "value"), + (("variables", {}), ("imports", []), ("module", None)), +) +def test_instantiated_model_rejects_authoring_fields_even_when_empty(field: str, value: object) -> None: + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + InstantiatedScenario.model_validate({**_CONCRETE, field: value}) # --- JSON Schema boundary (live bundle) ----------------------------------- @@ -96,14 +115,32 @@ def test_bundle_instantiated_schema_constraints_differ_from_authoring() -> None: def test_bundle_instantiated_schema_rejects_unresolved_variables(payload: dict) -> None: """Acceptance (b): a ``${var}`` payload fails instantiated-schema validation.""" bundle = schema_bundle() - assert not Draft202012Validator(bundle["instantiated-scenario-v1"]).is_valid(payload) + instantiated_payload = {**payload, "instantiation_provenance": _PROVENANCE} + assert not Draft202012Validator(bundle["instantiated-scenario-v1"]).is_valid(instantiated_payload) # The same payload is valid against the authoring contract. assert Draft202012Validator(bundle["sdl-authoring-input-v1"]).is_valid(payload) def test_bundle_instantiated_schema_accepts_concrete_scenario() -> None: bundle = schema_bundle() - Draft202012Validator(bundle["instantiated-scenario-v1"]).validate(_CONCRETE) + assert Draft202012Validator(bundle["instantiated-scenario-v1"]).is_valid(_CONCRETE) + + +@pytest.mark.parametrize( + "payload", + ( + {"name": "concrete-scenario"}, + { + "name": "incomplete-digest", + "instantiation_provenance": {"authored_digest": {"value": "sha256:" + "a" * 64}}, + }, + {**_CONCRETE, "variables": {}}, + {**_CONCRETE, "imports": []}, + {**_CONCRETE, "module": None}, + ), +) +def test_bundle_instantiated_schema_enforces_closed_phase_shape(payload: dict) -> None: + assert not Draft202012Validator(schema_bundle()["instantiated-scenario-v1"]).is_valid(payload) def test_bundle_authoring_schema_rejects_invalid_variable_name() -> None: @@ -125,7 +162,7 @@ def test_published_schemas_differ_in_constraints() -> None: def test_published_valid_fixture_passes() -> None: schema = _load(SDL_SCHEMA_DIR / "instantiated-scenario-v1.json") fixture = _load(FIXTURE_DIR / "valid" / "minimal.json") - Draft202012Validator(schema).validate(fixture) + assert Draft202012Validator(schema).is_valid(fixture) def test_published_authoring_schema_rejects_invalid_variable_name() -> None: @@ -138,7 +175,27 @@ def test_published_invalid_fixture_fails() -> None: schema = _load(SDL_SCHEMA_DIR / "instantiated-scenario-v1.json") fixture = _load(FIXTURE_DIR / "invalid" / "unresolved-variable.json") assert not Draft202012Validator(schema).is_valid(fixture) - # The invalid fixture is otherwise well-formed: it only fails because of the - # unresolved ${var}, so it still validates against the authoring contract. + concrete_control = {**fixture, "description": "deploy concrete cluster"} + Draft202012Validator(schema).validate(concrete_control) + # Removing provenance yields the corresponding normalized authoring shape; + # that shape remains valid because unresolved tokens are authoring syntax. authoring = _load(SDL_SCHEMA_DIR / "sdl-authoring-input-v1.json") - Draft202012Validator(authoring).validate(fixture) + authored_fixture = {key: value for key, value in fixture.items() if key != "instantiation_provenance"} + Draft202012Validator(authoring).validate(authored_fixture) + + +@pytest.mark.parametrize( + "filename", + ("missing-provenance.json", "variables-empty.json", "imports-empty.json", "module-null.json"), +) +def test_published_phase_invalid_fixtures_fail(filename: str) -> None: + schema = _load(SDL_SCHEMA_DIR / "instantiated-scenario-v1.json") + fixture = _load(FIXTURE_DIR / "invalid" / filename) + assert not Draft202012Validator(schema).is_valid(fixture) + + +def test_published_snapshot_fixtures_pin_the_profile_boundary() -> None: + schema = _load(SDL_SCHEMA_DIR / "instantiated-scenario-snapshot-v1.json") + Draft202012Validator(schema).validate(_load(SNAPSHOT_FIXTURE_DIR / "valid" / "minimal.json")) + assert not Draft202012Validator(schema).is_valid({"scenario": _CONCRETE}) + assert not Draft202012Validator(schema).is_valid(_load(SNAPSHOT_FIXTURE_DIR / "invalid" / "wrong-profile.json")) diff --git a/implementations/python/tests/test_language_service.py b/implementations/python/tests/test_language_service.py index 905d5ff29..6ea712f2e 100644 --- a/implementations/python/tests/test_language_service.py +++ b/implementations/python/tests/test_language_service.py @@ -83,6 +83,100 @@ def test_language_completions_cover_contexts_and_filters() -> None: assert workflow_refs["items"][0]["detail"] == "workflows.flow.steps.start-here" +def test_targetable_completions_exclude_non_targetable_sections() -> None: + sdl = """\ +name: targetable-completions +nodes: + web: {type: VM, os: linux, resources: {ram: 1 GiB, cpu: 1}} +variables: + count: {type: integer, default: 1} +evidence_requirements: + capture: {source_refs: [web], source_class: node} +objectives: + inspect: {targets: [web], success: {conditions: []}} +workflows: + flow: {start: done, steps: {done: {type: end}}} +""" + + result = language_completions(sdl, cursor_path="/objectives/inspect/targets") + + assert result["context"] == "reference:targetable" + details = {item["detail"] for item in result["items"]} + assert "nodes.web" in details + assert not details & { + "variables.count", + "evidence_requirements.capture", + "objectives.inspect", + "workflows.flow", + } + + +def test_valid_document_completions_use_typed_nested_declarations() -> None: + sdl = """\ +name: nested-completions +nodes: + web: + type: vm + resources: {ram: 1 GiB, cpu: 1} + services: [{name: http, port: 80}] + api: + type: vm + resources: {ram: 1 GiB, cpu: 1} + services: [{name: http, port: 8080}] +content: + fixtures: + type: dataset + target: web + items: [{name: seed-file, display_name: seed.json}] +relationships: + serves: {type: connects_to, source: web, target: api} +""" + + result = language_completions(sdl, cursor_path="/relationships/serves/target") + by_detail = {item["detail"]: item for item in result["items"]} + + assert result["context"] == "reference:targetable" + assert by_detail["content.fixtures.items.seed-file"]["label"] == "seed-file" + assert by_detail["nodes.web.services.http"]["label"] == "nodes.web.services.http" + assert by_detail["nodes.api.services.http"]["label"] == "nodes.api.services.http" + + +def test_qualified_targetable_reference_reports_occurrence() -> None: + sdl = """\ +name: targetable-reference +nodes: + web: {type: VM, os: linux, resources: {ram: 1 GiB, cpu: 1}} +behavior_specifications: + baseline: + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [] + authority_scope_refs: [nodes.web] + behavior_mode: baseline +""" + + result = language_references(sdl, "nodes.web") + + assert any( + item["path"] == "/behavior_specifications/baseline/authority_scope_refs/0" for item in result["occurrences"] + ) + + +def test_qualified_targetable_reference_excludes_non_targetable_occurrence() -> None: + sdl = """\ +name: targetable-reference +nodes: + web: {type: VM, os: linux, resources: {ram: 1 GiB, cpu: 1}} +objectives: + inspect: {targets: [objectives.inspect], success: {conditions: []}} +""" + + result = language_references(sdl, "objectives.inspect") + + assert result["definitions"][0]["qualified_name"] == "objectives.inspect" + assert not any(item["path"] == "/objectives/inspect/targets/0" for item in result["occurrences"]) + + def test_language_completions_report_parse_and_size_errors() -> None: parse_error = language_completions("name: [\n", cursor_path="/") assert parse_error["status"] == "invalid" @@ -157,9 +251,14 @@ def test_language_format_normalizes_field_keys() -> None: """ ) - assert payload["status"] == "formatted" + assert payload["status"] == "formatted_with_diagnostics" assert payload["content"].startswith("name: formatting-test\nnodes:\n") - assert "type: Switch" in payload["content"] + assert "type: switch" in payload["content"] + assert [item["code"] for item in payload["diagnostics"]] == [ + "sdl.noncanonical_field", + "sdl.noncanonical_field", + "sdl.noncanonical_field", + ] def test_language_format_reports_parse_error() -> None: diff --git a/implementations/python/tests/test_libvirt_backend_cli.py b/implementations/python/tests/test_libvirt_backend_cli.py index 400ea760e..89ccac861 100644 --- a/implementations/python/tests/test_libvirt_backend_cli.py +++ b/implementations/python/tests/test_libvirt_backend_cli.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from aces_cli.main import app +from aces_operations.libvirt_evidence_run import LibvirtEvidenceRunConfig from aces_operations.techvault_live import TechVaultLiveConfig from typer.testing import CliRunner @@ -14,7 +15,7 @@ class _Report: passed: bool = True def render(self) -> str: - return "live ok" + return "live ok" if self.passed else "live failed" def test_libvirt_techvault_validate_live_cli_invokes_gate(monkeypatch, tmp_path): @@ -44,10 +45,6 @@ def _validate(**kwargs): "--yes", "--connection-uri", "qemu:///session", - "--appliance-memory-mib", - "96", - "--boot-timeout-seconds", - "7", ], ) @@ -60,8 +57,178 @@ def _validate(**kwargs): "run_id": "cli-run", "config": TechVaultLiveConfig( connection_uri="qemu:///session", - appliance_memory_mib=96, - boot_timeout_seconds=7, ), } ] + + +def test_libvirt_techvault_validate_live_cli_returns_failure_exit(monkeypatch, tmp_path): + monkeypatch.setattr( + "aces_cli.libvirt.validate_techvault_live", + lambda **_kwargs: _Report(passed=False), + ) + scenario = tmp_path / "scenario.sdl.yaml" + scenario.write_text("name: cli\n", encoding="utf-8") + + result = CliRunner().invoke( + app, + [ + "libvirt", + "techvault", + "validate-live", + "--scenario", + str(scenario), + "--project-dir", + str(tmp_path), + "--run-id", + "cli-run", + "--yes", + ], + ) + + assert result.exit_code == 1 + assert "live failed" in result.output + + +def test_libvirt_techvault_cli_has_no_unbound_memory_override(monkeypatch, tmp_path): + calls: list[dict[str, object]] = [] + + def _validate(**kwargs): + calls.append(kwargs) + return _Report() + + monkeypatch.setattr("aces_cli.libvirt.validate_techvault_live", _validate) + scenario = tmp_path / "scenario.sdl.yaml" + scenario.write_text("name: cli\n", encoding="utf-8") + + result = CliRunner().invoke( + app, + [ + "libvirt", + "techvault", + "validate-live", + "--scenario", + str(scenario), + "--project-dir", + str(tmp_path), + "--run-id", + "cli-run", + "--yes", + "--appliance-memory-mib", + "96", + ], + ) + + assert result.exit_code == 2 + assert calls == [] + + +def test_libvirt_techvault_guest_certify_cli_invokes_evidence_run(monkeypatch, tmp_path): + calls: list[dict[str, object]] = [] + + def _run(**kwargs): + calls.append(kwargs) + return _Report() + + monkeypatch.setattr("aces_cli.libvirt.run_libvirt_evidence_run", _run) + scenario = tmp_path / "scenario.sdl.yaml" + scenario.write_text("name: cli\n", encoding="utf-8") + + result = CliRunner().invoke( + app, + [ + "libvirt", + "techvault", + "guest-certify", + "--scenario", + str(scenario), + "--project-dir", + str(tmp_path), + "--run-id", + "gc-cli", + "--yes", + "--connection-uri", + "qemu:///system", + ], + ) + + assert result.exit_code == 0, result.output + assert calls == [ + { + "scenario_path": scenario.resolve(), + "project_dir": tmp_path.resolve(), + "run_id": "gc-cli", + "config": LibvirtEvidenceRunConfig( + evidence_source_mode="guest-certified", + connection_uri="qemu:///system", + ), + } + ] + + +def test_libvirt_techvault_guest_certify_cli_returns_failure_exit(monkeypatch, tmp_path): + monkeypatch.setattr( + "aces_cli.libvirt.run_libvirt_evidence_run", + lambda **_kwargs: _Report(passed=False), + ) + scenario = tmp_path / "scenario.sdl.yaml" + scenario.write_text("name: cli\n", encoding="utf-8") + + result = CliRunner().invoke( + app, + ["libvirt", "techvault", "guest-certify", "--scenario", str(scenario), "--project-dir", str(tmp_path), "--yes"], + ) + + assert result.exit_code == 1 + + +def test_libvirt_techvault_guest_certify_cli_rejects_credentials(monkeypatch, tmp_path): + calls: list[dict[str, object]] = [] + monkeypatch.setattr("aces_cli.libvirt.run_libvirt_evidence_run", lambda **kwargs: calls.append(kwargs)) + scenario = tmp_path / "scenario.sdl.yaml" + scenario.write_text("name: cli\n", encoding="utf-8") + + result = CliRunner().invoke( + app, + [ + "libvirt", + "techvault", + "guest-certify", + "--scenario", + str(scenario), + "--yes", + "--connection-uri", + "qemu+ssh://operator:credential@example/system", + ], + ) + + assert result.exit_code == 2 + assert calls == [] + + +def test_libvirt_techvault_cli_rejects_connection_uri_credentials(monkeypatch, tmp_path): + calls: list[dict[str, object]] = [] + monkeypatch.setattr("aces_cli.libvirt.validate_techvault_live", lambda **kwargs: calls.append(kwargs)) + scenario = tmp_path / "scenario.sdl.yaml" + scenario.write_text("name: cli\n", encoding="utf-8") + + result = CliRunner().invoke( + app, + [ + "libvirt", + "techvault", + "validate-live", + "--scenario", + str(scenario), + "--project-dir", + str(tmp_path), + "--run-id", + "cli-run", + "--yes", + "--connection-uri", + "qemu+ssh://operator:credential@example/system", + ], + ) + + assert result.exit_code == 2 + assert calls == [] diff --git a/implementations/python/tests/test_libvirt_backend_driver.py b/implementations/python/tests/test_libvirt_backend_driver.py index 429d65429..2dba6d0bf 100644 --- a/implementations/python/tests/test_libvirt_backend_driver.py +++ b/implementations/python/tests/test_libvirt_backend_driver.py @@ -8,6 +8,7 @@ from aces_backend_libvirt.cloudinit import CloudInitSpec, CloudInitUser from aces_backend_libvirt.driver import DomainSpec, NetworkAcl, NetworkSpec from aces_backend_libvirt.drivers.libvirt import LibvirtDeploymentDriver, _aces_uuid +from aces_backend_protocols.naming import provider_resource_name # Real libvirt reports a missing object with these stable VIR_ERR_NO_* codes via # libvirtError.get_error_code(); VIR_ERR_OPERATION_INVALID is raised by destroy() @@ -21,6 +22,14 @@ _VIR_ERR_NO_NWFILTER = 620 +def _runtime_name(address: str) -> str: + return provider_resource_name(address, prefix="aces-test") + + +def _seed_dir(workspace: Path, address: str = "provision.node.web") -> Path: + return workspace / _runtime_name(address) + + class _FakeLibvirtError(Exception): """Stand-in for ``libvirt.libvirtError`` exposing ``get_error_code()``.""" @@ -145,11 +154,13 @@ def test_libvirt_driver_realize_defines_networks_and_domains_with_safe_names(): ) assert not result.diagnostics - assert "aces-test-lan" in connection.network_xml[0] - assert "aces-test-web-vm" in connection.domain_xml[0] + network_name = _runtime_name("provision.network.lan") + domain_name = _runtime_name("provision.node.web") + assert network_name in connection.network_xml[0] + assert domain_name in connection.domain_xml[0] assert "lan<>" not in connection.network_xml[0] assert "web/vm" not in connection.domain_xml[0] - assert 'source network="aces-test-lan"' in connection.domain_xml[0] + assert f'source network="{network_name}"' in connection.domain_xml[0] assert driver.realized_addresses() == frozenset({"provision.network.lan", "provision.node.web"}) @@ -212,7 +223,7 @@ def test_libvirt_driver_realizes_cloud_init_seed_as_readonly_cdrom(tmp_path): ) assert not result.diagnostics - seed_dir = tmp_path / "aces-test-web" + seed_dir = _seed_dir(tmp_path) assert (seed_dir / "user-data").read_text().startswith("#cloud-config") assert (seed_dir / "meta-data").exists() assert seed_builder.seed_dirs == [seed_dir] @@ -246,7 +257,7 @@ def test_libvirt_seed_artifacts_use_private_modes(tmp_path): _realize_seed_domain(driver) - seed_dir = tmp_path / "aces-test-web" + seed_dir = _seed_dir(tmp_path) # Directories are traversable (so the QEMU process can reach the attached ISO) # but not listable; the rendered source files stay 0o600-private. assert seed_dir.stat().st_mode & 0o777 == 0o711 @@ -259,7 +270,7 @@ def test_libvirt_seed_write_neutralizes_pre_positioned_symlink(tmp_path): # A pre-positioned symlink where user-data would be written must never be # followed: the stale entry is cleared (target untouched) and the seed is # created fresh inside an owner-private directory, so re-apply still succeeds. - seed_dir = tmp_path / "aces-test-web" + seed_dir = _seed_dir(tmp_path) seed_dir.mkdir(parents=True) target = tmp_path / "victim" target.write_text("original") @@ -284,7 +295,7 @@ def test_libvirt_seed_write_neutralizes_pre_positioned_symlink(tmp_path): def test_libvirt_seed_write_clears_stale_seed_dir_for_clean_reapply(tmp_path): # A leftover seed directory from a crashed prior apply is replaced wholesale, # so exclusive (O_EXCL) creation of the new seed files never collides. - seed_dir = tmp_path / "aces-test-web" + seed_dir = _seed_dir(tmp_path) seed_dir.mkdir(parents=True) (seed_dir / "user-data").write_text("stale") (seed_dir / "leftover").write_text("debris") @@ -321,7 +332,7 @@ def test_libvirt_driver_destroy_cleans_up_seed_media(tmp_path): ), ), ) - seed_dir = tmp_path / "aces-test-web" + seed_dir = _seed_dir(tmp_path) assert seed_dir.exists() driver.destroy(networks=(), domains=("provision.node.web",)) @@ -338,8 +349,8 @@ def test_libvirt_driver_converges_existing_objects_without_duplicating(tmp_path) ) first = driver.realize(**specs) - prior_domain = connection.domains["aces-test-web"] - prior_network = connection.networks["aces-test-lan"] + prior_domain = connection.domains[_runtime_name("provision.node.web")] + prior_network = connection.networks[_runtime_name("provision.network.lan")] second = driver.realize(**specs) assert not first.diagnostics and not second.diagnostics @@ -360,7 +371,7 @@ def test_libvirt_convergence_refuses_to_replace_a_foreign_object(tmp_path): # closed with an ownership-conflict diagnostic and the foreign object survives. connection = _FakeConnection() foreign = _NativeObject(uuid="11111111-2222-3333-4444-555555555555") - connection.domains["aces-test-web"] = foreign + connection.domains[_runtime_name("provision.node.web")] = foreign driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( @@ -370,7 +381,7 @@ def test_libvirt_convergence_refuses_to_replace_a_foreign_object(tmp_path): assert [d.code for d in result.diagnostics] == ["libvirt-backend.driver.ownership-conflict"] assert not foreign.destroyed and not foreign.undefined - assert connection.domains["aces-test-web"] is foreign # never replaced + assert connection.domains[_runtime_name("provision.node.web")] is foreign # never replaced assert connection.domain_xml == [] # nothing redefined @@ -381,7 +392,7 @@ def test_libvirt_convergence_fails_closed_when_stopping_owned_object_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) - connection.domains["aces-test-web"] = existing + connection.domains[_runtime_name("provision.node.web")] = existing driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( @@ -398,7 +409,7 @@ def test_libvirt_convergence_tolerates_stopping_an_inactive_owned_object(): # 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) - connection.domains["aces-test-web"] = existing + connection.domains[_runtime_name("provision.node.web")] = existing driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( @@ -536,7 +547,8 @@ def test_libvirt_nwfilter_define_refuses_to_overwrite_a_foreign_filter(): # left untouched (no redefine), so other domains' filtering is not weakened. connection = _FakeConnection() foreign = _NativeObject(uuid="99999999-8888-7777-6666-555555555555") - connection.nwfilters["aces-test-web-acl"] = foreign + filter_name = f"{_runtime_name('provision.node.web')}-acl" + connection.nwfilters[filter_name] = foreign driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") acl = NetworkAcl(name="allow-all", action="accept", direction="inout", protocol="all") @@ -547,7 +559,7 @@ def test_libvirt_nwfilter_define_refuses_to_overwrite_a_foreign_filter(): assert [d.code for d in result.diagnostics] == ["libvirt-backend.driver.ownership-conflict"] assert connection.nwfilter_xml == [] # the foreign filter was never redefined - assert connection.nwfilters["aces-test-web-acl"] is foreign + assert connection.nwfilters[filter_name] is foreign def test_libvirt_destroy_refuses_to_remove_a_foreign_object(): @@ -555,14 +567,14 @@ def test_libvirt_destroy_refuses_to_remove_a_foreign_object(): # it: the ownership guard applies to deletion, not just convergence. connection = _FakeConnection() foreign = _NativeObject(uuid="11111111-2222-3333-4444-555555555555") - connection.domains["aces-test-web"] = foreign + connection.domains[_runtime_name("provision.node.web")] = foreign driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") result = driver.destroy(networks=(), domains=("provision.node.web",)) assert [d.code for d in result.diagnostics] == ["libvirt-backend.driver.ownership-conflict"] assert not foreign.destroyed and not foreign.undefined - assert connection.domains["aces-test-web"] is foreign + assert connection.domains[_runtime_name("provision.node.web")] is foreign def test_libvirt_driver_destroy_uses_previously_realized_names(): @@ -576,10 +588,12 @@ def test_libvirt_driver_destroy_uses_previously_realized_names(): result = driver.destroy(networks=("provision.network.lan",), domains=("provision.node.web",)) assert not result.diagnostics - assert connection.networks["aces-test-lan"].destroyed is True - assert connection.networks["aces-test-lan"].undefined is True - assert connection.domains["aces-test-web"].destroyed is True - assert connection.domains["aces-test-web"].undefined is True + network = connection.networks[_runtime_name("provision.network.lan")] + domain = connection.domains[_runtime_name("provision.node.web")] + assert network.destroyed is True + assert network.undefined is True + assert domain.destroyed is True + assert domain.undefined is True assert driver.realized_addresses() == frozenset() @@ -677,10 +691,10 @@ def test_libvirt_driver_realize_rolls_back_partially_defined_domain_on_create_fa assert [d.code for d in result.diagnostics] == ["libvirt-backend.driver.operation-failed"] # The just-defined domain was undefined (rolled back), not left orphaned. - defined = connection.domains["aces-test-web"] + defined = connection.domains[_runtime_name("provision.node.web")] assert defined.undefined is True # Its private seed media was cleaned up too. - assert not (tmp_path / "aces-test-web").exists() + assert not _seed_dir(tmp_path).exists() assert driver.realized_addresses() == frozenset() @@ -692,9 +706,9 @@ def test_libvirt_realize_rollback_leaves_a_pre_existing_updated_object_intact(): # preserved baseline snapshot that still claims it realized stays truthful. connection = _FakeConnection() existing = _NativeObject(uuid=_aces_uuid("provision.node.web")) - connection.domains["aces-test-web"] = existing + connection.domains[_runtime_name("provision.node.web")] = existing foreign = _NativeObject(uuid="11111111-2222-3333-4444-555555555555") - connection.domains["aces-test-other"] = foreign + connection.domains[_runtime_name("provision.node.other")] = foreign driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test", seed_builder=_FakeSeedBuilder()) result = driver.realize( @@ -709,7 +723,7 @@ def test_libvirt_realize_rollback_leaves_a_pre_existing_updated_object_intact(): assert [d.code for d in result.diagnostics] == ["libvirt-backend.driver.ownership-conflict"] # The updated domain's fresh definition is NOT rolled back (its snapshot entry # remains truthful); the foreign object is never touched. - updated = connection.domains["aces-test-web"] + updated = connection.domains[_runtime_name("provision.node.web")] assert updated.created is True assert updated.undefined is False assert not foreign.destroyed and not foreign.undefined @@ -723,13 +737,14 @@ def test_libvirt_driver_teardown_fails_closed_when_stop_fails_for_a_running_obje connection = _FakeConnection() driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") driver.realize(networks=(), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),)) - connection.domains["aces-test-web"].fail_destroy_code = _VIR_ERR_INTERNAL_ERROR + runtime_name = _runtime_name("provision.node.web") + connection.domains[runtime_name].fail_destroy_code = _VIR_ERR_INTERNAL_ERROR result = driver.destroy(networks=(), domains=("provision.node.web",)) assert [d.code for d in result.diagnostics] == ["libvirt-backend.driver.operation-failed"] assert [handle.realized for handle in result.domains] == [True] - assert connection.domains["aces-test-web"].undefined is False # never undefined a still-running domain + assert connection.domains[runtime_name].undefined is False # never undefined a still-running domain def test_libvirt_driver_teardown_undefines_an_already_inactive_object(): @@ -739,10 +754,11 @@ def test_libvirt_driver_teardown_undefines_an_already_inactive_object(): connection = _FakeConnection() driver = LibvirtDeploymentDriver(connection=connection, name_prefix="aces-test") driver.realize(networks=(), domains=(DomainSpec(address="provision.node.web", name="web", image_ref=None),)) - connection.domains["aces-test-web"].fail_destroy_code = _VIR_ERR_OPERATION_INVALID + runtime_name = _runtime_name("provision.node.web") + connection.domains[runtime_name].fail_destroy_code = _VIR_ERR_OPERATION_INVALID result = driver.destroy(networks=(), domains=("provision.node.web",)) assert not result.diagnostics assert [handle.realized for handle in result.domains] == [False] - assert connection.domains["aces-test-web"].undefined is True + assert connection.domains[runtime_name].undefined is True diff --git a/implementations/python/tests/test_libvirt_backend_envelopes.py b/implementations/python/tests/test_libvirt_backend_envelopes.py new file mode 100644 index 000000000..5e7ac6747 --- /dev/null +++ b/implementations/python/tests/test_libvirt_backend_envelopes.py @@ -0,0 +1,155 @@ +"""Configuration-bound libvirt realization-envelope selection (ASR-519).""" + +from __future__ import annotations + +from dataclasses import replace +from textwrap import dedent + +import pytest +from aces_backend_libvirt.manifest import create_libvirt_manifest +from aces_backend_libvirt.provisioner import LibvirtProvisioner +from aces_backend_libvirt.target import create_libvirt_components, create_libvirt_target +from aces_backend_libvirt.techvault_native import TechVaultNativeLibvirtDriver +from aces_backend_protocols.manifest import backend_manifest_payload +from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel, realization_envelope_digest +from aces_processor.reference import run_reference_processor +from aces_sdl.parser import parse_sdl +from libvirt_conformance_fixtures import RecordingLibvirtDriver + + +def test_default_libvirt_manifest_selects_generic_envelope(): + manifest = create_libvirt_manifest() + + assert manifest.realization_envelope is not None + assert manifest.realization_envelope.id == "libvirt-qemu.generic.v1" + assert manifest.realization_envelope.configuration.mode == "generic" + assert "realization-envelope-v1" in manifest.supported_contract_versions + assert backend_manifest_payload(manifest)["realization_envelope"] == ( + manifest.realization_envelope.identity.model_dump(mode="json") + ) + + +def test_operational_config_does_not_change_generic_material_identity(): + default = create_libvirt_manifest().realization_envelope + configured = create_libvirt_manifest(connection_uri="qemu:///session", name_prefix="example").realization_envelope + + assert default is not None and configured is not None + assert default.identity == configured.identity + + +def test_injected_driver_requires_explicit_mode(): + class DriverWithoutMode: + pass + + with pytest.raises(ValueError, match="driver_mode is required"): + create_libvirt_target(driver=DriverWithoutMode()) + + +def test_injected_generic_driver_selects_generic_envelope(): + target = create_libvirt_target(driver=RecordingLibvirtDriver(), driver_mode="generic") + + assert target.manifest.realization_envelope is not None + assert target.manifest.realization_envelope.configuration.mode == "generic" + + +def test_techvault_driver_selects_narrow_appliance_envelope(tmp_path): + driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path) + target = create_libvirt_target(driver=driver, driver_mode="techvault-appliance") + envelope = target.manifest.realization_envelope + + assert envelope is not None + assert envelope.id == "libvirt-qemu.techvault-appliance.v1" + assert envelope.configuration.mode == "techvault-appliance" + assert target.manifest.provisioner.supported_os_families == {"linux"} + assert not target.manifest.provisioner.supported_content_types + assert not target.manifest.provisioner.supports_accounts + assert not target.manifest.provisioner.supports_acls + + claims = {claim.concern.value: claim for claim in envelope.concerns} + assert claims["service"].disposition.value == "unsupported" + assert claims["service"].observation_strength.value == "none" + for concern in ("topology", "architecture", "image", "resource-allocation", "network"): + assert claims[concern].observation_strength.value == "daemon-observed" + assert claims[concern].disposition.value == "realized" + + +def test_driver_and_declared_mode_must_match(tmp_path): + driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path) + + with pytest.raises(ValueError, match="does not match driver_mode"): + create_libvirt_target(driver=driver, driver_mode="generic") + + +def test_direct_provisioner_construction_binds_techvault_driver_mode(tmp_path): + driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path) + generic = create_libvirt_manifest(driver_mode="generic") + + with pytest.raises(ValueError, match="capabilities do not match driver mode"): + LibvirtProvisioner( + driver, + provisioner_capabilities=generic.provisioner, + realization_envelope=generic.realization_envelope.identity, + ) + + +def test_unknown_libvirt_configuration_fails_closed(): + with pytest.raises(ValueError, match="unknown libvirt target configuration"): + create_libvirt_target(unrecognized=True) + + +def test_manifest_broader_than_selected_envelope_fails_before_driver_io(): + manifest = create_libvirt_manifest(driver_mode="generic") + broader_provisioner = replace( + manifest.provisioner, + supported_content_types=manifest.provisioner.supported_content_types | {"dataset"}, + ) + broader_manifest = replace( + manifest, + capabilities=replace(manifest.capabilities, provisioner=broader_provisioner), + ) + driver = RecordingLibvirtDriver() + + with pytest.raises(ValueError, match="capabilities do not match realization envelope"): + create_libvirt_components( + manifest=broader_manifest, + driver=driver, + driver_mode="generic", + ) + + assert not driver.recorded_ops + + +def _scenario(): + return parse_sdl( + dedent( + """ + name: envelope-test + nodes: + vm: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} + """ + ) + ) + + +def test_planner_carries_selected_envelope_identity_to_provisioning_plan(): + manifest = create_libvirt_manifest() + + result = run_reference_processor(_scenario(), manifest) + + assert result.execution_plan.provisioning.realization_envelope == manifest.realization_envelope.identity + + +def test_planner_uses_shared_membership_relation_for_selected_envelope(): + manifest = create_libvirt_manifest() + payload = manifest.realization_envelope.model_dump(mode="json") + payload["expression"]["domains"] = {"name": {"kind": "exact", "value": "different-scenario"}} + payload["expression"]["bindings"] = [{"path": "name", "scope": "scenario", "posture": "exact", "domain": "name"}] + payload["digest"] = realization_envelope_digest(payload) + restricted = BackendRealizationEnvelopeModel.model_validate(payload) + + result = run_reference_processor(_scenario(), replace(manifest, realization_envelope=restricted)) + + assert "realization-envelope.membership.domain-mismatch" in {diag.code for diag in result.diagnostics} diff --git a/implementations/python/tests/test_libvirt_backend_guest_certified.py b/implementations/python/tests/test_libvirt_backend_guest_certified.py new file mode 100644 index 000000000..75c18581b --- /dev/null +++ b/implementations/python/tests/test_libvirt_backend_guest_certified.py @@ -0,0 +1,548 @@ +"""Hermetic guest-certified libvirt driver + guest-observation coverage. + +These tests exercise the guest-observation orchestration and its falsification +paths with a fake libvirt connection and a stub fact transport. They validate +staging, freshness, and concern comparison; per the preflight they cannot and do +not satisfy the native-proof gate (that is the opt-in real-daemon certification). +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass, field +from pathlib import Path + +import pytest +from aces_backend_libvirt.cloudinit import CloudInitFile, CloudInitSpec, CloudInitUser +from aces_backend_libvirt.driver import DomainSpec, NetworkSpec, ServiceSpec +from aces_backend_libvirt.guest_appliance import GuestObservingInitramfsBuilder +from aces_backend_libvirt.guest_certified_driver import GuestCertifiedLibvirtDriver +from aces_backend_libvirt.techvault_matrix import mac_address + +_CHALLENGE = "deadbeefcafef00d" + + +@dataclass +class _StubTransport: + facts_by_address: dict[str, str | None] + failure: str | None = None + + def read(self, *, address, fact_channel_path, deadline_seconds): # noqa: ANN001, ANN201 + del fact_channel_path, deadline_seconds + if self.failure is not None: + return None, self.failure + return self.facts_by_address.get(address), None + + +class _NativeObject: + def __init__(self, name: str = "", xml: str = "") -> None: + self._name = name + self._xml = xml + self.created = False + self.destroyed = False + self.undefined = False + + def name(self): + return self._name + + def create(self): + self.created = True + + def isActive(self): # noqa: N802 - mirrors libvirt API + return int(self.created and not self.destroyed) + + def XMLDesc(self, _flags=0): # noqa: N802 - mirrors libvirt API + return self._xml + + def UUIDString(self): # noqa: N802 - mirrors libvirt API + import xml.etree.ElementTree as ET + + return ET.fromstring(self._xml).findtext("uuid") # noqa: S314 - test XML + + def destroy(self): + self.destroyed = True + + def undefine(self): + self.undefined = True + + +class _FakeConnection: + def __init__(self) -> None: + self.networks: dict[str, _NativeObject] = {} + self.domains: dict[str, _NativeObject] = {} + + def networkDefineXML(self, xml: str): # noqa: N802 + native = _NativeObject(_name_from_xml(xml), xml) + self.networks[native.name()] = native + return native + + def defineXML(self, xml: str): # noqa: N802 + native = _NativeObject(_name_from_xml(xml), xml) + self.domains[native.name()] = native + return native + + def networkLookupByName(self, name: str): # noqa: N802 + return self.networks[name] + + def lookupByName(self, name: str): # noqa: N802 + return self.domains[name] + + def listAllDomains(self): # noqa: N802 + return [native for native in self.domains.values() if not native.undefined] + + def listAllNetworks(self): # noqa: N802 + return [native for native in self.networks.values() if not native.undefined] + + +def _name_from_xml(xml: str) -> str: + start = xml.index("") + len("") + return xml[start : xml.index("")] + + +@dataclass +class _GuestFacts: + challenge: str = _CHALLENGE + architecture: str = "x86_64" + vcpus: int = 1 + memory_mib: int = 120 + interfaces: list[tuple[str, str, int]] = field(default_factory=list) + content: list[tuple[str, str, str]] = field(default_factory=list) + accounts: list[tuple[str, int, str, str, int, str]] = field(default_factory=list) + services: list[tuple[str, int, int, int]] = field(default_factory=list) + init_complete: bool = True + + def render(self) -> str: + lines = [ + "ACES-GUEST-FACTS v1", + f"challenge {self.challenge}", + f"architecture {self.architecture}", + f"vcpus {self.vcpus}", + f"memory_mib {self.memory_mib}", + ] + lines.extend(f"iface {mac} {ip} {up}" for mac, ip, up in self.interfaces) + lines.extend(f"content {path} {digest} {mode}" for path, digest, mode in self.content) + lines.extend( + f"account {name} {uid} {home} {shell} {dis} {groups}" + for name, uid, home, shell, dis, groups in self.accounts + ) + lines.extend(f"service {name} {port} {lis} {pid}" for name, port, lis, pid in self.services) + if self.init_complete: + lines.append("init complete") + return "\n".join(lines) + "\n" + + +def _sha(content: str) -> str: + return hashlib.sha256(content.encode("utf-8")).hexdigest() + + +def _network() -> NetworkSpec: + return NetworkSpec( + address="scn.net", + name="net", + cidr="10.9.0.0/24", + gateway="10.9.0.1", + labels={"internal": "true"}, + ) + + +def _domain() -> DomainSpec: + return DomainSpec( + address="scn.vm", + name="vm", + image_ref=None, + memory_mib=128, + vcpus=1, + networks=("scn.net",), + services=(ServiceSpec(name="beacon", port=9000),), + 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 + ), + ), + write_files=(CloudInitFile(path="/etc/aces/marker", content="hello", permissions="0644"),), + ), + ) + + +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")], + services=[("beacon", 9000, 1, 1)], + ) + + +def _driver(tmp_path: Path, connection: _FakeConnection, transport: _StubTransport) -> GuestCertifiedLibvirtDriver: + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel-bytes") + return GuestCertifiedLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + name_prefix="aces-gc", + kernel_path=kernel, + initramfs_builder=GuestObservingInitramfsBuilder(busybox_path=Path("/usr/bin/busybox")), + guest_transport=transport, + challenge=_CHALLENGE, + ) + + +def _realize(tmp_path: Path, facts: _GuestFacts | None, *, failure: str | None = None): + connection = _FakeConnection() + facts_map = {"scn.vm": facts.render() if facts is not None else None} + transport = _StubTransport(facts_by_address=facts_map, failure=failure) + driver = _driver(tmp_path, connection, transport) + result = driver.realize(networks=(_network(),), domains=(_domain(),)) + return driver, connection, result + + +def _codes(result) -> set[str]: + return {diag.code for diag in result.diagnostics} + + +def test_guest_certified_happy_path_realizes_and_certifies(tmp_path: Path) -> None: + driver, connection, result = _realize(tmp_path, _matching_facts()) + assert result.diagnostics == () + assert {handle.address for handle in result.domains} == {"scn.vm"} + guest_fields = {obs.field_path for obs in result.observations if obs.source.value == "guest-observed"} + assert { + "guest-architecture", + "guest-vcpus", + "guest-network", + "guest-content", + "guest-account", + "guest-service", + } <= guest_fields + assert driver.last_guest_binding["challenge"] == _CHALLENGE + assert "scn.vm" in driver.last_guest_facts + # Native objects remain (committed), not rolled back. + assert all(not native.destroyed for native in connection.domains.values()) + + +@pytest.mark.parametrize( + ("failure", "expected_code"), + [ + ("transport-unavailable", "libvirt-backend.guest.transport-unavailable"), + ("boot-timeout", "libvirt-backend.guest.boot-timeout"), + ], +) +def test_transport_stage_failures_are_typed_and_roll_back(tmp_path: Path, failure: str, expected_code: str) -> None: + driver, connection, result = _realize(tmp_path, _matching_facts(), failure=failure) + assert expected_code in _codes(result) + assert driver.last_guest_observations == () + assert all(native.destroyed for native in connection.domains.values()) + + +def test_missing_init_completion_fails(tmp_path: Path) -> None: + facts = _matching_facts() + facts.init_complete = False + _, connection, result = _realize(tmp_path, facts) + assert "libvirt-backend.guest.init-incomplete" in _codes(result) + assert all(native.destroyed for native in connection.domains.values()) + + +def test_stale_challenge_is_rejected(tmp_path: Path) -> None: + facts = _matching_facts() + facts.challenge = "0000000000000000" + _, _, result = _realize(tmp_path, facts) + assert "libvirt-backend.guest.challenge-mismatch" in _codes(result) + + +def test_malformed_report_is_rejected(tmp_path: Path) -> None: + connection = _FakeConnection() + transport = _StubTransport(facts_by_address={"scn.vm": "not a fact report"}) + driver = _driver(tmp_path, connection, transport) + result = driver.realize(networks=(_network(),), domains=(_domain(),)) + assert "libvirt-backend.guest.observation-malformed" in _codes(result) + + +@pytest.mark.parametrize("mutate", ["ip", "vcpus", "memory", "content", "account", "service", "missing_content"]) +def test_concern_mismatches_are_falsified(tmp_path: Path, mutate: str) -> None: + facts = _matching_facts() + mac = mac_address("scn.vm", "scn.net") + if mutate == "ip": + facts.interfaces = [(mac, "10.9.0.99", 1)] + elif mutate == "vcpus": + facts.vcpus = 2 + elif mutate == "memory": + facts.memory_mib = 8 # below the corroboration floor + elif mutate == "content": + facts.content = [("/etc/aces/marker", _sha("tampered"), "644")] + elif mutate == "account": + facts.accounts = [("analyst", 1000, "/home/analyst", "/bin/bash", 1, "aces")] + elif mutate == "service": + facts.services = [("beacon", 9000, 0, 1)] + elif mutate == "missing_content": + facts.content = [] + _, connection, result = _realize(tmp_path, facts) + assert "libvirt-backend.guest.observation-mismatch" in _codes(result) + assert all(native.destroyed for native in connection.domains.values()) + + +@pytest.mark.parametrize("second", ["vcpus 9", "vcpus 1", "challenge deadbeefcafef00d"]) +def test_duplicate_singleton_fact_is_falsified(tmp_path: Path, second: str) -> None: + # A repeated singleton fact (identical or conflicting) must be rejected distinctly + # rather than silently collapsed to the last value. + text = _matching_facts().render().replace("vcpus 1\n", f"vcpus 1\n{second}\n") + connection = _FakeConnection() + transport = _StubTransport(facts_by_address={"scn.vm": text}) + driver = _driver(tmp_path, connection, transport) + result = driver.realize(networks=(_network(),), domains=(_domain(),)) + assert "libvirt-backend.guest.observation-duplicate" in _codes(result) + assert all(native.destroyed for native in connection.domains.values()) + + +def test_account_without_supplemental_groups_is_certified(tmp_path: Path) -> None: + # An account with no supplemental groups is valid; the empty trailing groups + # field must not cause the account observation to be dropped. + mac = mac_address("scn.vm", "scn.net") + domain = DomainSpec( + address="scn.vm", + name="vm", + image_ref=None, + memory_mib=128, + vcpus=1, + networks=("scn.net",), + cloud_init=CloudInitSpec( + # noqa below: `shell` is a CloudInitUser account field, not a subprocess shell. + users=(CloudInitUser(name="loner", groups=(), shell="/bin/sh", home="/home/loner", lock_passwd=False),), # noqa: S604 + ), + ) + facts = _GuestFacts( + interfaces=[(mac, "10.9.0.10", 1)], + accounts=[("loner", 1000, "/home/loner", "/bin/sh", 0, "")], + ) + connection = _FakeConnection() + transport = _StubTransport(facts_by_address={"scn.vm": facts.render()}) + driver = _driver(tmp_path, connection, transport) + result = driver.realize(networks=(_network(),), domains=(domain,)) + assert result.diagnostics == () + account_obs = next(obs for obs in result.observations if obs.field_path == "guest-account") + assert account_obs.value == ("loner|/home/loner|/bin/sh|0|",) + + +def test_requested_image_is_rejected_before_mutation(tmp_path: Path) -> None: + connection = _FakeConnection() + transport = _StubTransport(facts_by_address={}) + driver = _driver(tmp_path, connection, transport) + domain = DomainSpec( + address="scn.vm", name="vm", image_ref="ubuntu:24.04", memory_mib=128, vcpus=1, networks=("scn.net",) + ) + result = driver.realize(networks=(_network(),), domains=(domain,)) + assert "libvirt-backend.techvault.image-unsupported" in _codes(result) + assert connection.domains == {} + + +def test_unsupported_placement_is_rejected(tmp_path: Path) -> None: + connection = _FakeConnection() + transport = _StubTransport(facts_by_address={}) + driver = _driver(tmp_path, connection, transport) + domain = DomainSpec( + address="scn.vm", + name="vm", + image_ref=None, + memory_mib=128, + vcpus=1, + networks=("scn.net",), + cloud_init=CloudInitSpec(packages=("nginx",)), + ) + result = driver.realize(networks=(_network(),), domains=(domain,)) + assert "libvirt-backend.techvault.guest-placement-unsupported" in _codes(result) + assert connection.domains == {} + + +# --- evidence-run integration (full control-plane pipeline) -------------------- + + +class _FakeBuilder: + def build(self, *, domain, target: Path): # noqa: ANN001, ANN201 + del domain + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(b"initramfs") + return target + + +def _bounded_guest_scenario(tmp_path: Path) -> Path: + scenario = tmp_path / "guest-certified.sdl.yaml" + scenario.write_text( + """\ +name: guest-certified +nodes: + lab: + type: switch + demo: + type: vm + os: linux + resources: {ram: 128 MiB, cpu: 1} + services: [] +infrastructure: + lab: + properties: {cidr: 192.0.2.0/24, gateway: 192.0.2.1, internal: true} + demo: + links: [lab] +""", + encoding="utf-8", + ) + return scenario + + +def _facts_from_matrix(matrix, challenge: str) -> dict[str, str]: + facts: dict[str, str] = {} + for domain in matrix["domains"]: + lines = [ + "ACES-GUEST-FACTS v1", + f"challenge {challenge}", + "architecture x86_64", + f"vcpus {domain['vcpus']}", + f"memory_mib {max(64, int(domain['memory_mib']) - 8)}", + ] + lines.extend(f"iface {iface['mac']} {iface['ip']} 1" for iface in domain["interfaces"]) + lines.append("init complete") + facts[domain["address"]] = "\n".join(lines) + "\n" + return facts + + +def _guest_matrix(scenario: Path): + from aces_backend_libvirt.manifest import create_libvirt_manifest + from aces_backend_libvirt.realization import interpret_provisioning_plan + from aces_backend_libvirt.techvault_matrix import native_matrix + from aces_sdl.parser import parse_sdl_file + + from aces.core.runtime.manager import RuntimeManager + + scout = GuestCertifiedLibvirtDriver( + state_dir=scenario.parent / "scout", + connection=_FakeConnection(), + name_prefix="evidence-test", + kernel_path=_write_kernel(scenario.parent), + initramfs_builder=_FakeBuilder(), + guest_transport=_StubTransport(facts_by_address={}), + challenge=_CHALLENGE, + ) + from aces_backend_libvirt import create_libvirt_target + + target = create_libvirt_target(participant_runtime=True, driver=scout) + plan = RuntimeManager(target).plan(parse_sdl_file(scenario)).provisioning + capabilities = create_libvirt_manifest(driver_mode="guest-certified-appliance").capabilities.provisioner + realization = interpret_provisioning_plan(plan, provisioner_capabilities=capabilities) + return native_matrix( + networks=realization.networks, + domains=realization.domains, + name_prefix="evidence-test", + include_placements=True, + ) + + +def _write_kernel(directory: Path) -> Path: + kernel = directory / "vmlinuz" + kernel.write_bytes(b"kernel") + return kernel + + +def _guest_factory(tmp_path: Path, transport: _StubTransport): + def factory(): + return GuestCertifiedLibvirtDriver( + state_dir=tmp_path / "state", + connection=_FakeConnection(), + name_prefix="evidence-test", + kernel_path=_write_kernel(tmp_path), + initramfs_builder=_FakeBuilder(), + guest_transport=transport, + challenge=_CHALLENGE, + ) + + return factory + + +def test_evidence_run_guest_certified_publishes_bound_observations(tmp_path: Path) -> None: + from aces_operations.libvirt_evidence_run import ( + LibvirtEvidenceRunConfig, + run_libvirt_evidence_run, + validate_libvirt_evidence_run_artifact, + ) + + scenario = _bounded_guest_scenario(tmp_path) + facts = _facts_from_matrix(_guest_matrix(scenario), _CHALLENGE) + transport = _StubTransport(facts_by_address=facts) + report = run_libvirt_evidence_run( + scenario_path=scenario, + project_dir=tmp_path, + run_id="gc-live-1", + config=LibvirtEvidenceRunConfig(evidence_source_mode="guest-certified"), + driver_factory=_guest_factory(tmp_path, transport), + ) + assert report.passed, report.render() + artifact = report.artifact + assert artifact is not None + assert validate_libvirt_evidence_run_artifact(artifact) == [] + guest = artifact["realization_facts"]["guest_observed"] + assert guest["source"] == "guest-observed" + assert guest["challenge"] == _CHALLENGE + assert guest["operation_ref"].startswith("sha256:") + # Native-proof boundary: an injected fake driver factory can exercise the + # orchestration but must be marked non-certifying so its evidence can never be + # published as a real guest certification. + assert guest["certifying"] is False + assert guest["domains"] and all(domain["correlation"].startswith("sha256:") for domain in guest["domains"]) + assert any(check.name == "native_substrate_cleanup" and check.passed for check in report.checks) + + +def test_evidence_run_guest_certified_transport_failure_fails_closed(tmp_path: Path) -> None: + from aces_operations.libvirt_evidence_run import LibvirtEvidenceRunConfig, run_libvirt_evidence_run + + scenario = _bounded_guest_scenario(tmp_path) + transport = _StubTransport(facts_by_address={}, failure="boot-timeout") + report = run_libvirt_evidence_run( + scenario_path=scenario, + project_dir=tmp_path, + run_id="gc-live-2", + config=LibvirtEvidenceRunConfig(evidence_source_mode="guest-certified"), + driver_factory=_guest_factory(tmp_path, transport), + ) + assert not report.passed, report.render() + assert report.artifact["backend"]["realization_provenance"]["substrate_realized"] is False + assert report.artifact["realization_facts"]["guest_observed"] == { + "source": "guest-observed", + "status": "not-observed", + } + + +def test_evidence_run_guest_certified_residual_probe_artifacts_fail_closed(tmp_path: Path) -> None: + # Native domains/networks are torn down cleanly, but residual guest-probe state + # survives: the run must fail closed rather than claim verified cleanup. + from aces_operations.libvirt_evidence_run import LibvirtEvidenceRunConfig, run_libvirt_evidence_run + + class _ResidualGuestDriver(GuestCertifiedLibvirtDriver): + def destroy(self, *, networks: tuple[str, ...], domains: tuple[str, ...]): + result = super().destroy(networks=networks, domains=domains) + self.last_guest_binding = {"challenge": self.challenge} + return result + + scenario = _bounded_guest_scenario(tmp_path) + facts = _facts_from_matrix(_guest_matrix(scenario), _CHALLENGE) + transport = _StubTransport(facts_by_address=facts) + + def factory(): + return _ResidualGuestDriver( + state_dir=tmp_path / "state", + connection=_FakeConnection(), + name_prefix="evidence-test", + kernel_path=_write_kernel(tmp_path), + initramfs_builder=_FakeBuilder(), + guest_transport=transport, + challenge=_CHALLENGE, + ) + + report = run_libvirt_evidence_run( + scenario_path=scenario, + project_dir=tmp_path, + run_id="gc-residual", + config=LibvirtEvidenceRunConfig(evidence_source_mode="guest-certified"), + driver_factory=factory, + ) + assert not report.passed, report.render() + cleanup = next(check for check in report.checks if check.name == "native_substrate_cleanup") + assert not cleanup.passed + assert any("guest probe artifacts were not fully cleaned" in diag for diag in cleanup.diagnostics) 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 new file mode 100644 index 000000000..35c4599db --- /dev/null +++ b/implementations/python/tests/test_libvirt_backend_guest_certified_real_libvirt.py @@ -0,0 +1,72 @@ +"""Opt-in real-libvirt certification for guest-observed realization probes. + +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, +BusyBox, and a readable kernel. Hermetic fake-driver tests cannot satisfy this +gate. +""" + +from __future__ import annotations + +import importlib +import os +import shutil +from pathlib import Path + +import pytest +from aces_operations.libvirt_evidence_run import ( + LibvirtEvidenceRunConfig, + run_libvirt_evidence_run, + validate_libvirt_evidence_run_artifact, +) +from paths import EXAMPLES_DIR + + +@pytest.mark.integration +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") + if not connection_uri: + pytest.skip("set ACES_REAL_LIBVIRT_URI to run real-libvirt guest certification") + try: + libvirt = importlib.import_module("libvirt") + except ImportError: + pytest.skip("libvirt-python is unavailable") + if shutil.which("cpio") is None or not Path("/usr/bin/busybox").is_file(): + pytest.skip("cpio and BusyBox are required for guest-observing appliance certification") + if not tuple(Path("/boot").glob("vmlinuz-*")): + pytest.skip("a readable host kernel is required for guest-observing appliance certification") + + report = run_libvirt_evidence_run( + scenario_path=EXAMPLES_DIR / "techvault-guest-certified.sdl.yaml", + project_dir=tmp_path, + run_id="real-libvirt-guest-certification", + config=LibvirtEvidenceRunConfig(evidence_source_mode="guest-certified", connection_uri=connection_uri), + ) + + assert report.passed, report.render() + artifact = report.artifact + assert artifact is not None + assert validate_libvirt_evidence_run_artifact(artifact) == [] + guest = artifact["realization_facts"]["guest_observed"] + assert guest["source"] == "guest-observed" + assert guest["operation_ref"].startswith("sha256:") + # The production driver (no injected factory) yields a certifying artifact. + assert guest["certifying"] is True + assert guest["domains"] + + daemon = artifact["realization_facts"]["daemon_observed"] + observed_domains = {str(item.get("name")) for item in daemon["domains"]} + observed_networks = {str(item.get("name")) for item in daemon["networks"]} + connection = libvirt.open(connection_uri) + assert connection is not None + try: + remaining_domains = {item.name() for item in connection.listAllDomains()} + remaining_networks = {item.name() for item in connection.listAllNetworks()} + finally: + connection.close() + assert remaining_domains.isdisjoint(observed_domains) + assert remaining_networks.isdisjoint(observed_networks) diff --git a/implementations/python/tests/test_libvirt_backend_manifest.py b/implementations/python/tests/test_libvirt_backend_manifest.py index 3fac9ace1..5a837f32e 100644 --- a/implementations/python/tests/test_libvirt_backend_manifest.py +++ b/implementations/python/tests/test_libvirt_backend_manifest.py @@ -30,6 +30,7 @@ def test_libvirt_manifest_declares_only_provisioning_contract_surface(): assert manifest.supported_contract_versions == frozenset( { "backend-manifest-v2", + "realization-envelope-v1", "operation-receipt-v1", "operation-status-v1", "provisioning-plan-v1", @@ -44,14 +45,12 @@ def test_libvirt_manifest_supports_vm_domains_and_switch_networks(): assert manifest.provisioner.supported_node_types == frozenset({"switch", "vm"}) -def test_libvirt_manifest_declares_full_content_and_account_realization(): - """Issue #603: cloud-init realizes the full governed content/account vocabulary.""" +def test_libvirt_manifest_narrows_unproven_content_and_account_realization(): + """ASR-519: only terms with a concrete generic-driver mechanism are claimed.""" provisioner = create_libvirt_manifest().provisioner - assert provisioner.supported_os_families == frozenset({"linux", "windows", "macos", "freebsd", "other"}) - assert provisioner.supported_content_types == frozenset({"file", "dataset", "directory"}) - assert provisioner.supported_account_features == frozenset( - {"groups", "mail", "spn", "shell", "home", "disabled", "auth_method"} - ) + assert provisioner.supported_os_families == frozenset({"linux"}) + assert provisioner.supported_content_types == frozenset({"file"}) + assert provisioner.supported_account_features == frozenset({"groups", "shell", "home", "disabled", "auth_method"}) assert provisioner.supports_accounts is True assert provisioner.supports_acls is True diff --git a/implementations/python/tests/test_libvirt_backend_manifest_publication.py b/implementations/python/tests/test_libvirt_backend_manifest_publication.py index bd7611e23..3c6459cad 100644 --- a/implementations/python/tests/test_libvirt_backend_manifest_publication.py +++ b/implementations/python/tests/test_libvirt_backend_manifest_publication.py @@ -87,8 +87,8 @@ def test_libvirt_target_passes_provisioning_only_conformance(): assert not report.unsupported_contract_gaps assert not report.unsupported_capability_gaps - live_manifest = next((case for case in report.cases if case.name == "live-manifest"), None) - assert live_manifest is not None, "conformance must run the live-manifest validation case" + live_manifest = next((case for case in report.cases if case.name == "target-manifest"), None) + assert live_manifest is not None, "conformance must run the target-manifest validation case" assert live_manifest.passed, [diag.message for diag in live_manifest.diagnostics] @@ -122,8 +122,8 @@ def test_realization_support_is_not_hollow(): ) -def test_manifest_declares_full_realization_envelope(): - """AC3: libvirt declares — and realizes via cloud-init — the full governed vocabulary.""" +def test_manifest_declares_only_the_configuration_bound_realization_envelope(): + """ASR-519: descriptor-only and unobserved terms are absent from coarse claims.""" manifest = create_libvirt_manifest() provisioner = manifest.provisioner declared_kinds = { @@ -134,10 +134,8 @@ def test_manifest_declares_full_realization_envelope(): assert "content-type" in declared_kinds assert "account-feature" in declared_kinds - assert provisioner.supported_content_types == frozenset({"file", "dataset", "directory"}) - assert provisioner.supported_account_features == frozenset( - {"groups", "mail", "spn", "shell", "home", "disabled", "auth_method"} - ) + assert provisioner.supported_content_types == frozenset({"file"}) + assert provisioner.supported_account_features == frozenset({"groups", "shell", "home", "disabled", "auth_method"}) assert provisioner.supports_accounts is True assert provisioner.supports_acls is True - assert "macos" in provisioner.supported_os_families + assert provisioner.supported_os_families == frozenset({"linux"}) diff --git a/implementations/python/tests/test_libvirt_backend_provisioner.py b/implementations/python/tests/test_libvirt_backend_provisioner.py index f953e6d11..f18d53194 100644 --- a/implementations/python/tests/test_libvirt_backend_provisioner.py +++ b/implementations/python/tests/test_libvirt_backend_provisioner.py @@ -4,6 +4,8 @@ from aces_backend_libvirt import LibvirtProvisioner from aces_backend_libvirt.driver import DomainHandle, DriverResult, NetworkHandle +from aces_backend_libvirt.envelopes import load_libvirt_realization_envelope +from aces_contracts.contracts import RealizationEnvelopeIdentityModel from aces_contracts.planning import ( ChangeAction, EvaluationPlan, @@ -88,6 +90,7 @@ def _plan(*resources: PlannedResource, action: ChangeAction = ChangeAction.CREAT ) for resource in resources ], + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) @@ -119,12 +122,68 @@ def test_apply_reconciles_snapshot_and_drives_libvirt_driver_for_create(): assert result.snapshot.entries["provision.node.web"].status == "applied" assert result.snapshot.entries["provision.node.web"].payload["os_family"] == "linux" assert driver.realize_calls + assert result.snapshot.realization_envelope == plan.realization_envelope domains = driver.realize_calls[0]["domains"] networks = driver.realize_calls[0]["networks"] assert [spec.address for spec in domains] == ["provision.node.web"] assert [spec.address for spec in networks] == ["provision.network.lan"] +def test_apply_rejects_missing_envelope_identity_before_driver_io(): + driver = _RecordingDriver() + plan = ProvisioningPlan(operations=_plan(_node_resource()).operations) + baseline = RuntimeSnapshot() + + result = LibvirtProvisioner(driver).apply(plan, baseline) + + assert result.success is False + assert result.snapshot is baseline + assert [diag.code for diag in result.diagnostics] == ["libvirt-backend.realization-envelope.missing"] + assert not driver.realize_calls + + +def test_apply_rejects_mismatched_envelope_identity_before_driver_io(): + driver = _RecordingDriver() + plan = _plan(_node_resource()) + wrong = RealizationEnvelopeIdentityModel( + **{**plan.realization_envelope.model_dump(), "digest": "sha256:" + "f" * 64} # type: ignore[union-attr] + ) + plan = ProvisioningPlan( + resources=plan.resources, + operations=plan.operations, + realization_envelope=wrong, + ) + baseline = RuntimeSnapshot() + + result = LibvirtProvisioner(driver).apply(plan, baseline) + + assert result.success is False + assert result.snapshot is baseline + assert [diag.code for diag in result.diagnostics] == ["libvirt-backend.realization-envelope.mismatch"] + assert not driver.realize_calls + + +def test_apply_rejects_snapshot_bound_to_another_envelope_before_driver_io(): + driver = _RecordingDriver() + plan = _plan(_node_resource()) + wrong = RealizationEnvelopeIdentityModel( + **{**plan.realization_envelope.model_dump(), "configuration_digest": "sha256:" + "e" * 64} # type: ignore[union-attr] + ) + baseline = RuntimeSnapshot( + entries={ + "provision.node.existing": SnapshotEntry("provision.node.existing", RuntimeDomain.PROVISIONING, "node", {}) + }, + realization_envelope=wrong, + ) + + result = LibvirtProvisioner(driver).apply(plan, baseline) + + assert result.success is False + assert result.snapshot is baseline + assert [diag.code for diag in result.diagnostics] == ["libvirt-backend.realization-envelope.baseline-mismatch"] + assert not driver.realize_calls + + def _account_resource() -> PlannedResource: return PlannedResource( address="provision.account.admin", @@ -219,6 +278,7 @@ def test_apply_realizes_target_domain_when_only_a_placement_changes(): payload=account.payload, ), ], + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) result = LibvirtProvisioner(driver).apply(plan, RuntimeSnapshot()) @@ -240,7 +300,8 @@ def test_apply_delete_removes_snapshot_entry_and_drives_destroy(): resource_type="node", payload={}, ) - } + }, + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) plan = ProvisioningPlan( operations=[ @@ -250,7 +311,8 @@ def test_apply_delete_removes_snapshot_entry_and_drives_destroy(): resource_type="node", payload={}, ) - ] + ], + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) result = LibvirtProvisioner(driver).apply(plan, snapshot) @@ -273,7 +335,8 @@ def test_apply_delete_of_already_absent_entry_is_idempotent_success(): resource_type="node", payload={}, ) - ] + ], + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) result = LibvirtProvisioner(driver).apply(plan, RuntimeSnapshot()) @@ -333,6 +396,7 @@ def test_apply_validates_operation_payloads_not_only_resources(): payload={"name": "gw", "node_type": "router", "os_family": "linux", "spec": {}}, ) ], + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) result = LibvirtProvisioner(driver).apply(plan, snapshot) @@ -374,7 +438,8 @@ def destroy(self, *, networks, domains): resource_type="node", payload={}, ) - } + }, + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) plan = ProvisioningPlan( operations=[ @@ -384,7 +449,8 @@ def destroy(self, *, networks, domains): resource_type="node", payload={}, ) - ] + ], + realization_envelope=load_libvirt_realization_envelope("generic").identity, ) result = LibvirtProvisioner(_SilentDestroyDriver()).apply(plan, snapshot) diff --git a/implementations/python/tests/test_libvirt_backend_realization.py b/implementations/python/tests/test_libvirt_backend_realization.py index 42b236fee..86c6d76ad 100644 --- a/implementations/python/tests/test_libvirt_backend_realization.py +++ b/implementations/python/tests/test_libvirt_backend_realization.py @@ -2,6 +2,7 @@ from __future__ import annotations +import pytest from aces_backend_libvirt.realization import interpret_provisioning_plan from aces_backend_protocols.capabilities import ProvisionerCapabilities from aces_contracts.planning import PlannedResource, ProvisioningPlan, RuntimeDomain @@ -77,6 +78,29 @@ def test_node_without_placements_gets_hostname_only_cloud_init(): assert cloud_init.write_files == () +def test_network_preserves_explicit_false_internal_setting(): + network = _resource( + "network", + "provision.network.external", + { + "name": "external", + "spec": { + "infrastructure": { + "properties": { + "cidr": "192.0.2.0/24", + "gateway": "192.0.2.1", + "internal": False, + } + } + }, + }, + ) + + realization = interpret_provisioning_plan(_plan(network)) + + assert realization.networks[0].labels["internal"] == "false" + + def test_account_placement_realizes_user_with_all_features(): account = _resource( "account-placement", @@ -391,12 +415,11 @@ def test_acl_with_wildcard_protocol_and_ports_fails_closed(): assert _domain(realization).network_acls == () -def test_unsupported_resource_type_still_emits_error_diagnostic(): +def test_unsupported_resource_type_is_rejected_at_plan_admission(): bogus = _resource("mystery", "provision.mystery.x", {"name": "x"}) - realization = interpret_provisioning_plan(_plan(_node(), bogus)) - - assert [d.code for d in realization.diagnostics] == ["libvirt-backend.realization.unsupported-resource"] + with pytest.raises(ValueError, match="resource_type must belong"): + _plan(_node(), bogus) def test_placement_targeting_unknown_node_fails_closed_with_diagnostic(): @@ -478,9 +501,9 @@ def test_out_of_envelope_content_type_fails_closed(): assert _domain(realization).cloud_init.write_files == () -def test_governed_vocabulary_realizes_without_envelope_error(): - # The full issue #603 governed vocabulary (all content types + account features) - # is in-envelope and must realize without any capability-envelope diagnostic. +def test_descriptor_only_vocabulary_is_rejected_by_the_narrowed_envelope(): + # ASR-519: dataset/directory descriptors and mail/SPN descriptors are not + # allowed to inherit a stronger generic realization claim. account = _resource( "account-placement", "provision.account.admin", @@ -518,7 +541,12 @@ def test_governed_vocabulary_realizes_without_envelope_error(): realization = interpret_provisioning_plan(_plan(_node(), account, file_content, dir_content, dataset_content)) envelope_codes = [d.code for d in realization.diagnostics if "unsupported-" in d.code] - assert envelope_codes == [] + assert envelope_codes == [ + "libvirt-backend.realization.unsupported-account-feature", + "libvirt-backend.realization.unsupported-account-feature", + "libvirt-backend.realization.unsupported-content-type", + "libvirt-backend.realization.unsupported-content-type", + ] def test_account_feature_outside_narrowed_envelope_fails_closed(): diff --git a/implementations/python/tests/test_libvirt_backend_registry.py b/implementations/python/tests/test_libvirt_backend_registry.py index ca5584aa1..2bd631ea9 100644 --- a/implementations/python/tests/test_libvirt_backend_registry.py +++ b/implementations/python/tests/test_libvirt_backend_registry.py @@ -15,6 +15,8 @@ class _NoopDriver: + driver_mode = "generic" + def realize(self, *, networks, domains): from aces_backend_libvirt.driver import DriverResult diff --git a/implementations/python/tests/test_libvirt_backend_techvault_honesty.py b/implementations/python/tests/test_libvirt_backend_techvault_honesty.py new file mode 100644 index 000000000..d269a79bf --- /dev/null +++ b/implementations/python/tests/test_libvirt_backend_techvault_honesty.py @@ -0,0 +1,348 @@ +"""ASR-519 TechVault admission, observation, and recovery falsification tests.""" + +from __future__ import annotations + +import pytest +from aces_backend_libvirt.driver import ( + DomainHandle, + DriverResult, + NetworkHandle, + RealizationObservation, +) +from aces_backend_libvirt.envelopes import load_libvirt_realization_envelope +from aces_backend_libvirt.provisioner import LibvirtProvisioner +from aces_backend_libvirt.target import create_libvirt_target +from aces_contracts.planning import ChangeAction, PlannedResource, ProvisioningPlan, ProvisionOp, RuntimeDomain +from aces_contracts.realization_envelope import ObservationStrength, RealizationConcern +from aces_contracts.runtime_state import RuntimeSnapshot, SnapshotEntry +from aces_runtime.control_plane import RuntimeControlPlane +from aces_runtime.control_plane_store import LocalControlPlaneStore + + +class _RecordingTechVaultDriver: + driver_mode = "techvault-appliance" + + def __init__(self) -> None: + self.realize_calls: list[dict[str, object]] = [] + self.destroy_calls: list[dict[str, object]] = [] + + def realize(self, *, networks, domains): + self.realize_calls.append({"networks": networks, "domains": domains}) + return DriverResult( + networks=tuple(NetworkHandle(address=spec.address) for spec in networks), + domains=tuple(DomainHandle(address=spec.address) for spec in domains), + ) + + def destroy(self, *, networks, domains): + self.destroy_calls.append({"networks": networks, "domains": domains}) + return DriverResult( + networks=tuple(NetworkHandle(address=address, realized=False) for address in networks), + domains=tuple(DomainHandle(address=address, realized=False) for address in domains), + ) + + def realized_addresses(self): + return frozenset() + + +class _ObservedTechVaultDriver(_RecordingTechVaultDriver): + def __init__(self, *, observed_memory_mib: int = 128, observed_vcpus: object | None = None) -> None: + super().__init__() + self.observed_memory_mib = observed_memory_mib + self.observed_vcpus = observed_vcpus + + def realize(self, *, networks, domains): + self.realize_calls.append({"networks": networks, "domains": domains}) + observations: list[RealizationObservation] = [] + for spec in domains: + observations.extend( + ( + _observation(spec.address, "exists", RealizationConcern.TOPOLOGY, True), + _observation(spec.address, "architecture", RealizationConcern.ARCHITECTURE, "x86_64"), + _observation( + spec.address, + "image-policy", + RealizationConcern.IMAGE, + "generated-initramfs-appliance", + ), + _observation( + spec.address, + "memory-mib", + RealizationConcern.RESOURCE_ALLOCATION, + self.observed_memory_mib, + ), + _observation( + spec.address, + "vcpus", + RealizationConcern.RESOURCE_ALLOCATION, + spec.vcpus if self.observed_vcpus is None else self.observed_vcpus, + ), + _observation( + spec.address, + "network-attachments", + RealizationConcern.NETWORK, + tuple(spec.networks), + ), + ) + ) + return DriverResult( + domains=tuple(DomainHandle(address=spec.address) for spec in domains), + observations=tuple(observations), + ) + + +class _DuplicateObservationDriver(_ObservedTechVaultDriver): + def realize(self, *, networks, domains): + result = super().realize(networks=networks, domains=domains) + duplicate = next(item for item in result.observations if item.field_path == "vcpus") + return DriverResult( + networks=result.networks, + domains=result.domains, + observations=(*result.observations, duplicate), + ) + + +def _observation(address: str, field_path: str, concern: RealizationConcern, value: object): + return RealizationObservation( + address=address, + field_path=field_path, + concern=concern, + source=ObservationStrength.DAEMON_OBSERVED, + value=value, + ) + + +def _node_resource( + *, + memory_mib: int = 128, + vcpus: int = 1, + image_ref: str | None = None, + services: list[dict[str, object]] | None = None, + acls: list[dict[str, object]] | None = None, +) -> PlannedResource: + node: dict[str, object] = { + "type": "vm", + "resources": {"ram": memory_mib, "cpu": vcpus}, + "services": services or [], + } + if image_ref is not None: + node["source"] = {"name": image_ref} + infrastructure: dict[str, object] = {"networks": []} + if acls is not None: + infrastructure["acls"] = acls + return PlannedResource( + address="provision.node.demo", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={ + "name": "demo", + "node_type": "vm", + "os_family": "linux", + "spec": {"node": node, "infrastructure": infrastructure}, + }, + ) + + +def _placement_resource(resource_type: str) -> PlannedResource: + specs: dict[str, dict[str, object]] = { + "account-placement": {"username": "operator"}, + "content-placement": {"type": "file", "path": "/etc/demo", "text": "demo"}, + "feature-binding": {"template": {"type": "service", "source": {"name": "demo-agent"}}}, + } + return PlannedResource( + address=f"provision.{resource_type}.demo", + domain=RuntimeDomain.PROVISIONING, + resource_type=resource_type, + payload={ + "name": "demo", + "target_address": "provision.node.demo", + "spec": specs[resource_type], + }, + ) + + +def _plan(*resources: PlannedResource, action: ChangeAction = ChangeAction.CREATE) -> ProvisioningPlan: + return ProvisioningPlan( + resources={resource.address: resource for resource in resources}, + operations=[ + ProvisionOp( + action=action, + address=resource.address, + resource_type=resource.resource_type, + payload=resource.payload, + ) + for resource in resources + ], + realization_envelope=load_libvirt_realization_envelope("techvault-appliance").identity, + ) + + +@pytest.mark.parametrize( + ("resource", "code"), + ( + (_node_resource(memory_mib=1024), "libvirt-backend.techvault.resource-out-of-envelope"), + (_node_resource(vcpus=4), "libvirt-backend.techvault.resource-out-of-envelope"), + (_node_resource(image_ref="requested.qcow2"), "libvirt-backend.techvault.image-unsupported"), + ( + _node_resource(services=[{"name": "api", "port": 8443, "protocol": "tcp"}]), + "libvirt-backend.techvault.service-unsupported", + ), + ), +) +def test_techvault_rejects_silent_transformations_before_driver_io(resource, code): + driver = _RecordingTechVaultDriver() + baseline = RuntimeSnapshot() + + result = LibvirtProvisioner(driver).apply(_plan(resource), baseline) + + assert result.success is False + assert result.snapshot is baseline + assert result.changed_addresses == [] + assert code in {diagnostic.code for diagnostic in result.diagnostics} + assert driver.realize_calls == [] + + +@pytest.mark.parametrize("resource_type", ("account-placement", "content-placement", "feature-binding")) +def test_techvault_rejects_unsupported_guest_placements_before_driver_io(resource_type): + driver = _RecordingTechVaultDriver() + node = _node_resource() + placement = _placement_resource(resource_type) + baseline = RuntimeSnapshot() + + result = LibvirtProvisioner(driver).apply(_plan(node, placement), baseline) + + assert result.success is False + assert result.snapshot is baseline + assert result.changed_addresses == [] + assert any(diagnostic.address == placement.address for diagnostic in result.diagnostics) + assert driver.realize_calls == [] + + +def test_techvault_rejects_updates_before_native_mutation(): + driver = _RecordingTechVaultDriver() + resource = _node_resource() + plan = _plan(resource, action=ChangeAction.UPDATE) + baseline = RuntimeSnapshot(realization_envelope=plan.realization_envelope) + + result = LibvirtProvisioner(driver).apply(plan, baseline) + + assert result.success is False + assert result.snapshot is baseline + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.update-unsupported"] + assert driver.realize_calls == [] + + +def test_techvault_rejects_delete_combined_with_another_mutation(): + driver = _RecordingTechVaultDriver() + created = _node_resource() + deleted_address = "provision.node.prior" + plan = ProvisioningPlan( + resources={created.address: created}, + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=created.address, + resource_type=created.resource_type, + payload=created.payload, + ), + ProvisionOp( + action=ChangeAction.DELETE, + address=deleted_address, + resource_type="node", + payload={"name": "prior"}, + ), + ], + realization_envelope=load_libvirt_realization_envelope("techvault-appliance").identity, + ) + baseline = RuntimeSnapshot(realization_envelope=plan.realization_envelope) + + result = LibvirtProvisioner(driver).apply(plan, baseline) + + assert result.success is False + assert result.snapshot is baseline + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "libvirt-backend.techvault.transaction-unsupported" + ] + assert driver.realize_calls == [] + assert driver.destroy_calls == [] + + +def test_techvault_rejects_fabricated_handle_without_daemon_observations(): + driver = _RecordingTechVaultDriver() + baseline = RuntimeSnapshot() + + result = LibvirtProvisioner(driver).apply(_plan(_node_resource()), baseline) + + assert result.success is False + assert result.snapshot is baseline + assert result.changed_addresses == [] + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.observation-missing"] + assert len(driver.realize_calls) == 1 + + +def test_techvault_commits_snapshot_only_after_complete_matching_daemon_observations(): + driver = _ObservedTechVaultDriver() + + result = LibvirtProvisioner(driver).apply(_plan(_node_resource()), RuntimeSnapshot()) + + assert result.success is True + assert result.changed_addresses == ["provision.node.demo"] + assert result.snapshot.entries["provision.node.demo"].status == "applied" + + +def test_techvault_detects_resource_clamping_in_daemon_readback(): + driver = _ObservedTechVaultDriver(observed_memory_mib=64) + baseline = RuntimeSnapshot() + + result = LibvirtProvisioner(driver).apply(_plan(_node_resource()), baseline) + + assert result.success is False + assert result.snapshot is baseline + assert result.changed_addresses == [] + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.observation-mismatch"] + assert driver.destroy_calls == [{"networks": (), "domains": ("provision.node.demo",)}] + + +@pytest.mark.parametrize( + "driver", + ( + _ObservedTechVaultDriver(observed_vcpus=True), + _DuplicateObservationDriver(), + ), +) +def test_techvault_rejects_type_coercion_and_duplicate_daemon_observations(driver): + baseline = RuntimeSnapshot() + + result = LibvirtProvisioner(driver).apply(_plan(_node_resource()), baseline) + + assert result.success is False + assert result.snapshot is baseline + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.observation-mismatch"] + assert driver.destroy_calls == [{"networks": (), "domains": ("provision.node.demo",)}] + + +def test_failed_techvault_admission_preserves_persisted_runtime_snapshot(tmp_path): + driver = _RecordingTechVaultDriver() + envelope = load_libvirt_realization_envelope("techvault-appliance").identity + baseline = RuntimeSnapshot( + entries={ + "provision.node.prior": SnapshotEntry( + address="provision.node.prior", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={"name": "prior"}, + ) + }, + realization_envelope=envelope, + ) + store = LocalControlPlaneStore(tmp_path / "control-plane") + store.save_snapshot(baseline) + control_plane = RuntimeControlPlane(create_libvirt_target(driver=driver), store=store) + invalid = _node_resource(services=[{"name": "api", "port": 8443, "protocol": "tcp"}]) + + receipt = control_plane.submit_provisioning(_plan(invalid)) + status = control_plane.get_operation(receipt.operation_id) + restarted = RuntimeControlPlane(create_libvirt_target(driver=driver), store=store) + + assert status is not None and status.state.value == "failed" + assert restarted.snapshot == baseline + assert driver.realize_calls == [] diff --git a/implementations/python/tests/test_libvirt_backend_techvault_integration.py b/implementations/python/tests/test_libvirt_backend_techvault_integration.py index c2a1905d2..759ffc704 100644 --- a/implementations/python/tests/test_libvirt_backend_techvault_integration.py +++ b/implementations/python/tests/test_libvirt_backend_techvault_integration.py @@ -22,6 +22,8 @@ class _RecordingLibvirtDriver: + driver_mode = "generic" + def __init__(self) -> None: self.realize_calls: list[dict[str, object]] = [] self.destroy_calls: list[dict[str, object]] = [] diff --git a/implementations/python/tests/test_libvirt_backend_techvault_native.py b/implementations/python/tests/test_libvirt_backend_techvault_native.py index 8d015c774..0fd1bba33 100644 --- a/implementations/python/tests/test_libvirt_backend_techvault_native.py +++ b/implementations/python/tests/test_libvirt_backend_techvault_native.py @@ -2,19 +2,32 @@ from __future__ import annotations +import gzip import json +import xml.etree.ElementTree as ET +from dataclasses import replace from pathlib import Path import pytest from aces_backend_libvirt import create_libvirt_target +from aces_backend_libvirt.cloudinit import CloudInitSpec, CloudInitUser +from aces_backend_libvirt.driver import DomainSpec, NetworkAcl, NetworkSpec, ServiceSpec +from aces_backend_libvirt.envelopes import load_libvirt_realization_envelope from aces_backend_libvirt.techvault_native import ( BusyboxInitramfsBuilder, ProbeResult, TechVaultNativeLibvirtDriver, + check_native_readiness, expected_surface, + native_soc_readback, ) +from aces_backend_protocols.naming import provider_resource_name from aces_operations import techvault_live -from aces_operations.techvault_live import TechVaultLiveConfig, validate_techvault_live +from aces_operations.techvault_live import ( + TechVaultLiveConfig, + validate_techvault_live, + validate_techvault_live_manifest, +) from paths import EXAMPLES_DIR from aces.core.runtime.control_plane import RuntimeControlPlane @@ -23,8 +36,9 @@ class _NativeObject: - def __init__(self, name: str = "") -> None: + def __init__(self, name: str = "", xml: str = "") -> None: self._name = name + self._xml = xml self.created = False self.destroyed = False self.undefined = False @@ -35,6 +49,17 @@ def name(self): def create(self): self.created = True + def isActive(self): # noqa: N802 - mirrors libvirt API + return int(self.created and not self.destroyed) + + def XMLDesc(self, _flags=0): # noqa: N802 - mirrors libvirt API + return self._xml + + def UUIDString(self): # noqa: N802 - mirrors libvirt API + if not self._xml: + return None + return ET.fromstring(self._xml).findtext("uuid") # noqa: S314 - test-generated XML + def destroy(self): self.destroyed = True @@ -42,6 +67,11 @@ def undefine(self): self.undefined = True +class _RollbackFailObject(_NativeObject): + def destroy(self): + raise RuntimeError("rollback blocked") + + class _FakeConnection: def __init__(self) -> None: self.network_xml: list[str] = [] @@ -52,14 +82,14 @@ def __init__(self) -> None: def networkDefineXML(self, xml: str): # noqa: N802 - mirrors libvirt API self.network_xml.append(xml) name = _name_from_xml(xml) - native = _NativeObject(name) + native = _NativeObject(name, xml) self.networks[name] = native return native def defineXML(self, xml: str): # noqa: N802 - mirrors libvirt API self.domain_xml.append(xml) name = _name_from_xml(xml) - native = _NativeObject(name) + native = _NativeObject(name, xml) self.domains[name] = native return native @@ -70,10 +100,81 @@ def lookupByName(self, name: str): # noqa: N802 - mirrors libvirt API return self.domains[name] def listAllDomains(self): # noqa: N802 - mirrors libvirt API - return list(self.domains.values()) + return [native for native in self.domains.values() if not native.undefined] def listAllNetworks(self): # noqa: N802 - mirrors libvirt API - return list(self.networks.values()) + return [native for native in self.networks.values() if not native.undefined] + + +class _SecondDomainFailsConnection(_FakeConnection): + def __init__(self, *, rollback_fails: bool = False) -> None: + super().__init__() + self._define_count = 0 + self.rollback_fails = rollback_fails + + def defineXML(self, xml: str): # noqa: N802 - mirrors libvirt API + self._define_count += 1 + if self._define_count == 2: + raise RuntimeError("second define failed") + self.domain_xml.append(xml) + name = _name_from_xml(xml) + native_type = _RollbackFailObject if self.rollback_fails else _NativeObject + native = native_type(name, xml) + self.domains[name] = native + return native + + +class _LookupFailure(Exception): + def __init__(self, code: int) -> None: + super().__init__("native lookup failed") + self.code = code + + def get_error_code(self): + return self.code + + +class _FailingLookupConnection(_FakeConnection): + def lookupByName(self, name: str): # noqa: N802 - mirrors libvirt API + del name + raise _LookupFailure(1) + + def listAllDomains(self): # noqa: N802 - mirrors libvirt API + raise _LookupFailure(1) + + +class _InactiveDomainConnection(_FakeConnection): + def defineXML(self, xml: str): # noqa: N802 - mirrors libvirt API + native = super().defineXML(xml) + native.isActive = lambda: 0 # type: ignore[method-assign] + return native + + +class _SubstitutedInitrdConnection(_FakeConnection): + def defineXML(self, xml: str): # noqa: N802 - mirrors libvirt API + root = ET.fromstring(xml) # noqa: S314 - test-generated XML + root.find("./os/initrd").text = "/unbound/substitute.cpio.gz" + return super().defineXML(ET.tostring(root, encoding="unicode")) + + +class _ExtraAttachmentConnection(_FakeConnection): + def defineXML(self, xml: str): # noqa: N802 - mirrors libvirt API + root = ET.fromstring(xml) # noqa: S314 - test-generated XML + devices = root.find("devices") + interface = ET.SubElement(devices, "interface", {"type": "network"}) + ET.SubElement(interface, "source", {"network": "foreign-network"}) + return super().defineXML(ET.tostring(root, encoding="unicode")) + + +class _WrongForwardModeConnection(_FakeConnection): + def networkDefineXML(self, xml: str): # noqa: N802 - mirrors libvirt API + root = ET.fromstring(xml) # noqa: S314 - test-generated XML + root.find("forward").set("mode", "route") + return super().networkDefineXML(ET.tostring(root, encoding="unicode")) + + +class _UnverifiableDomainCleanupConnection(_FakeConnection): + def listAllDomains(self): # noqa: N802 - mirrors libvirt API + raise RuntimeError("listing unavailable") class _Builder: @@ -97,7 +198,49 @@ def _name_from_xml(xml: str) -> str: return xml[start:end] -def _apply_native_scenario(path: Path, tmp_path: Path): +def _bounded_scenario(tmp_path: Path) -> Path: + path = tmp_path / "bounded.sdl.yaml" + path.write_text( + """\ +name: bounded +nodes: + lab: {type: switch} + demo: + type: vm + os: linux + resources: {ram: 128 MiB, cpu: 1} + services: [] +infrastructure: + lab: + properties: {cidr: 192.0.2.0/24, gateway: 192.0.2.1, internal: true} + demo: + links: [lab] +""", + encoding="utf-8", + ) + return path + + +def _bounded_specs() -> tuple[NetworkSpec, DomainSpec]: + network = NetworkSpec( + address="provision.network.lab", + name="lab", + cidr="192.0.2.0/24", + gateway="192.0.2.1", + labels={"internal": "true"}, + ) + domain = DomainSpec( + address="provision.node.demo", + name="demo", + image_ref=None, + memory_mib=128, + vcpus=2, + networks=(network.address,), + ) + return network, domain + + +def _submit_native_scenario(path: Path, tmp_path: Path): connection = _FakeConnection() kernel = tmp_path / "vmlinuz" kernel.write_bytes(b"kernel") @@ -116,25 +259,22 @@ def _apply_native_scenario(path: Path, tmp_path: Path): receipt = control_plane.submit_provisioning(execution_plan.provisioning) status = control_plane.get_operation(receipt.operation_id) assert status is not None - assert status.state.value == "succeeded", status.diagnostics - return driver, connection + return driver, connection, status, control_plane.snapshot -def test_operational_techvault_realizes_native_libvirt_domains_without_compose(tmp_path): - driver, connection = _apply_native_scenario(EXAMPLES_DIR / "techvault-operational.sdl.yaml", tmp_path) +def test_operational_techvault_rejects_unrealized_concerns_before_libvirt_io(tmp_path): + driver, connection, status, snapshot = _submit_native_scenario( + EXAMPLES_DIR / "techvault-operational.sdl.yaml", tmp_path + ) - surface = expected_surface(driver.last_snapshot) - assert surface["substrate"] == "libvirt-qemu-initramfs" - assert len(surface["domains"]) == 30 - assert len(surface["networks"]) == 4 - assert "thehive" in surface["domains"] - assert "misp" in surface["domains"] - assert "suricata" in surface["domains"] - assert "docker" not in json.dumps(driver.last_snapshot).lower() - assert "compose" not in json.dumps(driver.last_snapshot).lower() - assert len(connection.domain_xml) == 30 - assert len(connection.network_xml) == 4 - assert all("" in xml and "" in xml for xml in connection.domain_xml) + assert status.state.value == "failed" + codes = {diagnostic.code for diagnostic in status.diagnostics} + assert "libvirt-backend.techvault.resource-out-of-envelope" in codes + assert "libvirt-backend.techvault.service-unsupported" in codes + assert connection.domain_xml == [] + assert connection.network_xml == [] + assert driver.last_snapshot == {} + assert snapshot.entries == {} @pytest.mark.parametrize( @@ -146,16 +286,486 @@ def test_operational_techvault_realizes_native_libvirt_domains_without_compose(t ("techvault-attacker-target.sdl.yaml", 8, 3), ), ) -def test_curated_variants_drive_distinct_native_surfaces(filename, domain_count, network_count, tmp_path): - driver, _connection = _apply_native_scenario(EXAMPLES_DIR / filename, tmp_path) +def test_curated_variants_do_not_turn_planned_surfaces_into_native_claims( + filename, domain_count, network_count, tmp_path +): + del domain_count, network_count + driver, connection, status, snapshot = _submit_native_scenario(EXAMPLES_DIR / filename, tmp_path) + + assert status.state.value == "failed" + assert connection.domain_xml == [] + assert connection.network_xml == [] + assert driver.last_snapshot == {} + assert snapshot.entries == {} + +def test_bounded_substrate_emits_complete_daemon_observations(tmp_path): + connection = _FakeConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + network = NetworkSpec( + address="provision.network.lab", + name="lab", + cidr="192.0.2.0/24", + gateway="192.0.2.1", + labels={"internal": "true"}, + ) + domain = DomainSpec( + address="provision.node.demo", + name="demo", + image_ref=None, + memory_mib=128, + vcpus=2, + networks=(network.address,), + ) + + result = driver.realize(networks=(network,), domains=(domain,)) + + assert not result.diagnostics + assert len(result.observations) == 13 + assert {observation.source.value for observation in result.observations} == {"daemon-observed"} surface = expected_surface(driver.last_snapshot) - assert len(surface["domains"]) == domain_count - assert len(surface["networks"]) == network_count - assert surface["service_count"] > 0 + assert surface["source"] == "daemon-observed" + assert surface["domains"] == (provider_resource_name(domain.address, prefix="native-test"),) + assert surface["networks"] == (provider_resource_name(network.address, prefix="native-test"),) + assert "service_count" not in surface + network_uuid = ET.fromstring(connection.network_xml[0]).findtext("uuid") # noqa: S314 - test XML + domain_uuid = ET.fromstring(connection.domain_xml[0]).findtext("uuid") # noqa: S314 - test XML + assert network_uuid + assert domain_uuid + assert network_uuid != domain_uuid + binding = driver.last_snapshot["binding"] + envelope = load_libvirt_realization_envelope("techvault-appliance") + assert binding["driver"] == "techvault-appliance" + assert binding["realization_envelope_digest"] == envelope.digest + assert binding["configuration_digest"] == envelope.configuration.configuration_digest + assert binding["driver_configuration_digest"].startswith("sha256:") + assert set(binding["boot_artifact_digests"]) == {"kernel", "initramfs"} + assert native_soc_readback(driver.last_snapshot) == { + "status": "not-observed", + "observation_source": "none", + "reason": "guest SOC state requires concern-specific guest observation", + } + ready, readiness_diagnostics = check_native_readiness( + driver.last_snapshot, + probe=_Probe(), + timeout_seconds=1, + poll_seconds=1, + ) + assert ready is False + assert readiness_diagnostics == ["guest readiness requires concern-specific guest observation"] -def test_validate_techvault_live_records_native_manifest(tmp_path): +def test_native_driver_rejects_inactive_daemon_readback_and_rolls_back(tmp_path): + connection = _InactiveDomainConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + network, domain = _bounded_specs() + + result = driver.realize(networks=(network,), domains=(domain,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.observation-mismatch"] + assert connection.domains[provider_resource_name(domain.address, prefix="native-test")].undefined is True + assert connection.networks[provider_resource_name(network.address, prefix="native-test")].undefined is True + assert driver.last_snapshot == {} + + +def test_native_driver_rejects_substituted_boot_artifact_readback(tmp_path): + connection = _SubstitutedInitrdConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + network, domain = _bounded_specs() + + result = driver.realize(networks=(network,), domains=(domain,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.observation-mismatch"] + assert connection.domains[provider_resource_name(domain.address, prefix="native-test")].undefined is True + assert connection.networks[provider_resource_name(network.address, prefix="native-test")].undefined is True + + +def test_native_driver_rejects_extra_unbound_network_attachment(tmp_path): + connection = _ExtraAttachmentConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + network, domain = _bounded_specs() + + result = driver.realize(networks=(network,), domains=(domain,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.observation-mismatch"] + + +def test_native_driver_rejects_substituted_network_forwarding_policy(tmp_path): + connection = _WrongForwardModeConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + network, domain = _bounded_specs() + network = replace(network, labels={"internal": "false"}) + + result = driver.realize(networks=(network,), domains=(domain,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault.observation-mismatch"] + + +def test_native_driver_rolls_back_when_evidence_binding_cannot_be_built(tmp_path, monkeypatch): + connection = _FakeConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + network, domain = _bounded_specs() + + def _fail_binding(*_args): + raise OSError("material unavailable") + + monkeypatch.setattr(driver, "_material_binding", _fail_binding) + + result = driver.realize(networks=(network,), domains=(domain,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "libvirt-backend.techvault-native.operation-failed" + ] + assert connection.domains[provider_resource_name(domain.address, prefix="native-test")].undefined is True + assert connection.networks[provider_resource_name(network.address, prefix="native-test")].undefined is True + assert driver.last_snapshot == {} + + +def test_native_driver_does_not_claim_cleanup_when_native_listing_fails(tmp_path): + connection = _UnverifiableDomainCleanupConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + network, domain = _bounded_specs() + realized = driver.realize(networks=(network,), domains=(domain,)) + assert not realized.diagnostics + + result = driver.destroy(networks=(), domains=(domain.address,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == ["libvirt-backend.techvault-native.residual-state"] + assert result.domains[0].realized is True + + +def test_native_driver_recovers_owned_resources_by_uuid_after_restart(tmp_path): + connection = _FakeConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + network = NetworkSpec( + address="provision.network.identity", + name="lab-display", + cidr="192.0.2.0/24", + gateway="192.0.2.1", + labels={"internal": "true"}, + ) + domain = DomainSpec( + address="provision.node.identity", + name="demo-display", + image_ref=None, + memory_mib=128, + vcpus=1, + networks=(network.address,), + ) + first = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + assert not first.realize(networks=(network,), domains=(domain,)).diagnostics + artifact_paths = tuple(path for paths in first._artifacts.values() for path in paths) + assert all(path.exists() for path in artifact_paths) + restarted = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + ) + + result = restarted.destroy(networks=(network.address,), domains=(domain.address,)) + + assert not result.diagnostics + assert connection.domains[provider_resource_name(domain.address, prefix="native-test")].undefined is True + assert connection.networks[provider_resource_name(network.address, prefix="native-test")].undefined is True + assert all(not path.exists() for path in artifact_paths) + + +@pytest.mark.parametrize( + ("mutation", "code"), + ( + ({"memory_mib": 256}, "libvirt-backend.techvault.resource-out-of-envelope"), + ({"image_ref": "requested.qcow2"}, "libvirt-backend.techvault.image-unsupported"), + ( + {"services": (ServiceSpec(name="api", port=8443),)}, + "libvirt-backend.techvault.service-unsupported", + ), + ( + {"cloud_init": CloudInitSpec(users=(CloudInitUser(name="operator"),))}, + "libvirt-backend.techvault.guest-placement-unsupported", + ), + ( + {"cloud_init": CloudInitSpec(hostname="substituted")}, + "libvirt-backend.techvault.guest-placement-unsupported", + ), + ({"labels": {"unbound": "value"}}, "libvirt-backend.techvault.metadata-unsupported"), + ( + {"network_acls": (NetworkAcl(name="deny", action="drop", direction="in", protocol="all"),)}, + "libvirt-backend.techvault.acl-unsupported", + ), + ), +) +def test_native_driver_direct_entrypoint_rejects_unsupported_domain_concerns(tmp_path, mutation, code): + connection = _FakeConnection() + driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path / "state", connection=connection) + domain = replace( + DomainSpec( + address="provision.node.demo", + name="demo", + image_ref=None, + memory_mib=128, + vcpus=1, + ), + **mutation, + ) + + result = driver.realize(networks=(), domains=(domain,)) + + assert code in {diagnostic.code for diagnostic in result.diagnostics} + assert connection.domain_xml == [] + assert connection.network_xml == [] + assert not (tmp_path / "state").exists() + + +@pytest.mark.parametrize( + "network", + ( + NetworkSpec(address="provision.network.lab", name="lab"), + NetworkSpec( + address="provision.network.lab", + name="lab", + cidr="192.0.2.0/24", + gateway="192.0.2.1", + labels={"internal": "implicit"}, + ), + NetworkSpec( + address="provision.network.lab", + name="lab", + cidr="192.0.2.0/24", + gateway="192.0.2.1", + labels={"internal": "true", "unbound": "value"}, + ), + ), +) +def test_native_driver_direct_entrypoint_rejects_implicit_network_values(tmp_path, network): + connection = _FakeConnection() + driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path / "state", connection=connection) + + result = driver.realize(networks=(network,), domains=()) + + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "libvirt-backend.techvault.network-exactness-required" + ] + assert connection.network_xml == [] + assert not (tmp_path / "state").exists() + + +def test_native_driver_rejects_network_without_deterministic_host_capacity(tmp_path): + connection = _FakeConnection() + driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path / "state", connection=connection) + network = NetworkSpec( + address="provision.network.small", + name="small", + cidr="192.0.2.0/29", + gateway="192.0.2.1", + labels={"internal": "true"}, + ) + domain = DomainSpec( + address="provision.node.demo", + name="demo", + image_ref=None, + memory_mib=128, + vcpus=1, + networks=(network.address,), + ) + + result = driver.realize(networks=(network,), domains=(domain,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "libvirt-backend.techvault.network-exactness-required" + ] + assert connection.network_xml == [] + assert connection.domain_xml == [] + assert not (tmp_path / "state").exists() + + +@pytest.mark.parametrize("rollback_fails", (False, True)) +def test_native_driver_verifies_partial_create_rollback_and_reports_residual_state(tmp_path, rollback_fails): + connection = _SecondDomainFailsConnection(rollback_fails=rollback_fails) + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + initramfs_builder=_Builder(), + ) + driver.last_snapshot = {"prior": "observation"} + domains = tuple( + DomainSpec( + address=f"provision.node.demo-{index}", + name=f"demo-{index}", + image_ref=None, + memory_mib=128, + vcpus=1, + ) + for index in (1, 2) + ) + + result = driver.realize(networks=(), domains=domains) + + 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")] + if rollback_fails: + assert "libvirt-backend.techvault-native.residual-state" in { + diagnostic.code for diagnostic in result.diagnostics + } + assert first.undefined is False + assert list((tmp_path / "state" / "initramfs").glob("*.cpio.gz")) + else: + assert "libvirt-backend.techvault-native.residual-state" not in { + diagnostic.code for diagnostic in result.diagnostics + } + assert first.destroyed is True + assert first.undefined is True + assert list((tmp_path / "state" / "initramfs").glob("*.cpio.gz")) == [] + assert driver.last_snapshot == {"prior": "observation"} + + +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 = _NativeObject( + foreign_name, + f"{foreign_name}00000000-0000-4000-8000-000000000000", + ) + foreign.created = True + connection.domains[foreign.name()] = foreign + driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path / "state", connection=connection) + + result = driver.destroy(networks=(), domains=("provision.node.demo",)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "libvirt-backend.techvault-native.ownership-conflict" + ] + assert result.domains[0].realized is True + assert foreign.destroyed is False + assert foreign.undefined is False + + +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 = _NativeObject( + foreign_name, + f"{foreign_name}00000000-0000-4000-8000-000000000000", + ) + foreign.created = True + connection.domains[foreign.name()] = foreign + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") + driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + initramfs_builder=_Builder(), + ) + domain = DomainSpec( + address="provision.node.demo", + name="demo", + image_ref=None, + memory_mib=128, + vcpus=1, + ) + + result = driver.realize(networks=(), domains=(domain,)) + + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "libvirt-backend.techvault-native.ownership-conflict" + ] + assert connection.domains[foreign.name()] is foreign + assert foreign.destroyed is False + assert foreign.undefined is False + assert connection.domain_xml == [] + + +def test_native_driver_destroy_is_idempotent_only_for_verified_absence(tmp_path): + absent_driver = TechVaultNativeLibvirtDriver(state_dir=tmp_path / "absent", connection=_FakeConnection()) + + absent = absent_driver.destroy(networks=(), domains=("provision.node.demo",)) + + assert not absent.diagnostics + assert absent.domains[0].realized is False + + uncertain_driver = TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "uncertain", + connection=_FailingLookupConnection(), + ) + + uncertain = uncertain_driver.destroy(networks=(), domains=("provision.node.demo",)) + + assert [diagnostic.code for diagnostic in uncertain.diagnostics] == [ + "libvirt-backend.techvault-native.residual-state" + ] + assert uncertain.domains[0].realized is True + + +def test_validate_techvault_live_records_truthful_failed_manifest(tmp_path): scenario = EXAMPLES_DIR / "techvault-attacker-target.sdl.yaml" def _driver_factory(): @@ -173,19 +783,64 @@ def _driver_factory(): scenario_path=scenario, project_dir=tmp_path, run_id="native-live", - config=TechVaultLiveConfig(boot_timeout_seconds=1), + config=TechVaultLiveConfig(), driver_factory=_driver_factory, - probe=_Probe(), ) - assert report.passed, report.render() + 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["aces_libvirt"]["substrate"] == "libvirt-qemu-initramfs" - assert payload["snapshot"]["containers"] == [] - assert "kali" in payload["aces_libvirt"]["surface"]["domains"] - assert "victim" in payload["aces_libvirt"]["surface"]["domains"] + assert payload["scenario"]["path"] == "examples/scenarios/techvault-attacker-target.sdl.yaml" + facts = payload["realization_facts"] + assert facts["authored"]["source"] == "authored" + assert facts["planned"]["source"] == "planned" + assert facts["driver_reported"]["status"] == "failed" + assert facts["daemon_observed"] == {"source": "daemon-observed", "domains": [], "networks": []} + assert facts["guest_observed"] == {"source": "guest-observed", "status": "not-observed"} + assert payload["validation"]["ok"] is False + rendered = json.dumps(payload, sort_keys=True) + assert "native-realized" not in rendered + assert "soc_readback" not in rendered + assert str(tmp_path) not in rendered + + +def test_validate_techvault_live_accepts_bounded_daemon_observed_substrate(tmp_path): + connection = _FakeConnection() + + def _driver_factory(): + kernel = tmp_path / "vmlinuz-live" + kernel.write_bytes(b"kernel") + return TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="live-test", + initramfs_builder=_Builder(), + ) + + report = validate_techvault_live( + scenario_path=_bounded_scenario(tmp_path), + project_dir=tmp_path, + run_id="bounded-live", + config=TechVaultLiveConfig(), + driver_factory=_driver_factory, + ) + + assert report.passed, report.render() + payload = json.loads( + (tmp_path / "runs" / "bounded-live" / "live-gate" / "manifest.json").read_text(encoding="utf-8") + ) + assert payload["realization_facts"]["daemon_observed"]["domains"] == [ + provider_resource_name("provision.node.demo", prefix="live-test") + ] + assert payload["realization_facts"]["guest_observed"]["status"] == "not-observed" + assert payload["cleanup"] == {"source": "driver-reported", "status": "verified"} + assert all(native.undefined for native in (*connection.domains.values(), *connection.networks.values())) + assert "native-realized" not in json.dumps(payload, sort_keys=True) + + payload["realization_facts"]["planned"]["source"] = "daemon-observed" + assert any("planned.source" in violation for violation in validate_techvault_live_manifest(payload)) def test_live_gate_has_no_aptl_or_docker_probe_dependency(): @@ -195,7 +850,7 @@ def test_live_gate_has_no_aptl_or_docker_probe_dependency(): assert "aptl" not in source.lower() -def test_native_driver_clean_boot_removes_previous_prefixed_resources(tmp_path): +def test_native_driver_refuses_prefix_wide_cleanup(tmp_path): connection = _FakeConnection() old_domain = _NativeObject("native-test-old-domain") old_network = _NativeObject("native-test-old-network") @@ -203,22 +858,57 @@ def test_native_driver_clean_boot_removes_previous_prefixed_resources(tmp_path): connection.networks[old_network.name()] = old_network kernel = tmp_path / "vmlinuz" kernel.write_bytes(b"kernel") + with pytest.raises(ValueError, match="prefix-wide cleanup"): + TechVaultNativeLibvirtDriver( + state_dir=tmp_path / "state", + connection=connection, + kernel_path=kernel, + name_prefix="native-test", + initramfs_builder=_Builder(), + clean_existing=True, + ) + + assert old_domain.destroyed is False + assert old_network.destroyed is False + + +@pytest.mark.parametrize( + ("kwargs", "message"), + ( + ({"define_only": True}, "define-only"), + ({"connection_uri": "qemu+ssh://operator:credential@example/system"}, "credentials"), + ({"connection_uri": "qemu+ssh://operator@example/system"}, "credentials"), + ({"name_prefix": "unsafe prefix"}, "libvirt-safe"), + ), +) +def test_native_driver_rejects_unbound_material_or_secret_configuration(tmp_path, kwargs, message): + with pytest.raises(ValueError, match=message): + TechVaultNativeLibvirtDriver(state_dir=tmp_path / "state", **kwargs) + + +def test_native_driver_derives_provider_name_from_address_not_display_name(tmp_path): + connection = _FakeConnection() + kernel = tmp_path / "vmlinuz" + kernel.write_bytes(b"kernel") driver = TechVaultNativeLibvirtDriver( state_dir=tmp_path / "state", connection=connection, kernel_path=kernel, - name_prefix="native-test", initramfs_builder=_Builder(), - clean_existing=True, + ) + domain = DomainSpec( + address="provision.node.demo", + name="unsafe name", + image_ref=None, + memory_mib=128, + vcpus=1, ) - result = driver.realize(networks=(), domains=()) + result = driver.realize(networks=(), domains=(domain,)) assert not result.diagnostics - assert old_domain.destroyed is True - assert old_domain.undefined is True - assert old_network.destroyed is True - assert old_network.undefined is True + assert provider_resource_name(domain.address, prefix="aces-techvault") in connection.domain_xml[0] + assert "unsafe name" not in connection.domain_xml[0] def test_busybox_initramfs_builder_writes_gzip_cpio(tmp_path): @@ -233,3 +923,4 @@ def test_busybox_initramfs_builder_writes_gzip_cpio(tmp_path): assert target.read_bytes().startswith(b"\x1f\x8b") assert target.stat().st_size > 1000 + assert b"httpd -p" not in gzip.decompress(target.read_bytes()) diff --git a/implementations/python/tests/test_libvirt_backend_techvault_real_libvirt.py b/implementations/python/tests/test_libvirt_backend_techvault_real_libvirt.py new file mode 100644 index 000000000..6f0217184 --- /dev/null +++ b/implementations/python/tests/test_libvirt_backend_techvault_real_libvirt.py @@ -0,0 +1,52 @@ +"""Opt-in real-libvirt certification for the bounded TechVault substrate.""" + +from __future__ import annotations + +import importlib +import json +import os +import shutil +from pathlib import Path + +import pytest +from aces_operations.techvault_live import TechVaultLiveConfig, validate_techvault_live +from paths import EXAMPLES_DIR + + +@pytest.mark.integration +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") + if not connection_uri: + pytest.skip("set ACES_REAL_LIBVIRT_URI to run real-libvirt certification") + try: + libvirt = importlib.import_module("libvirt") + except ImportError: + pytest.skip("libvirt-python is unavailable") + if shutil.which("cpio") is None or not Path("/usr/bin/busybox").is_file(): + pytest.skip("cpio and static BusyBox are required for native appliance certification") + if not tuple(Path("/boot").glob("vmlinuz-*")): + pytest.skip("a readable host kernel is required for native appliance certification") + + report = validate_techvault_live( + scenario_path=EXAMPLES_DIR / "techvault-bounded-native.sdl.yaml", + project_dir=tmp_path, + run_id="real-libvirt-certification", + config=TechVaultLiveConfig(connection_uri=connection_uri), + ) + + assert report.passed, report.render() + assert report.manifest_path is not None + manifest = json.loads(Path(report.manifest_path).read_text(encoding="utf-8")) + assert manifest["cleanup"] == {"source": "driver-reported", "status": "verified"} + observed = manifest["realization_facts"]["daemon_observed"] + connection = libvirt.open(connection_uri) + assert connection is not None + try: + remaining_domains = {item.name() for item in connection.listAllDomains()} + remaining_networks = {item.name() for item in connection.listAllNetworks()} + finally: + connection.close() + assert remaining_domains.isdisjoint(observed["domains"]) + assert remaining_networks.isdisjoint(observed["networks"]) diff --git a/implementations/python/tests/test_libvirt_conformance.py b/implementations/python/tests/test_libvirt_conformance.py index d827a224a..dafd55150 100644 --- a/implementations/python/tests/test_libvirt_conformance.py +++ b/implementations/python/tests/test_libvirt_conformance.py @@ -1,4 +1,4 @@ -"""Issue #606: libvirt backend conformance (fixture + live target). +"""Issue #606: libvirt backend conformance (fixture + target adapter). Acceptance bar: @@ -6,13 +6,12 @@ ``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). -2. ``run_target_conformance`` against the libvirt target passes a real +2. ``run_target_conformance`` against the libvirt target passes a target *provisioning probe* and asserts *snapshot mutation* -- not manifest / - contract-surface only. The probe drives ``RuntimeControlPlane`` and proves - the snapshot gained provisioning entries. + contract-surface only. This is adapter evidence, not daemon or guest proof. 3. A conformance report is captured and committed (drift-guarded here). -The live probe runs daemon-free through an injected ``RecordingLibvirtDriver`` +The target probe runs daemon-free through an injected ``RecordingLibvirtDriver`` that confirms realization, so the real ``LibvirtProvisioner`` path is exercised without a libvirt/QEMU daemon. """ @@ -106,7 +105,7 @@ def test_provisioning_only_fixture_suite_has_no_unsupported_diagnostics(): # --------------------------------------------------------------------------- -# AC2: live provisioning probe + real snapshot mutation +# AC2: target provisioning probe + snapshot mutation # --------------------------------------------------------------------------- @@ -121,9 +120,9 @@ def test_provisioning_only_conformance_runs_live_provisioning_probe(): case_names = {case.name for case in report.cases} # Not manifest/contract-surface only: the probe must actually provision and # validate a mutated snapshot. - assert {"live-manifest", "live-provisioning", "live-snapshot"} <= case_names + assert {"target-manifest", "target-provisioning", "target-snapshot"} <= case_names for case in report.cases: - if case.name in {"live-manifest", "live-provisioning", "live-snapshot"}: + if case.name in {"target-manifest", "target-provisioning", "target-snapshot"}: assert case.passed, [diag.message for diag in case.diagnostics] @@ -150,17 +149,17 @@ def test_libvirt_provisioning_mutates_snapshot(): def test_provisioning_only_conformance_requires_confirmed_realization(): - """A driver that does not confirm realization must fail the live probe. + """A driver that does not confirm realization must fail the target probe. Guards the backend-neutral anti-pattern: provisioning-only conformance must - not pass on ``live-manifest`` alone, and must not accept an empty snapshot. + not pass on ``target-manifest`` alone, and must not accept an empty snapshot. """ report = run_target_conformance(create_libvirt_target(driver=NullLibvirtDriver())) assert report.passed is False - live_provisioning = next((case for case in report.cases if case.name == "live-provisioning"), None) - assert live_provisioning is not None, "provisioning-only conformance must run a live-provisioning probe" + live_provisioning = next((case for case in report.cases if case.name == "target-provisioning"), None) + assert live_provisioning is not None, "provisioning-only conformance must run a target-provisioning probe" assert live_provisioning.passed is False diff --git a/implementations/python/tests/test_libvirt_evidence_run.py b/implementations/python/tests/test_libvirt_evidence_run.py index 66fa57c03..18dba1e98 100644 --- a/implementations/python/tests/test_libvirt_evidence_run.py +++ b/implementations/python/tests/test_libvirt_evidence_run.py @@ -10,10 +10,12 @@ from __future__ import annotations import json +import xml.etree.ElementTree as ET from pathlib import Path import pytest -from aces_backend_libvirt.techvault_native import ProbeResult, TechVaultNativeLibvirtDriver +from aces_backend_libvirt.techvault_native import TechVaultNativeLibvirtDriver +from aces_backend_protocols.naming import provider_resource_name from aces_contracts.contracts import ( BackendManifestV2Model, EvaluationHistoryEventModel, @@ -40,6 +42,7 @@ "scenario", "compiled_artifact", "backend", + "realization_facts", "realized_topology", "participant_action_proof", "terminal_observation", @@ -58,20 +61,32 @@ class _NativeObject: - def __init__(self, name: str = "") -> None: + def __init__(self, name: str = "", xml: str = "") -> None: self._name = name + self._xml = xml + self._active = False + self._undefined = False def name(self) -> str: return self._name def create(self) -> None: # pragma: no cover - structural stub - pass + self._active = True def destroy(self) -> None: # pragma: no cover - structural stub - pass + self._active = False def undefine(self) -> None: # pragma: no cover - structural stub - pass + self._undefined = True + + def isActive(self): # noqa: N802 - mirrors libvirt API + return int(self._active) + + def XMLDesc(self, _flags=0): # noqa: N802 - mirrors libvirt API + return self._xml + + def UUIDString(self): # noqa: N802 - mirrors libvirt API + return ET.fromstring(self._xml).findtext("uuid") # noqa: S314 - test-generated XML def _name_from_xml(xml: str) -> str: @@ -84,12 +99,12 @@ def __init__(self) -> None: self.domains: dict[str, _NativeObject] = {} def networkDefineXML(self, xml: str): # noqa: N802 - mirrors libvirt API - obj = _NativeObject(_name_from_xml(xml)) + obj = _NativeObject(_name_from_xml(xml), xml) self.networks[obj.name()] = obj return obj def defineXML(self, xml: str): # noqa: N802 - mirrors libvirt API - obj = _NativeObject(_name_from_xml(xml)) + obj = _NativeObject(_name_from_xml(xml), xml) self.domains[obj.name()] = obj return obj @@ -100,10 +115,10 @@ def lookupByName(self, name: str): # noqa: N802 - mirrors libvirt API return self.domains[name] def listAllDomains(self): # noqa: N802 - mirrors libvirt API - return list(self.domains.values()) + return [item for item in self.domains.values() if not item._undefined] def listAllNetworks(self): # noqa: N802 - mirrors libvirt API - return list(self.networks.values()) + return [item for item in self.networks.values() if not item._undefined] class _InitramfsBuilder: @@ -113,14 +128,6 @@ def build(self, *, domain, target: Path): return target -class _Probe: - def ping(self, ip: str): - return ProbeResult(True) - - def tcp(self, ip: str, port: int): - return ProbeResult(True) - - def _native_driver_factory(tmp_path: Path): kernel = tmp_path / "vmlinuz" kernel.write_bytes(b"kernel") @@ -137,6 +144,30 @@ def factory() -> TechVaultNativeLibvirtDriver: return factory +def _bounded_scenario(tmp_path: Path) -> Path: + scenario = tmp_path / "bounded-techvault.sdl.yaml" + scenario.write_text( + """\ +name: bounded-techvault +nodes: + lab: + type: switch + demo: + type: vm + os: linux + resources: {ram: 128 MiB, cpu: 1} + services: [] +infrastructure: + lab: + properties: {cidr: 192.0.2.0/24, gateway: 192.0.2.1, internal: true} + demo: + links: [lab] +""", + encoding="utf-8", + ) + return scenario + + # --- deterministic mode -------------------------------------------------------- @@ -280,59 +311,134 @@ def test_validator_flags_participant_boundary_exposure(tmp_path): assert any("boundary violation" in p for p in problems) +def test_validator_rejects_nonempty_domain_list_as_daemon_observation(tmp_path): + artifact = run_libvirt_evidence_run( + scenario_path=_REFERENCE_SCENARIO, + project_dir=tmp_path, + run_id="evidence-source-mutation-1", + ).artifact + envelope = artifact["backend"]["manifest"]["realization_envelope"] + artifact["backend"]["realization_provenance"].update( + {"substrate_realized": True, "basis": "daemon-observed-substrate"} + ) + artifact["realized_topology"]["basis"] = "mixed-source" + artifact["realization_facts"]["daemon_observed"]["domains"] = [ + {"name": "fabricated", "observation_source": "daemon-observed"} + ] + artifact["realization_facts"]["binding"] = { + "driver": "techvault-appliance", + "realization_envelope_digest": envelope["digest"], + "configuration_digest": envelope["configuration_digest"], + "driver_configuration_digest": "sha256:" + "0" * 64, + "boot_artifact_digests": {}, + } + + problems = validate_libvirt_evidence_run_artifact(artifact) + + assert any("incomplete daemon domain observation" in problem for problem in problems) + + +def test_validator_rejects_planned_topology_relabelled_as_observed(tmp_path): + artifact = run_libvirt_evidence_run( + scenario_path=_REFERENCE_SCENARIO, + project_dir=tmp_path, + run_id="evidence-source-mutation-2", + ).artifact + artifact["realized_topology"]["nodes"][0]["source"] = "daemon-observed" + + problems = validate_libvirt_evidence_run_artifact(artifact) + + assert any("realized_topology.nodes is planned" in problem for problem in problems) + + +def test_validator_rejects_native_realized_label_and_fabricated_soc_readback(tmp_path): + artifact = run_libvirt_evidence_run( + scenario_path=_REFERENCE_SCENARIO, + project_dir=tmp_path, + run_id="evidence-source-mutation-3", + ).artifact + artifact["realized_topology"]["basis"] = "native-realized" + artifact["defensive_evidence"]["soc_readback"] = {"wazuh_active_agents": ["fabricated"]} + + problems = validate_libvirt_evidence_run_artifact(artifact) + + assert any("native-realized" in problem for problem in problems) + assert any("cannot supply SOC readback" in problem for problem in problems) + + +def test_validator_rejects_mismatched_realization_binding(tmp_path): + artifact = run_libvirt_evidence_run( + scenario_path=_bounded_scenario(tmp_path), + project_dir=tmp_path, + run_id="evidence-source-mutation-4", + config=LibvirtEvidenceRunConfig(evidence_source_mode="native-live"), + driver_factory=_native_driver_factory(tmp_path), + ).artifact + artifact["realization_facts"]["binding"]["realization_envelope_digest"] = "sha256:" + "f" * 64 + + problems = validate_libvirt_evidence_run_artifact(artifact) + + assert any("envelope digest does not match" in problem for problem in problems) + + # --- native-live mode ---------------------------------------------------------- -def test_native_live_reference_scenario_realizes_content_plane(tmp_path): +def test_native_live_reference_scenario_discloses_unrealized_content_plane(tmp_path): report = run_libvirt_evidence_run( scenario_path=_REFERENCE_SCENARIO, project_dir=tmp_path, run_id="evidence-live-1", config=LibvirtEvidenceRunConfig(evidence_source_mode="native-live"), driver_factory=_native_driver_factory(tmp_path), - probe=_Probe(), ) - # Issue #603: the libvirt backend now realizes the reference scenario's content and - # account placements through cloud-init, so native-live realizes the provisioning - # substrate. The disclosure remains honest: only the orchestration/evaluation - # planes (outside a provisioning-only target) are still surfaced as unrealized — - # no content/account provisioning capability is disclosed as unrealized anymore. - assert report.passed, report.render() + # ASR-519: the TechVault appliance driver does not consume the generic cloud-init + # content/account surfaces. A native domain must not hide that gap. + assert report.passed is False artifact = report.artifact assert artifact is not None assert validate_libvirt_evidence_run_artifact(artifact) == [] provenance = artifact["backend"]["realization_provenance"] - assert provenance["substrate_realized"] is True + assert provenance["substrate_realized"] is False unrealized = artifact["realized_topology"]["unrealized_capabilities"] assert unrealized, "orchestration/evaluation planes must still be disclosed, not faked" - assert all(cap.split(".", 1)[0] in {"orchestrator", "evaluator", "evaluation"} for cap in unrealized), unrealized - assert not any("content" in cap.lower() or "account" in cap.lower() for cap in unrealized) + assert any("content" in cap.lower() or "account" in cap.lower() for cap in unrealized) def test_native_live_realizes_substrate_for_provisionable_scenario(tmp_path): report = run_libvirt_evidence_run( - scenario_path=_TECHVAULT_SCENARIO, + scenario_path=_bounded_scenario(tmp_path), project_dir=tmp_path, run_id="tv-live-1", config=LibvirtEvidenceRunConfig(evidence_source_mode="native-live"), driver_factory=_native_driver_factory(tmp_path), - probe=_Probe(), ) - # This is the only native-live success path: real domain/network snapshot data - # flows through artifact assembly, so it must clear the full check set and the - # redaction/contract validator (the path most able to leak host-private data). assert report.passed, report.render() artifact = report.artifact assert validate_libvirt_evidence_run_artifact(artifact) == [] assert artifact["backend"]["realization_provenance"]["substrate_realized"] is True + assert artifact["backend"]["realization_provenance"]["basis"] == "daemon-observed-substrate" + assert artifact["backend"]["realization_provenance"]["cleanup_verified"] is True + assert any(check.name == "native_substrate_cleanup" and check.passed for check in report.checks) + assert artifact["realized_topology"]["basis"] == "mixed-source" native_surface = artifact["realized_topology"]["native_surface"] - assert len(native_surface["domains"]) == 30 - assert len(native_surface["networks"]) == 4 - # Native SOC readback is the translated native readback, explicitly disclosed. + assert native_surface["source"] == "daemon-observed" + assert native_surface["domains"] == (provider_resource_name("provision.node.demo", prefix="evidence-test"),) + assert native_surface["networks"] == (provider_resource_name("provision.network.lab", prefix="evidence-test"),) + facts = artifact["realization_facts"] + assert facts["planned"]["source"] == "planned" + assert facts["driver_reported"]["source"] == "driver-reported" + assert facts["daemon_observed"]["source"] == "daemon-observed" + assert facts["guest_observed"] == {"source": "guest-observed", "status": "not-observed"} + assert ( + facts["binding"]["realization_envelope_digest"] + == artifact["backend"]["manifest"]["realization_envelope"]["digest"] + ) defensive = artifact["defensive_evidence"] - assert defensive["evidence_source"] == "native-translated-readback" - assert "soc_readback" in defensive + assert defensive["evidence_source"] == "structural-evaluator-channel" + assert "soc_readback" not in defensive assert defensive["captured_at"] == artifact["recorded_at"] + assert "native_reachability" not in artifact["negative_boundary_checks"] def test_native_live_without_realized_substrate_fails(tmp_path): diff --git a/implementations/python/tests/test_libvirt_participant_runtime.py b/implementations/python/tests/test_libvirt_participant_runtime.py index eb9f561f5..71bd422eb 100644 --- a/implementations/python/tests/test_libvirt_participant_runtime.py +++ b/implementations/python/tests/test_libvirt_participant_runtime.py @@ -110,9 +110,9 @@ def test_ac2_conformance_passes_with_participant_runtime_manifest(): # pipeline actually ran end-to-end for the participant-runtime manifest: # the provisioning probe + snapshot-mutation cases must be present and green. case_names = {case.name for case in report.cases} - assert {"live-manifest", "live-provisioning", "live-snapshot"} <= case_names + assert {"target-manifest", "target-provisioning", "target-snapshot"} <= case_names for case in report.cases: - if case.name in {"live-manifest", "live-provisioning", "live-snapshot"}: + if case.name in {"target-manifest", "target-provisioning", "target-snapshot"}: assert case.passed, [diag.message for diag in case.diagnostics] diff --git a/implementations/python/tests/test_mcp_server.py b/implementations/python/tests/test_mcp_server.py index be979d95c..2de04c535 100644 --- a/implementations/python/tests/test_mcp_server.py +++ b/implementations/python/tests/test_mcp_server.py @@ -83,9 +83,45 @@ def _json_call(server, tool: str, args: dict | None = None) -> dict: conditions: alive: + proposition: web-alive command: "curl -sf http://localhost/ || exit 1" interval: 15 +evidence_requirements: + web-health-evidence: + description: Capture the governed web health observation. + source_refs: [nodes.web] + scope_refs: [nodes.web] + trigger_ref: conditions.alive + channel: api_response + artifact_role: proposition_truth_evidence + media_types: [application/json] + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required + +propositions: + web-alive: + description: The governed web service responds successfully. + subjects: [nodes.web] + basis: observed_state + predicate: + kind: boolean + property: service-alive + semantic_ref: urn:aces:observable:service-alive + operator: equals + expected: true + evidence_requirements: [web-health-evidence] + +assertions: + web-alive: + description: The web service must be alive at the objective boundary. + proposition: web-alive + role: postcondition + polarity: positive + vulnerabilities: sqli: {name: SQL Injection, description: "SQLi in login", technical: true, class: CWE-89} @@ -107,8 +143,8 @@ def _json_call(server, tool: str, args: dict | None = None) -> dict: scripts: timeline: - start-time: 0 - end-time: 2 hour + start_time: 0 + end_time: 2 hour speed: "${speed}" events: attack: 30 min @@ -141,14 +177,14 @@ def _json_call(server, tool: str, args: dict | None = None) -> dict: agent: red-agent actions: [Scan] targets: [web] - success: {conditions: [alive]} + success: {assertions: [web-alive]} window: {stories: [exercise]} workflows: flow: start: do-it steps: - do-it: {type: objective, objective: red-access, on-success: done} + do-it: {type: objective, objective: red-access, on_success: done} done: {type: end} """ @@ -172,12 +208,26 @@ class TestReferenceTools: def test_sdl_overview_returns_content(self, server): text = _call(server, "sdl_overview") assert "SDL" in text - # Both pieces of evidence must be present; an OR disjunction over - # "17" / "sections" would let either drift go undetected. - assert "17" in text - assert "sections" in text.lower() + assert "authoring sections" in text.lower() + assert "17 sections" not in text.lower() assert "nodes" in text + @pytest.mark.parametrize( + "section", + [ + "forwarding_agents", + "action_contracts", + "observation_boundaries", + "outcome_interpretation_rules", + "behavior_specifications", + "evidence_requirements", + ], + ) + def test_sdl_section_reference_covers_live_sections(self, server, section): + text = _call(server, "sdl_section_reference", {"section": section}) + assert "Unknown section" not in text + assert "not found" not in text + def test_sdl_section_reference_valid(self, server): text = _call(server, "sdl_section_reference", {"section": "nodes"}) assert "Nodes" in text @@ -253,6 +303,19 @@ def test_validate_structural_only(self, server): assert "semantic validation was skipped" in text assert "ghost-feature" not in text + def test_validate_requires_explicit_migration_policy(self, server): + strict = _call(server, "sdl_validate", {"sdl_content": "Name: migrated\n"}) + assert "sdl.noncanonical_field" in strict + + migrated = _call( + server, + "sdl_validate", + {"sdl_content": "Name: migrated\n", "accept_migration_syntax": True}, + ) + assert migrated.startswith("VALID") + assert "Source migration advisories (1)" in migrated + assert "sdl.noncanonical_field" in migrated + def test_validate_section_valid(self, server): text = _call( server, @@ -272,6 +335,21 @@ def test_validate_section_invalid_yaml(self, server): ) assert "YAML ERROR" in text + def test_validate_section_rejects_duplicates_before_fragment_round_trip(self, server): + text = _call( + server, + "sdl_validate_section", + { + "section": "nodes", + "section_yaml": "sw:\n Type: switch\n type: switch", + }, + ) + + assert "PARSE ERROR" in text + assert "sdl.mapping_key_conflict" in text + assert "/nodes/sw/type" in text + assert "line 3, column 3" in text + def test_validate_section_bad_section(self, server): text = _call( server, @@ -306,18 +384,22 @@ def test_instantiate(self, server): variables: greeting: type: string - default: hello + required: true """ text = _call( server, "sdl_instantiate", - {"sdl_content": sdl, "parameters_json": '{"greeting": "hi"}'}, + {"sdl_content": sdl, "parameters_json": '{"greeting": "s3cr3t-marker-9b2d"}'}, ) assert text.startswith("INSTANTIATED") - # Verify the supplied parameter actually substituted into the resolved - # scenario rather than the default being silently retained. - assert "'greeting': 'hi'" in text - assert "'greeting': 'hello'" not in text + assert "Bindings resolved: 1" in text + assert "greeting" not in text + assert "s3cr3t-marker-9b2d" not in text + + missing = _call(server, "sdl_instantiate", {"sdl_content": sdl}) + assert missing.startswith("INSTANTIATION ERRORS") + assert "required" in missing + assert "s3cr3t-marker-9b2d" not in missing def test_instantiate_bad_json(self, server): text = _call( @@ -363,8 +445,13 @@ def test_format_returns_normalized_yaml(self, server): {"sdl_content": "Name: x\nNodes:\n sw: {Type: Switch}\n"}, ) - assert payload["status"] == "formatted" + assert payload["status"] == "formatted_with_diagnostics" assert payload["content"].startswith("name: x\nnodes:\n") + assert [item["code"] for item in payload["diagnostics"]] == [ + "sdl.noncanonical_field", + "sdl.noncanonical_field", + "sdl.noncanonical_field", + ] def test_diagnostics_return_structured_errors(self, server): payload = _json_call( @@ -588,9 +675,11 @@ def test_tool_surface_self_describes_boundaries(self, server): payload = _json_call(server, "aces_tool_surface") assert payload["surface"] == "aces-sdl" assert "aces_agent_guidance" in payload["recommended_workflow"] + assert "aces_intended_use_profiles" in payload["recommended_workflow"] assert "sdl_claims_assessment" in payload["recommended_workflow"] assert "sdl_completions" in payload["tool_families"]["language_service"] assert "aces_agent_guidance" in payload["tool_families"]["guidance"] + assert "aces_intended_use_profiles" in payload["tool_families"]["guidance"] 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): @@ -614,6 +703,50 @@ def test_agent_guidance_filters_by_audience(self, server): assert entries assert all("operator" in entry["audience"] for entry in entries) + def test_intended_use_profiles_exposes_canonical_catalog(self, server): + payload = _json_call(server, "aces_intended_use_profiles") + + assert payload["status"] == "ok" + assert payload["scope"] == "aces-delivery-capability" + assert payload["taxonomy_revision"] == "rev1" + assert len(payload["profiles"]) == 5 + assert payload["scenario_assessment"]["status"] == "not-assessed" + assert {item["profile_id"] for item in payload["profiles"]} == { + "valid-sdl-fragment", + "deployable-scenario-intent", + "participant-evaluation-scenario", + "controlled-experiment-scenario", + "reproducible-benchmark-study-input", + } + + def test_intended_use_profile_exposes_blockers_and_authoring_route(self, server): + payload = _json_call( + server, + "aces_intended_use_profiles", + {"profile_id": "controlled-experiment-scenario"}, + ) + + profile = payload["profile"] + assert profile["aces_delivery"]["complete"] is False + assert "behavioral-relation-taxonomy" in profile["aces_delivery"]["blocking_concern_ids"] + assert "experiment_validate" in profile["next_tools"] + required = {item["concern_id"]: item for item in profile["required_concerns"]} + assert required["behavioral-relation-taxonomy"]["status"] == "missing" + assert required["behavioral-relation-taxonomy"]["issue_refs"] == ["#747"] + assert payload["scenario_assessment"]["performed"] is False + + def test_intended_use_profile_rejects_unknown_id(self, server): + payload = _json_call(server, "aces_intended_use_profiles", {"profile_id": "attack-scenario"}) + + assert payload["status"] == "invalid" + assert payload["diagnostics"][0]["code"] == "aces.intended_use_profile.unknown" + assert "valid-sdl-fragment" in payload["diagnostics"][0]["available_profile_ids"] + + def test_non_experiment_profile_does_not_recommend_experiment_tools(self, server): + payload = _json_call(server, "aces_intended_use_profiles", {"profile_id": "valid-sdl-fragment"}) + + assert "experiment_validate" not in payload["profile"]["next_tools"] + def test_parse_returns_machine_readable_summary(self, server): payload = _json_call(server, "sdl_parse", {"sdl_content": MINIMAL_SDL}) assert payload["status"] == "parsed" @@ -631,6 +764,32 @@ def test_parse_can_run_semantic_validation(self, server): assert payload["stage"] == "semantic_validation" assert "ghost-feature" in payload["diagnostics"][0]["message"] + @pytest.mark.parametrize("tool", ["sdl_parse", "sdl_compile"]) + def test_operation_tools_require_explicit_migration_policy(self, server, tool): + strict = _json_call(server, tool, {"sdl_content": "Name: migrated\n"}) + assert strict["status"] == "invalid" + assert strict["diagnostics"][0]["code"] == "sdl.noncanonical_field" + + migrated = _json_call( + server, + tool, + {"sdl_content": "Name: migrated\n", "accept_migration_syntax": True}, + ) + assert migrated["status"] in {"parsed", "compiled"} + diagnostics = migrated.get("source_diagnostics", migrated["scenario"].get("source_diagnostics", [])) + assert diagnostics[0]["code"] == "sdl.noncanonical_field" + + @pytest.mark.parametrize("tool", ["sdl_parse", "sdl_compile"]) + def test_operation_tools_preserve_mapping_conflict_diagnostics(self, server, tool): + payload = _json_call(server, tool, {"sdl_content": "Name: first\nname: second\n"}) + + assert payload["status"] == "invalid" + diagnostic = payload["diagnostics"][0] + assert diagnostic["code"] == "sdl.mapping_key_conflict" + assert diagnostic["path"] == "/name" + assert diagnostic["authored_keys"] == ["Name", "name"] + assert diagnostic["range"]["start"] == {"line": 2, "column": 1} + def test_compile_summarizes_runtime_model(self, server): payload = _json_call(server, "sdl_compile", {"sdl_content": FULL_SDL}) assert payload["status"] == "compiled" diff --git a/implementations/python/tests/test_proposition_capabilities.py b/implementations/python/tests/test_proposition_capabilities.py new file mode 100644 index 000000000..1f55756c1 --- /dev/null +++ b/implementations/python/tests/test_proposition_capabilities.py @@ -0,0 +1,61 @@ +"""Backend capability declarations for proposition realization.""" + +from __future__ import annotations + +import pytest +from aces_backend_protocols.capabilities import EvaluatorCapabilities +from aces_reference_backend.manifest import create_reference_backend_manifest + + +def test_evaluator_declares_finite_truth_and_predicate_capabilities() -> None: + evaluator = EvaluatorCapabilities( + name="portable-evaluator", + supported_sections=frozenset({"propositions", "assertions", "objectives"}), + supported_predicate_families=frozenset({"presence", "boolean", "string", "number"}), + supported_quantifiers=frozenset({"all", "any", "at_least"}), + supported_truth_outcomes=frozenset({"true", "false", "unknown", "unsupported"}), + supported_evidence_channels=frozenset({"log", "api_response"}), + supported_time_domains=frozenset({"scenario_time"}), + preserves_binding_provenance=True, + ) + + assert evaluator.supported_truth_outcomes == frozenset({"true", "false", "unknown", "unsupported"}) + assert evaluator.preserves_binding_provenance is True + + +def test_evaluator_cannot_claim_proposition_support_without_full_outcome_domain() -> None: + with pytest.raises(ValueError, match="all portable truth outcomes"): + EvaluatorCapabilities( + name="boolean-only", + supported_sections=frozenset({"propositions", "assertions", "objectives"}), + supported_predicate_families=frozenset({"boolean"}), + supported_quantifiers=frozenset({"all"}), + supported_truth_outcomes=frozenset({"true", "false"}), + supported_evidence_channels=frozenset({"log"}), + supported_time_domains=frozenset({"scenario_time"}), + preserves_binding_provenance=True, + ) + + +def test_evaluator_cannot_claim_observed_truth_without_provenance() -> None: + with pytest.raises(ValueError, match="binding provenance"): + EvaluatorCapabilities( + name="opaque-evaluator", + supported_sections=frozenset({"propositions", "assertions", "objectives"}), + supported_predicate_families=frozenset({"string"}), + supported_quantifiers=frozenset({"all"}), + supported_truth_outcomes=frozenset({"true", "false", "unknown", "unsupported"}), + supported_evidence_channels=frozenset({"log"}), + supported_time_domains=frozenset({"scenario_time"}), + preserves_binding_provenance=False, + ) + + +def test_reference_backend_discloses_truth_realization_surface() -> None: + evaluator = create_reference_backend_manifest().evaluator + + assert evaluator is not None + assert {"propositions", "assertions", "objectives"} <= evaluator.supported_sections + assert evaluator.supported_predicate_families == frozenset({"presence", "boolean", "string", "number"}) + assert evaluator.supported_truth_outcomes == frozenset({"true", "false", "unknown", "unsupported"}) + assert evaluator.preserves_binding_provenance is True diff --git a/implementations/python/tests/test_proposition_semantics.py b/implementations/python/tests/test_proposition_semantics.py new file mode 100644 index 000000000..2457ed277 --- /dev/null +++ b/implementations/python/tests/test_proposition_semantics.py @@ -0,0 +1,411 @@ +"""Backend-neutral proposition and assertion semantics for issue #725.""" + +from __future__ import annotations + +import pytest +from aces_processor.compiler import compile_runtime_model +from aces_sdl._errors import SDLParseError, SDLValidationError +from aces_sdl.parser import parse_sdl +from aces_sdl.propositions import ( + Assertion, + AssertionPolarity, + AssertionRole, + BooleanPredicate, + NumericPredicate, + Proposition, + PropositionBasis, + StringPredicate, + SubjectQuantifier, + TruthCompositionMode, +) +from aces_sdl.semantics.propositions import ( + TruthValue, + compose_truth, + evaluate_assertion_polarity, + negate_truth, + quantify_subject_truth, +) +from pydantic import ValidationError + + +def test_proposition_is_inspectable_without_probe_execution() -> None: + proposition = Proposition.model_validate( + { + "description": "The addressed service reports its governed availability state as available.", + "subjects": ["nodes.web.services.http"], + "basis": "observed_state", + "predicate": { + "kind": "string", + "property": "service-availability", + "semantic_ref": "urn:aces:observable:service-availability", + "operator": "equals", + "expected": "available", + }, + "quantifier": "all", + "evidence_requirements": ["service-health-evidence"], + } + ) + + assert isinstance(proposition.predicate, StringPredicate) + assert proposition.predicate.expected == "available" + assert proposition.quantifier is SubjectQuantifier.ALL + assert proposition.model_dump(mode="json") == { + "description": "The addressed service reports its governed availability state as available.", + "subjects": ["nodes.web.services.http"], + "basis": "observed_state", + "predicate": { + "kind": "string", + "property": "service-availability", + "semantic_ref": "urn:aces:observable:service-availability", + "operator": "equals", + "expected": "available", + }, + "quantifier": "all", + "threshold": None, + "evidence_requirements": ["service-health-evidence"], + } + + +@pytest.mark.parametrize( + ("payload", "message"), + [ + ( + { + "description": "Observed property without evidence basis.", + "subjects": ["nodes.web"], + "basis": "observed_state", + "predicate": { + "kind": "boolean", + "property": "reachable", + "semantic_ref": "urn:aces:observable:reachable", + "operator": "equals", + "expected": True, + }, + }, + "observed-state propositions require at least one evidence requirement", + ), + ( + { + "description": "An impossible finite threshold.", + "subjects": ["nodes.web"], + "basis": "declared_state", + "predicate": { + "kind": "presence", + "property": "runtime", + "semantic_ref": "urn:aces:declared-property:runtime", + "operator": "exists", + }, + "quantifier": "at_least", + "threshold": 2, + }, + "threshold cannot exceed the finite subject count", + ), + ], +) +def test_proposition_rejects_semantically_incomplete_shapes(payload: dict[str, object], message: str) -> None: + with pytest.raises(ValidationError, match=message): + Proposition.model_validate(payload) + + +def test_predicate_families_are_closed_and_type_specific() -> None: + assert ( + BooleanPredicate.model_validate( + { + "kind": "boolean", + "property": "reachable", + "semantic_ref": "urn:aces:observable:reachable", + "operator": "equals", + "expected": False, + } + ).expected + is False + ) + assert ( + NumericPredicate.model_validate( + { + "kind": "number", + "property": "packet-loss", + "semantic_ref": "urn:aces:observable:packet-loss", + "operator": "less_than_or_equal", + "expected": 0.01, + "unit": "ratio", + "unit_semantic_ref": "urn:qudt:unit:UNITLESS", + } + ).expected + == 0.01 + ) + + with pytest.raises(ValidationError): + BooleanPredicate.model_validate( + { + "kind": "boolean", + "property": "reachable", + "semantic_ref": "urn:aces:observable:reachable", + "operator": "greater_than", + "expected": 1, + } + ) + with pytest.raises(ValidationError): + NumericPredicate.model_validate( + { + "kind": "number", + "property": "packet-loss", + "semantic_ref": "urn:aces:observable:packet-loss", + "operator": "matches", + "expected": ".*", + "unit": "ratio", + "unit_semantic_ref": "urn:qudt:unit:UNITLESS", + } + ) + + +@pytest.mark.parametrize("expected", [float("nan"), float("inf"), float("-inf")]) +def test_numeric_predicate_rejects_non_finite_operands(expected: float) -> None: + with pytest.raises(ValidationError, match="must be finite"): + NumericPredicate.model_validate( + { + "kind": "number", + "property": "packet-loss", + "semantic_ref": "urn:aces:observable:packet-loss", + "operator": "less_than_or_equal", + "expected": expected, + "unit": "ratio", + "unit_semantic_ref": "urn:qudt:unit:UNITLESS", + } + ) + + +def test_assertion_carries_role_and_polarity_without_redefining_proposition() -> None: + assertion = Assertion.model_validate( + { + "description": "The service must remain unavailable throughout the governed objective window.", + "proposition": "service-available", + "role": "invariant", + "polarity": "negative", + } + ) + + assert assertion.role is AssertionRole.INVARIANT + assert assertion.polarity is AssertionPolarity.NEGATIVE + assert assertion.proposition == "service-available" + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (TruthValue.TRUE, TruthValue.FALSE), + (TruthValue.FALSE, TruthValue.TRUE), + (TruthValue.UNKNOWN, TruthValue.UNKNOWN), + (TruthValue.UNSUPPORTED, TruthValue.UNSUPPORTED), + ], +) +def test_negation_preserves_indeterminate_and_unsupported_outcomes( + value: TruthValue, + expected: TruthValue, +) -> None: + assert negate_truth(value) is expected + assert evaluate_assertion_polarity(value, AssertionPolarity.NEGATIVE) is expected + + +def test_finite_subject_quantifiers_use_the_portable_truth_tables() -> None: + values = [TruthValue.TRUE, TruthValue.UNKNOWN, TruthValue.FALSE] + + assert quantify_subject_truth(values, quantifier=SubjectQuantifier.ALL) is TruthValue.FALSE + assert quantify_subject_truth(values, quantifier=SubjectQuantifier.ANY) is TruthValue.TRUE + assert quantify_subject_truth(values, quantifier=SubjectQuantifier.AT_LEAST, threshold=2) is TruthValue.UNKNOWN + + +@pytest.mark.parametrize( + ("mode", "values", "threshold", "expected"), + [ + (TruthCompositionMode.ALL_OF, [TruthValue.TRUE, TruthValue.TRUE], None, TruthValue.TRUE), + (TruthCompositionMode.ALL_OF, [TruthValue.TRUE, TruthValue.FALSE], None, TruthValue.FALSE), + (TruthCompositionMode.ALL_OF, [TruthValue.UNKNOWN, TruthValue.UNSUPPORTED], None, TruthValue.UNSUPPORTED), + (TruthCompositionMode.ANY_OF, [TruthValue.FALSE, TruthValue.FALSE], None, TruthValue.FALSE), + (TruthCompositionMode.ANY_OF, [TruthValue.UNKNOWN, TruthValue.TRUE], None, TruthValue.TRUE), + (TruthCompositionMode.ANY_OF, [TruthValue.UNKNOWN, TruthValue.UNSUPPORTED], None, TruthValue.UNSUPPORTED), + ( + TruthCompositionMode.AT_LEAST, + [TruthValue.TRUE, TruthValue.FALSE, TruthValue.TRUE], + 2, + TruthValue.TRUE, + ), + ( + TruthCompositionMode.AT_LEAST, + [TruthValue.TRUE, TruthValue.FALSE, TruthValue.FALSE], + 2, + TruthValue.FALSE, + ), + ( + TruthCompositionMode.AT_LEAST, + [TruthValue.TRUE, TruthValue.UNKNOWN, TruthValue.FALSE], + 2, + TruthValue.UNKNOWN, + ), + ( + TruthCompositionMode.AT_LEAST, + [TruthValue.TRUE, TruthValue.UNSUPPORTED, TruthValue.FALSE], + 2, + TruthValue.UNSUPPORTED, + ), + ], +) +def test_truth_composition_is_total_over_the_portable_outcome_domain( + mode: TruthCompositionMode, + values: list[TruthValue], + threshold: int | None, + expected: TruthValue, +) -> None: + assert compose_truth(values, mode=mode, threshold=threshold) is expected + + +def test_truth_composition_rejects_empty_or_invalid_thresholds() -> None: + with pytest.raises(ValueError, match="at least one truth value"): + compose_truth([], mode=TruthCompositionMode.ALL_OF) + with pytest.raises(ValueError, match="requires threshold"): + compose_truth([TruthValue.TRUE], mode=TruthCompositionMode.AT_LEAST) + with pytest.raises(ValueError, match="must not declare threshold"): + compose_truth([TruthValue.TRUE], mode=TruthCompositionMode.ALL_OF, threshold=1) + + +def test_declared_state_can_be_decided_without_an_evidence_capture_requirement() -> None: + proposition = Proposition( + description="The admitted SDL declares an HTTP service on the addressed node.", + subjects=["nodes.web"], + basis=PropositionBasis.DECLARED_STATE, + predicate={ + "kind": "presence", + "property": "services.http", + "semantic_ref": "urn:aces:declared-property:service", + "operator": "exists", + }, + ) + + assert proposition.evidence_requirements == [] + + +def _semantic_scenario(*, success_assertion: str = "service-ready") -> str: + return f""" +name: proposition-semantics +nodes: + web: + type: vm + resources: {{ram: 1 gib, cpu: 1}} + conditions: {{http-health: ops}} + roles: {{ops: operator}} +conditions: + http-health: + proposition: service-available + command: /usr/local/bin/check-http + interval: 15 +entities: + blue: + role: blue +evidence_requirements: + service-health-evidence: + description: Capture the declared service state at the objective boundary. + source_refs: [nodes.web] + scope_refs: [nodes.web] + boundary_kind: objective + channel: log + artifact_role: state_snapshot + media_types: [application/json] + sensitivity: plain + redaction: none + integrity: checksum + retention: study_lifetime + loss_disclosure: required +propositions: + service-available: + description: The addressed service reports availability as available. + subjects: [nodes.web] + basis: observed_state + predicate: + kind: string + property: service-availability + semantic_ref: urn:aces:observable:service-availability + operator: equals + expected: available + evidence_requirements: [service-health-evidence] +assertions: + service-ready: + description: The service is available at objective completion. + proposition: service-available + role: postcondition + polarity: positive + service-observed: + description: The service is available before the event fires. + proposition: service-available + role: precondition + polarity: positive +events: + notify-operator: + assertions: [service-observed] +objectives: + restore-service: + entity: blue + targets: [nodes.web] + success: + assertions: [{success_assertion}] +""" + + +def test_scenario_uses_assertions_for_backend_neutral_objective_success() -> None: + scenario = parse_sdl(_semantic_scenario()) + + assert scenario.conditions["http-health"].proposition == "service-available" + assert scenario.objectives["restore-service"].success.assertions == ["service-ready"] + assert scenario.assertions["service-ready"].role is AssertionRole.POSTCONDITION + assert scenario.events["notify-operator"].assertions == ["service-observed"] + + +def test_scenario_rejects_dangling_objective_assertion() -> None: + with pytest.raises(SDLValidationError, match="success assertion 'missing' not in assertions section"): + parse_sdl(_semantic_scenario(success_assertion="missing")) + + +def test_objective_success_rejects_precondition_assertion() -> None: + payload = _semantic_scenario().replace("role: postcondition", "role: precondition") + with pytest.raises(SDLValidationError, match="must be an invariant or postcondition"): + parse_sdl(payload) + + +def test_legacy_success_conditions_get_a_bounded_migration_error() -> None: + payload = _semantic_scenario().replace("assertions: [service-ready]", "conditions: [http-health]") + with pytest.raises(SDLParseError, match="success.conditions cannot state backend-neutral truth"): + parse_sdl(payload) + + +def test_event_trigger_rejects_non_precondition_assertion() -> None: + payload = _semantic_scenario().replace( + "assertions: [service-observed]", + "assertions: [service-ready]", + 1, + ) + with pytest.raises(SDLValidationError, match="event trigger must be a precondition"): + parse_sdl(payload) + + +def test_legacy_event_conditions_get_a_bounded_migration_error() -> None: + payload = _semantic_scenario().replace( + "assertions: [service-observed]", + "conditions: [http-health]", + 1, + ) + with pytest.raises(SDLParseError, match="event conditions cannot state backend-neutral truth"): + parse_sdl(payload) + + +def test_compiler_preserves_proposition_assertion_and_probe_binding_boundaries() -> None: + runtime = compile_runtime_model(parse_sdl(_semantic_scenario())) + + proposition = runtime.propositions["evaluation.proposition.service-available"] + assertion = runtime.assertions["evaluation.assertion.service-ready"] + binding = runtime.condition_bindings["evaluation.condition.web.http-health"] + objective = runtime.objectives["evaluation.objective.restore-service"] + + assert proposition.subject_addresses == ("provision.node.web",) + assert assertion.proposition_address == proposition.address + assert binding.proposition_address == proposition.address + assert objective.success_addresses == (assertion.address,) + assert assertion.ordering_dependencies == (proposition.address,) diff --git a/implementations/python/tests/test_realization_envelope_contract.py b/implementations/python/tests/test_realization_envelope_contract.py new file mode 100644 index 000000000..320e4d1cd --- /dev/null +++ b/implementations/python/tests/test_realization_envelope_contract.py @@ -0,0 +1,322 @@ +"""Published realization-envelope carrier and identity tests (ASR-519).""" + +from __future__ import annotations + +import json +from copy import deepcopy +from pathlib import Path + +import pytest +from aces_contracts.contracts import BackendManifestV2Model, ProvisioningPlanModel, RuntimeSnapshotEnvelopeModel +from aces_contracts.realization_envelope import ( + BackendRealizationEnvelopeModel, + ConcernDisposition, + ObservationStrength, + RealizationConcern, + RealizationConcernDisclosureModel, + RealizationEnvelopeIdentityModel, + RealizationEnvelopeModel, + RealizerConfigurationModel, + realization_envelope_digest, + realizer_configuration_digest, + validate_backend_realization_envelope, +) +from aces_contracts.runtime_state import RuntimeSnapshot +from aces_runtime.control_plane_store import LocalControlPlaneStore +from jsonschema import Draft202012Validator +from pydantic import ValidationError + + +def _payload() -> dict[str, object]: + payload: dict[str, object] = { + "schema_version": "realization-envelope/v1", + "contract_id": "realization-envelope-v1", + "id": "libvirt-qemu.generic.v1", + "expression": { + "id": "libvirt-qemu.generic.expression.v1", + "scope": "scenario", + "domains": {}, + "bindings": [], + "closure": [], + }, + "configuration": { + "mode": "generic", + "configuration_digest": "sha256:" + "1" * 64, + "architecture": "x86_64", + "image_policy": "local-qcow2", + "network_policy": "libvirt-managed", + "supported_node_types": ["switch", "vm"], + "supported_os_families": ["linux"], + "supported_content_types": ["file"], + "supported_account_features": ["groups"], + "supports_acls": True, + "memory_mib": {"minimum": 128, "maximum": None}, + "vcpus": {"minimum": 1, "maximum": None}, + }, + "concerns": [ + { + "concern": "topology", + "disposition": "realized", + "observation_strength": "driver-reported", + "mechanism": "libvirt-domain-network", + "transformations": [], + }, + { + "concern": "resource-allocation", + "disposition": "transformed", + "observation_strength": "driver-reported", + "mechanism": "libvirt-domain-xml", + "transformations": ["bounded-normalization"], + }, + *[ + { + "concern": concern, + "disposition": "unsupported", + "observation_strength": "none", + "mechanism": None, + "transformations": [], + } + for concern in ( + "architecture", + "image", + "network", + "content-placement", + "account-placement", + "feature-binding", + "service", + "acl", + ) + ], + ], + } + payload["configuration"]["configuration_digest"] = realizer_configuration_digest(payload["configuration"]) # type: ignore[index] + payload["digest"] = realization_envelope_digest(payload) + return payload + + +def test_backend_realization_envelope_validates_its_canonical_digest(): + model = BackendRealizationEnvelopeModel.model_validate(_payload()) + + assert model.expression == RealizationEnvelopeModel.model_validate(_payload()["expression"]) + assert model.configuration == RealizerConfigurationModel( + mode="generic", + configuration_digest=realizer_configuration_digest(_payload()["configuration"]), + architecture="x86_64", + image_policy="local-qcow2", + network_policy="libvirt-managed", + supported_node_types=["switch", "vm"], + supported_os_families=["linux"], + supported_content_types=["file"], + supported_account_features=["groups"], + supports_acls=True, + memory_mib={"minimum": 128, "maximum": None}, + vcpus={"minimum": 1, "maximum": None}, + ) + assert model.concerns[0] == RealizationConcernDisclosureModel( + concern="topology", + disposition=ConcernDisposition.REALIZED, + observation_strength=ObservationStrength.DRIVER_REPORTED, + mechanism="libvirt-domain-network", + ) + assert model.identity == RealizationEnvelopeIdentityModel( + contract_id="realization-envelope-v1", + envelope_id="libvirt-qemu.generic.v1", + schema_version="realization-envelope/v1", + digest=model.digest, + configuration_digest=model.configuration.configuration_digest, + ) + + +def test_realization_concern_taxonomy_accounts_for_declared_services(): + assert RealizationConcern.SERVICE.value == "service" + + +def test_backend_realization_envelope_rejects_content_tampering(): + payload = _payload() + payload["configuration"]["mode"] = "techvault-appliance" # type: ignore[index] + + with pytest.raises(ValidationError, match="digest does not match"): + BackendRealizationEnvelopeModel.model_validate(payload) + + +def test_backend_realization_envelope_rejects_duplicate_concerns(): + payload = _payload() + payload["concerns"] = [deepcopy(payload["concerns"][0]), deepcopy(payload["concerns"][0])] # type: ignore[index] + payload["digest"] = realization_envelope_digest(payload) + + with pytest.raises(ValidationError, match="concerns must not contain duplicate concern values"): + BackendRealizationEnvelopeModel.model_validate(payload) + + +def test_backend_realization_envelope_rejects_missing_concern_disclosure(): + payload = _payload() + payload["concerns"] = payload["concerns"][:-1] # type: ignore[index] + payload["digest"] = realization_envelope_digest(payload) + + with pytest.raises(ValidationError, match="must disclose every governed concern"): + BackendRealizationEnvelopeModel.model_validate(payload) + + +def test_published_schema_enforces_expressible_realization_invariants(): + schema = BackendRealizationEnvelopeModel.model_json_schema() + validator = Draft202012Validator(schema) + + duplicate_term = _payload() + duplicate_term["configuration"]["supported_node_types"] = ["vm", "vm"] # type: ignore[index] + assert list(validator.iter_errors(duplicate_term)) + + missing_concern = _payload() + missing_concern["concerns"] = missing_concern["concerns"][:-1] # type: ignore[index] + assert list(validator.iter_errors(missing_concern)) + + incoherent_disposition = _payload() + incoherent_disposition["concerns"][0]["transformations"] = ["default-substitution"] # type: ignore[index] + assert list(validator.iter_errors(incoherent_disposition)) + + +def test_published_schema_declares_callable_canonical_semantic_validator(): + schema = BackendRealizationEnvelopeModel.model_json_schema() + invariant = schema["x-aces-invariants"][0] + + assert invariant["validator"] == "aces_contracts.realization_envelope.validate_backend_realization_envelope" + with pytest.raises(ValidationError, match="digest does not match"): + validate_backend_realization_envelope({**_payload(), "digest": "sha256:" + "f" * 64}) + + +def test_transformed_concern_requires_a_transformation(): + with pytest.raises(ValidationError, match="transformed disposition requires transformations"): + RealizationConcernDisclosureModel( + concern="resource-allocation", + disposition="transformed", + observation_strength="driver-reported", + mechanism="libvirt-domain-xml", + ) + + +def test_unsupported_concern_cannot_claim_observation_or_mechanism(): + with pytest.raises(ValidationError, match="unsupported disposition"): + RealizationConcernDisclosureModel( + concern="content-placement", + disposition="unsupported", + observation_strength="driver-reported", + mechanism="descriptor-only", + ) + + +def test_identity_rejects_non_sha256_digests(): + with pytest.raises(ValidationError): + RealizationEnvelopeIdentityModel( + contract_id="realization-envelope-v1", + envelope_id="libvirt-qemu.generic.v1", + schema_version="realization-envelope/v1", + digest="sha256:short", + configuration_digest="sha256:" + "1" * 64, + ) + + +def test_manifest_plan_and_snapshot_publish_the_same_typed_identity(): + identity = BackendRealizationEnvelopeModel.model_validate(_payload()).identity.model_dump(mode="json") + manifest = BackendManifestV2Model.model_validate( + { + "identity": {"name": "test-backend", "version": "1.0.0"}, + "supported_contract_versions": [ + "backend-manifest-v2", + "realization-envelope-v1", + "provisioning-plan-v1", + "runtime-snapshot-v1", + ], + "compatibility": {"processors": ["aces-reference-processor"]}, + "realization_support": [ + { + "domain": "runtime-realization", + "support_mode": "constrained", + "supported_constraint_kinds": ["node-type"], + "supported_exact_requirement_kinds": ["declared-capability-match"], + "disclosure_kinds": ["runtime-snapshot-v1"], + } + ], + "concept_bindings": [{"scope": "capabilities.provisioner.supported_node_types", "family": "assets"}], + "capabilities": { + "provisioner": { + "name": "test", + "supported_node_types": ["vm"], + "supported_os_families": ["linux"], + } + }, + "realization_envelope": identity, + } + ) + plan = ProvisioningPlanModel(realization_envelope=identity) + snapshot = RuntimeSnapshotEnvelopeModel(realization_envelope=identity) + + assert manifest.realization_envelope == plan.realization_envelope == snapshot.realization_envelope + + +def test_local_control_plane_store_roundtrips_envelope_identity(tmp_path): + identity = BackendRealizationEnvelopeModel.model_validate(_payload()).identity + store = LocalControlPlaneStore(tmp_path / "store") + + store.save_snapshot(RuntimeSnapshot(realization_envelope=identity)) + + assert store.load_snapshot().realization_envelope == identity + + +def test_published_realization_envelope_fixture_corpus_is_nonvacuous(): + root = ( + Path(__file__).resolve().parents[3] + / "contracts" + / "fixtures" + / "realization-envelope" + / "realization-envelope-v1" + ) + valid = sorted((root / "valid").glob("*.json")) + invalid = sorted((root / "invalid").glob("*.json")) + + assert valid and invalid + for path in valid: + BackendRealizationEnvelopeModel.model_validate(json.loads(path.read_text(encoding="utf-8"))) + for path in invalid: + with pytest.raises(ValidationError): + BackendRealizationEnvelopeModel.model_validate(json.loads(path.read_text(encoding="utf-8"))) + + +def test_backend_manifest_requires_contract_declaration_for_envelope_identity(): + identity = BackendRealizationEnvelopeModel.model_validate(_payload()).identity + base = { + "identity": {"name": "test-backend", "version": "1.0.0"}, + "supported_contract_versions": ["backend-manifest-v2"], + "compatibility": {"processors": ["aces-reference-processor"]}, + "realization_support": [ + { + "domain": "runtime-realization", + "support_mode": "constrained", + "supported_constraint_kinds": ["node-type"], + "disclosure_kinds": ["runtime-snapshot-v1"], + } + ], + "concept_bindings": [{"scope": "capabilities.provisioner.supported_node_types", "family": "assets"}], + "capabilities": { + "provisioner": { + "name": "test", + "supported_node_types": ["vm"], + "supported_os_families": ["linux"], + } + }, + "realization_envelope": identity.model_dump(mode="json"), + } + + with pytest.raises(ValidationError, match="realization-envelope-v1"): + BackendManifestV2Model.model_validate(base) + + schema_validator = Draft202012Validator(BackendManifestV2Model.model_json_schema()) + explicit_null = {**base, "realization_envelope": None} + BackendManifestV2Model.model_validate(explicit_null) + schema_validator.validate(explicit_null) + + declared_with_null = { + **explicit_null, + "supported_contract_versions": ["backend-manifest-v2", "realization-envelope-v1"], + } + with pytest.raises(ValidationError, match="requires realization_envelope identity"): + BackendManifestV2Model.model_validate(declared_with_null) + assert list(schema_validator.iter_errors(declared_with_null)) diff --git a/implementations/python/tests/test_realization_envelope_relation.py b/implementations/python/tests/test_realization_envelope_relation.py new file mode 100644 index 000000000..4ed5349c3 --- /dev/null +++ b/implementations/python/tests/test_realization_envelope_relation.py @@ -0,0 +1,594 @@ +"""Tests for the realization-envelope relation (issue #668). + +Covers the envelope contract's construction invariants and the four relation +kinds — membership, subsumption, witness generation, and negative probes — against +``specs/formal/realization/envelope-semantics.md`` R1-R8 and ADR-070 §2/§3. Property +tests prove decidability/determinism, that subsumption is set inclusion on the +admitted fragment, that witnesses are in-envelope, and that negative probes are +out-of-envelope. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from aces_contracts.realization_envelope import ( + BooleanDomain, + Closure, + ClosureOverlay, + EnumDomain, + EnvelopeBinding, + EnvelopeScope, + ExactDomain, + GovernedReferenceDomain, + NumericIntervalDomain, + NumericType, + Posture, + RealizationEnvelopeModel, + RecordDomain, + WitnessPolicy, +) +from aces_sdl import ( + SDLInstantiationError, + SDLValidationError, + instantiate_scenario, + parse_sdl, +) +from aces_sdl._realization_envelope_domains import _MISSING, default_witness_value, out_of_domain_value +from aces_sdl.realization_envelope import generate_negative_probes, member, subsumes, witness +from aces_sdl.scenario import InstantiatedScenario, Scenario +from hypothesis import given, settings +from hypothesis import strategies as st +from pydantic import ValidationError + +_DATA = Path(__file__).parent / "data" / "realization_envelope" + + +# --------------------------------------------------------------------------- # +# Builders # +# --------------------------------------------------------------------------- # + + +def _web_envelope( + *, + os_values: tuple[str, ...] = ("linux", "windows"), + closed: bool = False, + name: str = "web-family", + selection: str | None = None, +) -> RealizationEnvelopeModel: + return RealizationEnvelopeModel( + id="web-family", + scope=EnvelopeScope.SCENARIO, + domains={ + "scenario_name": ExactDomain(value=name), + "node_type": ExactDomain(value="vm"), + "os": EnumDomain(values=list(os_values)), + }, + bindings=[ + EnvelopeBinding(path="name", scope=EnvelopeScope.SCENARIO, posture=Posture.EXACT, domain="scenario_name"), + EnvelopeBinding(path="nodes.web.type", scope=EnvelopeScope.NODE, posture=Posture.EXACT, domain="node_type"), + EnvelopeBinding(path="nodes.web.os", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="os"), + ], + closure=( + [ClosureOverlay(path="", scope=EnvelopeScope.SCENARIO, closure=Closure.CLOSED_WORLD)] if closed else [] + ), + witness_policy=(WitnessPolicy(selections={"os": selection}) if selection is not None else None), + ) + + +def _instantiate(yaml_text: str) -> InstantiatedScenario: + return instantiate_scenario(parse_sdl(yaml_text)) + + +def _leaf_enum_env(values: set[str], envelope_id: str = "leaf") -> RealizationEnvelopeModel: + """Single-leaf envelope over an arbitrary enum universe (no SDL binding).""" + + return RealizationEnvelopeModel( + id=envelope_id, + scope=EnvelopeScope.FIELD, + domains={"leaf": EnumDomain(values=sorted(values))}, + bindings=[EnvelopeBinding(path="x", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="leaf")], + ) + + +def _leaf_int_interval_env(lower: int, upper: int, envelope_id: str = "leaf") -> RealizationEnvelopeModel: + return RealizationEnvelopeModel( + id=envelope_id, + scope=EnvelopeScope.FIELD, + domains={"leaf": NumericIntervalDomain(numeric_type=NumericType.INTEGER, lower=lower, upper=upper)}, + bindings=[EnvelopeBinding(path="x", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="leaf")], + ) + + +# --------------------------------------------------------------------------- # +# Contract fixtures # +# --------------------------------------------------------------------------- # + + +@pytest.mark.parametrize("path", sorted((_DATA / "valid").glob("*.json")), ids=lambda p: p.stem) +def test_valid_fixtures_parse(path: Path) -> None: + model = RealizationEnvelopeModel.model_validate(json.loads(path.read_text(encoding="utf-8"))) + assert model.schema_version == "realization-envelope/v1" + # round-trips through JSON without loss + reparsed = RealizationEnvelopeModel.model_validate(json.loads(model.model_dump_json())) + assert reparsed == model + + +@pytest.mark.parametrize("path", sorted((_DATA / "invalid").glob("*.json")), ids=lambda p: p.stem) +def test_invalid_fixtures_rejected(path: Path) -> None: + with pytest.raises(ValidationError): + RealizationEnvelopeModel.model_validate(json.loads(path.read_text(encoding="utf-8"))) + + +def test_contract_rejects_unknown_field() -> None: + with pytest.raises(ValidationError): + RealizationEnvelopeModel.model_validate({"id": "x", "scope": "scenario", "unexpected": True}) + + +def test_contract_rejects_arbitrary_predicate_domain() -> None: + # The closed discriminated union is the portability guarantee: no predicate + # domain kind exists, so one cannot be expressed (envelope-semantics.md R3). + with pytest.raises(ValidationError): + RealizationEnvelopeModel.model_validate( + { + "id": "x", + "scope": "field", + "domains": {"p": {"kind": "predicate", "expr": "lambda v: True"}}, + "bindings": [], + } + ) + + +# --------------------------------------------------------------------------- # +# Membership (R1, R3) # +# --------------------------------------------------------------------------- # + + +def test_member_accepts_in_envelope_instance() -> None: + env = _web_envelope() + inst = _instantiate("name: web-family\nnodes:\n web:\n type: vm\n os: linux\n") + assert member(inst, env).holds + + +def test_member_rejects_out_of_domain_value() -> None: + env = _web_envelope(os_values=("linux",)) + inst = _instantiate("name: web-family\nnodes:\n web:\n type: vm\n os: windows\n") + result = member(inst, env) + assert not result.holds + assert any(d.code == "realization-envelope.membership.domain-mismatch" for d in result.diagnostics) + assert all(d.address == "nodes.web.os" for d in result.diagnostics) + + +def test_member_rejects_wrong_exact_name() -> None: + env = _web_envelope() + inst = _instantiate("name: other\nnodes:\n web:\n type: vm\n os: linux\n") + assert not member(inst, env).holds + + +def test_member_reports_absent_constrained_path() -> None: + env = _web_envelope() + inst = _instantiate("name: web-family\nnodes:\n web:\n type: vm\n") + result = member(inst, env) + assert not result.holds + assert any(d.code == "realization-envelope.membership.path-absent" for d in result.diagnostics) + + +def test_member_closed_world_rejects_unspecified_dimension() -> None: + env = _web_envelope(closed=True) + ok = _instantiate("name: web-family\nnodes:\n web:\n type: vm\n os: linux\n") + assert member(ok, env).holds + extra = _instantiate('name: web-family\nversion: "2.0"\nnodes:\n web:\n type: vm\n os: linux\n') + result = member(extra, env) + assert not result.holds + assert any(d.code == "realization-envelope.membership.closed-world-extra" for d in result.diagnostics) + + +def test_member_diagnostics_are_secret_free() -> None: + env = _web_envelope(os_values=("linux",)) + inst = _instantiate("name: web-family\nnodes:\n web:\n type: vm\n os: windows\n") + for diagnostic in member(inst, env).diagnostics: + # Diagnostics name paths, ids, and domain kinds — never raw values (R8). + assert "windows" not in diagnostic.message + assert diagnostic.domain == "realization-envelope" + + +def test_member_governed_reference_and_numeric_interval() -> None: + env = RealizationEnvelopeModel( + id="host", + scope=EnvelopeScope.NODE, + domains={ + "name": ExactDomain(value="s"), + "vm": ExactDomain(value="vm"), + "cpu": NumericIntervalDomain(numeric_type=NumericType.INTEGER, lower=1, upper=4), + }, + bindings=[ + EnvelopeBinding(path="name", scope=EnvelopeScope.SCENARIO, posture=Posture.EXACT, domain="name"), + EnvelopeBinding(path="nodes.h.type", scope=EnvelopeScope.NODE, posture=Posture.EXACT, domain="vm"), + EnvelopeBinding( + path="nodes.h.resources.cpu", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="cpu" + ), + ], + ) + within = _instantiate("name: s\nnodes:\n h:\n type: vm\n resources:\n ram: 2048\n cpu: 2\n") + outside = _instantiate("name: s\nnodes:\n h:\n type: vm\n resources:\n ram: 2048\n cpu: 9\n") + assert member(within, env).holds + assert not member(outside, env).holds + + +# --------------------------------------------------------------------------- # +# Subsumption (R4) # +# --------------------------------------------------------------------------- # + + +def test_subsumption_reflexive() -> None: + env = _web_envelope() + assert subsumes(env, env).holds + + +def test_subsumption_wider_offered_contains_narrower() -> None: + offered = _leaf_enum_env({"a", "b", "c"}) + requested = _leaf_enum_env({"a", "b"}) + assert subsumes(offered, requested).holds + assert not subsumes(requested, offered).holds + + +def test_subsumption_requested_open_where_offered_constrained() -> None: + offered = _leaf_enum_env({"a", "b"}) + requested = RealizationEnvelopeModel( + id="req", + scope=EnvelopeScope.FIELD, + domains={"leaf": EnumDomain(values=["a", "b"])}, + bindings=[EnvelopeBinding(path="x", scope=EnvelopeScope.FIELD, posture=Posture.OPEN)], + ) + result = subsumes(offered, requested) + assert not result.holds + assert any(d.code == "realization-envelope.subsumption.requested-unconstrained" for d in result.diagnostics) + + +def test_subsumption_closure_mismatch() -> None: + offered = _web_envelope(closed=True) + requested = _web_envelope(closed=False) + result = subsumes(offered, requested) + assert not result.holds + assert any(d.code == "realization-envelope.subsumption.closure-mismatch" for d in result.diagnostics) + + +def test_subsumption_numeric_interval_types() -> None: + number = RealizationEnvelopeModel( + id="num", + scope=EnvelopeScope.FIELD, + domains={"leaf": NumericIntervalDomain(numeric_type=NumericType.NUMBER, lower=0.0, upper=10.0)}, + bindings=[EnvelopeBinding(path="x", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="leaf")], + ) + integer = _leaf_int_interval_env(2, 5) + # An integer sub-interval fits inside a real super-interval... + assert subsumes(number, integer).holds + # ...but a real interval is not contained in an integer one (it has non-ints). + assert not subsumes(integer, number).holds + + +# --------------------------------------------------------------------------- # +# Witness (R5) # +# --------------------------------------------------------------------------- # + + +def test_witness_is_deterministic_and_in_envelope() -> None: + env = _web_envelope() + first = witness(env) + second = witness(env) + assert first.scenario is not None + assert second.scenario is not None + assert first.scenario.model_dump(mode="json") == second.scenario.model_dump(mode="json") + assert member(first.scenario, env).holds + + +def test_witness_default_selects_lexicographically_first_enum() -> None: + env = _web_envelope(os_values=("windows", "linux")) + result = witness(env) + assert result.scenario is not None + assert result.scenario.nodes["web"].os.value == "linux" + + +def test_witness_policy_overrides_default() -> None: + env = _web_envelope(selection="windows") + result = witness(env) + assert result.scenario is not None + assert result.scenario.nodes["web"].os.value == "windows" + + +def test_witness_under_specified_envelope_yields_diagnostic() -> None: + # Binds os but not the required scenario name/node type: no valid witness. + env = RealizationEnvelopeModel( + id="partial", + scope=EnvelopeScope.FIELD, + domains={"os": EnumDomain(values=["linux"])}, + bindings=[ + EnvelopeBinding(path="nodes.web.os", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="os") + ], + ) + result = witness(env) + assert result.scenario is None + assert result.diagnostics + assert result.diagnostics[0].code.startswith("realization-envelope.witness.") + + +def test_witness_rejects_list_indexed_path() -> None: + env = RealizationEnvelopeModel( + id="listy", + scope=EnvelopeScope.SCENARIO, + domains={"name": ExactDomain(value="s"), "v": ExactDomain(value="x")}, + bindings=[ + EnvelopeBinding(path="name", scope=EnvelopeScope.SCENARIO, posture=Posture.EXACT, domain="name"), + EnvelopeBinding( + path="forwarding_agents[0].id", scope=EnvelopeScope.FIELD, posture=Posture.EXACT, domain="v" + ), + ], + ) + result = witness(env) + assert result.scenario is None + assert any(d.code == "realization-envelope.witness.no-witness" for d in result.diagnostics) + + +# --------------------------------------------------------------------------- # +# Negative probes (R6) # +# --------------------------------------------------------------------------- # + + +def _probe_is_out_of_envelope(payload: dict, env: RealizationEnvelopeModel) -> bool: + try: + instance = instantiate_scenario(Scenario.model_validate(payload)) + except (ValidationError, SDLValidationError, SDLInstantiationError): + # invalid SDL is never a member; any other exception is a real bug and must propagate + return True + return not member(instance, env).holds + + +def test_negative_probes_are_all_out_of_envelope() -> None: + env = _web_envelope(closed=True) + probes, diagnostics = generate_negative_probes(env) + assert not diagnostics + assert probes + variations = {p.variation for p in probes} + assert "value-outside-domain" in variations + assert "extra-dimension" in variations + assert "omitted-required-exact" in variations + for probe in probes: + assert _probe_is_out_of_envelope(probe.payload, env), probe.path + + +def test_negative_probes_without_witness_report_diagnostic() -> None: + env = RealizationEnvelopeModel( + id="no-witness", + scope=EnvelopeScope.FIELD, + domains={"os": EnumDomain(values=["linux"])}, + bindings=[ + EnvelopeBinding(path="nodes.web.os", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="os") + ], + ) + probes, diagnostics = generate_negative_probes(env) + assert probes == () + assert any(d.code == "realization-envelope.negative-probe.no-witness" for d in diagnostics) + + +# --------------------------------------------------------------------------- # +# Property tests: decidability, determinism, set inclusion # +# --------------------------------------------------------------------------- # + +_UNIVERSE = ("a", "b", "c", "d") +_enum_subsets = st.sets(st.sampled_from(_UNIVERSE), min_size=1) + + +@settings(max_examples=200, deadline=None) +@given(offered=_enum_subsets, requested=_enum_subsets) +def test_property_subsumption_is_enum_set_inclusion(offered: set[str], requested: set[str]) -> None: + result = subsumes(_leaf_enum_env(offered, "off"), _leaf_enum_env(requested, "req")) + assert result.holds == (requested <= offered) + + +@settings(max_examples=100, deadline=None) +@given(a=_enum_subsets, b=_enum_subsets, c=_enum_subsets) +def test_property_subsumption_transitive(a: set[str], b: set[str], c: set[str]) -> None: + env_a, env_b, env_c = _leaf_enum_env(a, "a"), _leaf_enum_env(b, "b"), _leaf_enum_env(c, "c") + if subsumes(env_a, env_b).holds and subsumes(env_b, env_c).holds: + assert subsumes(env_a, env_c).holds + + +@settings(max_examples=100, deadline=None) +@given( + lo=st.integers(min_value=-20, max_value=20), + span=st.integers(min_value=0, max_value=20), + lo2=st.integers(min_value=-20, max_value=20), + span2=st.integers(min_value=0, max_value=20), +) +def test_property_integer_interval_subsumption_matches_int_ranges(lo: int, span: int, lo2: int, span2: int) -> None: + offered = _leaf_int_interval_env(lo, lo + span, "off") + requested = _leaf_int_interval_env(lo2, lo2 + span2, "req") + expected = set(range(lo2, lo2 + span2 + 1)) <= set(range(lo, lo + span + 1)) + assert subsumes(offered, requested).holds == expected + + +@settings(max_examples=50, deadline=None) +@given(os_values=st.sets(st.sampled_from(("linux", "windows")), min_size=1)) +def test_property_witness_is_member_and_deterministic(os_values: set[str]) -> None: + env = _web_envelope(os_values=tuple(sorted(os_values))) + first = witness(env) + second = witness(env) + assert first.scenario is not None + assert first.scenario.model_dump(mode="json") == second.scenario.model_dump(mode="json") + assert member(first.scenario, env).holds + + +@settings(max_examples=50, deadline=None) +@given(os_values=st.sets(st.sampled_from(("linux", "windows")), min_size=1)) +def test_property_negative_probes_out_of_envelope(os_values: set[str]) -> None: + env = _web_envelope(os_values=tuple(sorted(os_values)), closed=True) + probes, diagnostics = generate_negative_probes(env) + assert not diagnostics + for probe in probes: + assert _probe_is_out_of_envelope(probe.payload, env), probe.path + + +# --------------------------------------------------------------------------- # +# Record domains + open posture (R2) # +# --------------------------------------------------------------------------- # + + +def _record_node_envelope(*, open_os: bool = False, record_overrideable: bool = False) -> RealizationEnvelopeModel: + bindings = [ + EnvelopeBinding(path="name", scope=EnvelopeScope.SCENARIO, posture=Posture.EXACT, domain="name"), + EnvelopeBinding( + path="nodes.web", + scope=EnvelopeScope.NODE, + posture=Posture.CONSTRAINED, + domain="node", + overrideable=record_overrideable, + ), + ] + if open_os: + bindings.append(EnvelopeBinding(path="nodes.web.os", scope=EnvelopeScope.FIELD, posture=Posture.OPEN)) + return RealizationEnvelopeModel( + id="record-node", + scope=EnvelopeScope.SCENARIO, + domains={ + "name": ExactDomain(value="rec"), + "vm": ExactDomain(value="vm"), + "os": EnumDomain(values=["linux", "windows"]), + "node": RecordDomain(fields={"type": "vm", "os": "os"}, extra=False), + }, + bindings=bindings, + ) + + +def test_record_domain_membership_and_witness() -> None: + env = _record_node_envelope() + result = witness(env) + assert result.scenario is not None + assert member(result.scenario, env).holds + # A closed record rejects an undeclared field on the node. + extra = _instantiate('name: rec\nnodes:\n web:\n type: vm\n os: linux\n os_version: "9"\n') + rejected = member(extra, env) + assert not rejected.holds + assert any(d.code == "realization-envelope.membership.closed-world-extra" for d in rejected.diagnostics) + + +def test_open_posture_widens_overrideable_record_leaf() -> None: + # The record binding is marked overrideable, so opening os at the field scope + # legally widens the record's os constraint (most-specific-wins, R2). + env = _record_node_envelope(open_os=True, record_overrideable=True) + linux = _instantiate("name: rec\nnodes:\n web:\n type: vm\n os: linux\n") + windows = _instantiate("name: rec\nnodes:\n web:\n type: vm\n os: windows\n") + assert member(linux, env).holds + assert member(windows, env).holds + + +def test_widening_non_overrideable_inherited_value_is_invalid() -> None: + # Same shape but the record is NOT overrideable: opening os would widen a fixed + # inherited value, which R2 makes an ill-formed envelope. The relation denies. + env = _record_node_envelope(open_os=True, record_overrideable=False) + linux = _instantiate("name: rec\nnodes:\n web:\n type: vm\n os: linux\n") + result = member(linux, env) + assert not result.holds + assert any(d.code == "realization-envelope.invalid.non-overrideable-widen" for d in result.diagnostics) + witness_result = witness(env) + assert witness_result.scenario is None + assert any(d.code == "realization-envelope.invalid.non-overrideable-widen" for d in witness_result.diagnostics) + assert not subsumes(env, env).holds + + +def test_subsumption_over_governed_and_boolean_domains() -> None: + def governed(refs: list[str], name: str, authority: str = "reg") -> RealizationEnvelopeModel: + return RealizationEnvelopeModel( + id=name, + scope=EnvelopeScope.FIELD, + domains={"leaf": GovernedReferenceDomain(authority=authority, allowed_refs=refs)}, + bindings=[EnvelopeBinding(path="x", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="leaf")], + ) + + assert subsumes(governed(["a", "b", "c"], "o"), governed(["a"], "r")).holds + assert not subsumes(governed(["a"], "o"), governed(["a", "b"], "r")).holds + # Authority scopes the refs: overlapping ref strings under different authorities + # must NOT subsume (governed-reference authority boundary — codex security finding). + assert not subsumes( + governed(["a", "b"], "o", authority="registry-x"), + governed(["a"], "r", authority="registry-y"), + ).holds + + +def test_subsumption_governed_reference_rejects_cross_kind() -> None: + # A governed-reference domain and a raw enum with the same strings must not + # subsume in either direction: the enum values are not authority-scoped refs. + governed = RealizationEnvelopeModel( + id="g", + scope=EnvelopeScope.FIELD, + domains={"leaf": GovernedReferenceDomain(authority="reg", allowed_refs=["a", "b"])}, + bindings=[EnvelopeBinding(path="x", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="leaf")], + ) + enum = _leaf_enum_env({"a"}, "e") + assert not subsumes(governed, enum).holds + assert not subsumes(enum, governed).holds + + def boolean(value: bool | None, name: str) -> RealizationEnvelopeModel: + return RealizationEnvelopeModel( + id=name, + scope=EnvelopeScope.FIELD, + domains={"leaf": BooleanDomain(value=value)}, + bindings=[EnvelopeBinding(path="x", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="leaf")], + ) + + assert subsumes(boolean(None, "o"), boolean(True, "r")).holds + assert not subsumes(boolean(True, "o"), boolean(None, "r")).holds + + +# --------------------------------------------------------------------------- # +# Deterministic selection rules (R5 default policy / R6 variation) # +# --------------------------------------------------------------------------- # + + +def test_default_witness_selection_rules() -> None: + assert default_witness_value(ExactDomain(value=5)) == (5, None) + assert default_witness_value(EnumDomain(values=["b", "a", "c"])) == ("a", None) + assert default_witness_value(BooleanDomain()) == (False, None) + assert default_witness_value(BooleanDomain(value=True)) == (True, None) + assert default_witness_value(GovernedReferenceDomain(authority="r", allowed_refs=["z", "a"])) == ("a", None) + assert default_witness_value(NumericIntervalDomain(numeric_type=NumericType.INTEGER, lower=1, upper=8)) == ( + 1, + None, + ) + assert default_witness_value( + NumericIntervalDomain(numeric_type=NumericType.INTEGER, lower=1, upper=8, lower_closed=False) + ) == (2, None) + assert default_witness_value(NumericIntervalDomain(numeric_type=NumericType.NUMBER, lower=0.0, upper=1.0)) == ( + 0.0, + None, + ) + # An open-lower but bounded real interval is non-empty: pick the interior midpoint. + assert default_witness_value( + NumericIntervalDomain(numeric_type=NumericType.NUMBER, lower=0.0, upper=1.0, lower_closed=False) + ) == (0.5, None) + # An integer interval that admits no integer (open both ends, adjacent bounds) has no witness. + value, error = default_witness_value( + NumericIntervalDomain( + numeric_type=NumericType.INTEGER, lower=1, upper=2, lower_closed=False, upper_closed=False + ) + ) + assert value is None and error is not None + + +def test_out_of_domain_variation_rules() -> None: + assert out_of_domain_value(ExactDomain(value="x")) == "x-out-of-envelope" + assert out_of_domain_value(ExactDomain(value=3)) == 4 + assert out_of_domain_value(ExactDomain(value=True)) is False + assert out_of_domain_value(BooleanDomain(value=True)) is False + assert out_of_domain_value(BooleanDomain()) is _MISSING + assert out_of_domain_value(EnumDomain(values=["a", "b"])) not in {"a", "b"} + assert out_of_domain_value(EnumDomain(values=[1, 2])) == 3 + assert out_of_domain_value(GovernedReferenceDomain(authority="r", allowed_refs=["a", "b"])) not in {"a", "b"} + assert out_of_domain_value(NumericIntervalDomain(numeric_type=NumericType.INTEGER, lower=1, upper=4)) == 5 + assert ( + out_of_domain_value( + NumericIntervalDomain(numeric_type=NumericType.NUMBER, lower=0.0, upper=1.0, upper_closed=False) + ) + == 1.0 + ) diff --git a/implementations/python/tests/test_reference_backend_components.py b/implementations/python/tests/test_reference_backend_components.py index 3c88cef8c..ec27e8947 100644 --- a/implementations/python/tests/test_reference_backend_components.py +++ b/implementations/python/tests/test_reference_backend_components.py @@ -25,12 +25,27 @@ roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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 +assertions: + health: + proposition: health + role: postcondition + polarity: positive entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -38,7 +53,7 @@ run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """ @@ -54,6 +69,7 @@ def _control_plane(): def test_orchestrator_start_records_workflow_result_and_history(): target, control_plane, execution_plan = _control_plane() control_plane.submit_provisioning(execution_plan.provisioning) + control_plane.submit_evaluation(execution_plan.evaluation) receipt = control_plane.submit_orchestration(execution_plan.orchestration) status = control_plane.get_operation(receipt.operation_id) diff --git a/implementations/python/tests/test_reference_backend_manifest.py b/implementations/python/tests/test_reference_backend_manifest.py index d69f3a3a3..f228b4b13 100644 --- a/implementations/python/tests/test_reference_backend_manifest.py +++ b/implementations/python/tests/test_reference_backend_manifest.py @@ -53,3 +53,5 @@ def test_manifest_declares_only_evidence_backed_contract_ids(): stub = create_stub_manifest() assert reference.supported_contract_versions == stub.supported_contract_versions + assert "realization-envelope-v1" not in reference.supported_contract_versions + assert reference.realization_envelope is None diff --git a/implementations/python/tests/test_reference_backend_oci_driver.py b/implementations/python/tests/test_reference_backend_oci_driver.py index 3e79dd40e..adbf330ed 100644 --- a/implementations/python/tests/test_reference_backend_oci_driver.py +++ b/implementations/python/tests/test_reference_backend_oci_driver.py @@ -5,6 +5,7 @@ import subprocess import pytest +from aces_backend_protocols.naming import provider_resource_name from aces_reference_backend.driver import ContainerSpec, NetworkSpec from aces_reference_backend.drivers.oci import ImageTrustPolicy, OciDeploymentDriver @@ -50,7 +51,7 @@ def test_oci_realize_uses_fixed_argv_list_never_shell(): address="provision.node.web", name="web", image_ref="aces-reference/linux", - networks=("lan",), + networks=("provision.network.lan",), ), ), ) @@ -157,9 +158,7 @@ def test_oci_rejects_unknown_runtime(): def test_oci_destroy_removes_by_the_name_realize_used(): - """Regression: when a payload pins an explicit name that differs from the - address's last segment, destroy must remove the name realize() actually - created, not a name re-derived from the address.""" + """Destroy uses the canonical-address-derived name created by realize.""" recorder = _Recorder(stdout="id\n") driver = _driver(recorder) @@ -172,7 +171,8 @@ 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] - assert rm_calls == [["docker", "rm", "--force", "pinned-web-name"]] + runtime_name = provider_resource_name("provision.node.web", prefix="aces") + assert rm_calls == [["docker", "rm", "--force", runtime_name]] def test_oci_attaches_container_to_requested_networks(): @@ -198,7 +198,7 @@ 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] == "lan" + assert run_argv[run_argv.index("--network") + 1] == provider_resource_name("provision.network.lan", prefix="aces") def test_oci_rejects_plan_pinned_image_without_allowlist(): @@ -265,5 +265,10 @@ def __call__(self, argv, **kwargs): assert result.networks == () # no resource is reported as realized assert result.containers == () # The successfully-created network was rolled back. - assert ["docker", "network", "rm", "lan"] in runner.calls + assert [ + "docker", + "network", + "rm", + provider_resource_name("provision.network.lan", prefix="aces"), + ] in runner.calls assert driver.realized_addresses() == frozenset() diff --git a/implementations/python/tests/test_reference_backend_provisioner.py b/implementations/python/tests/test_reference_backend_provisioner.py index 54986ee28..1e9050f99 100644 --- a/implementations/python/tests/test_reference_backend_provisioner.py +++ b/implementations/python/tests/test_reference_backend_provisioner.py @@ -4,6 +4,7 @@ import textwrap +import pytest from aces_contracts.planning import ( ChangeAction, PlannedResource, @@ -142,21 +143,19 @@ def test_snapshot_payload_carries_only_portable_facts(): assert forbidden not in rendered -def test_validate_surfaces_realization_diagnostics_without_driver(): +def test_invalid_resource_type_is_rejected_before_provisioner_validation(): driver = InProcessDriver() - target = _target_with_driver(driver) - bad_plan = ProvisioningPlan( - resources={ - "provision.mystery.x": PlannedResource( - address="provision.mystery.x", - domain=RuntimeDomain.PROVISIONING, - resource_type="mystery", - payload={"name": "x"}, - ) - } - ) - - diagnostics = target.provisioner.validate(bad_plan) - - assert any(diag.code == "reference-backend.realization.unsupported-resource" for diag in diagnostics) + _target_with_driver(driver) + + with pytest.raises(ValueError, match="resource_type must belong"): + ProvisioningPlan( + resources={ + "provision.mystery.x": PlannedResource( + address="provision.mystery.x", + domain=RuntimeDomain.PROVISIONING, + resource_type="mystery", + payload={"name": "x"}, + ) + } + ) assert not driver.recorded_ops diff --git a/implementations/python/tests/test_reference_backend_realization.py b/implementations/python/tests/test_reference_backend_realization.py index a94d47402..49cbc2705 100644 --- a/implementations/python/tests/test_reference_backend_realization.py +++ b/implementations/python/tests/test_reference_backend_realization.py @@ -2,7 +2,7 @@ from __future__ import annotations -from aces_contracts.diagnostics import Severity +import pytest from aces_contracts.planning import ( ChangeAction, PlannedResource, @@ -100,6 +100,15 @@ def test_interpret_passes_through_unresolved_network_reference(): assert realization.containers[0].networks == ("lan",) +def test_interpret_does_not_guess_network_from_address_tail(): + network = _network_resource("provision.network.shared.lan", "shared.lan") + plan = _plan(_node_resource("provision.node.web", "web"), network) + + realization = interpret_provisioning_plan(plan) + + assert realization.containers[0].networks == ("lan",) + + def test_interpret_records_placement_resources(): placement = PlannedResource( address="provision.content.payload", @@ -115,21 +124,15 @@ def test_interpret_records_placement_resources(): assert realization.placements[0].resource_type == "content-placement" -def test_interpret_diagnoses_unsupported_resource_type(): +def test_plan_rejects_unsupported_resource_type_before_interpretation(): bad = PlannedResource( address="provision.mystery.x", domain=RuntimeDomain.PROVISIONING, resource_type="mystery-resource", payload={"name": "x"}, ) - plan = _plan(bad) - - realization = interpret_provisioning_plan(plan) - - assert realization.diagnostics - codes = {diag.code for diag in realization.diagnostics} - assert "reference-backend.realization.unsupported-resource" in codes - assert all(diag.severity == Severity.ERROR for diag in realization.diagnostics) + with pytest.raises(ValueError, match="resource_type must belong"): + _plan(bad) def test_interpret_diagnoses_invalid_node_payload(): diff --git a/implementations/python/tests/test_reference_processor.py b/implementations/python/tests/test_reference_processor.py index 744610d1d..440195f20 100644 --- a/implementations/python/tests/test_reference_processor.py +++ b/implementations/python/tests/test_reference_processor.py @@ -49,18 +49,26 @@ conditions: {health: ops} roles: {ops: operator} conditions: - health: {command: /bin/true, interval: 15} + health: {proposition: health-state, command: /bin/true, interval: 15} entities: blue: {role: blue} + propositions: + health-state: + 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} + assertions: + health: {proposition: health-state, role: postcondition} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run steps: - run: {type: objective, objective: validate, on-success: finish} + run: {type: objective, objective: validate, on_success: finish} finish: {type: end} """ ) @@ -80,18 +88,26 @@ conditions: {health: ops} roles: {ops: operator} conditions: - health: {command: /bin/true, interval: 15} + health: {proposition: health-state, command: /bin/true, interval: 15} entities: blue: {role: blue} + propositions: + health-state: + 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} + assertions: + health: {proposition: health-state, role: postcondition} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run steps: - run: {type: objective, objective: validate, on-success: finish} + run: {type: objective, objective: validate, on_success: finish} finish: {type: end} """ ) @@ -207,8 +223,8 @@ def test_every_claimed_contract_is_exercised_end_to_end(self): control_plane = RuntimeControlPlane(target) for sub_plan, submit in ( (result.execution_plan.provisioning, control_plane.submit_provisioning), - (result.execution_plan.orchestration, control_plane.submit_orchestration), (result.execution_plan.evaluation, control_plane.submit_evaluation), + (result.execution_plan.orchestration, control_plane.submit_orchestration), ): receipt = submit(sub_plan) assert receipt.accepted, receipt.diagnostics diff --git a/implementations/python/tests/test_repo_policy_tools.py b/implementations/python/tests/test_repo_policy_tools.py index 6b2d915fb..bed19ddd5 100644 --- a/implementations/python/tests/test_repo_policy_tools.py +++ b/implementations/python/tests/test_repo_policy_tools.py @@ -549,7 +549,7 @@ def test_module_boundaries_reject_runtime_importing_sdl_semantics(tmp_path: Path assert [f.rule_id for f in failures] == ["module-boundary-import"] -# ── ADR-015: 600-line source-file cap ─────────────────────────────────── +# ── ADR-015: 500-line source-file cap ─────────────────────────────────── # A path that is in _ADR015_INITIAL_OVERSIZED_FILES (the code constant in # tools/policy/repo_policy.py), so the allowlist-subset (drain) check passes @@ -629,7 +629,7 @@ def test_allowlist_entry_not_in_locked_set_is_rejected(tmp_path: Path) -> None: def test_allowlist_subset_of_locked_set_passes(tmp_path: Path) -> None: repo_root = setup_policy_repo(tmp_path) - # A strict subset of the 14 initial oversized entries — the drained state. + # A strict subset of the initial oversized entries — the drained state. write_text(repo_root / "tools" / "policy" / "oversized_allowlist.yaml", f"files:\n - {_LOCKED_PATH}\n") write_text(repo_root / _LOCKED_PATH, "x = 1\n" * 700) @@ -752,7 +752,7 @@ def test_stale_allowlist_entry_below_cap_is_rejected(tmp_path: Path) -> None: even though the file's deletion/shrink isn't in the changed set.""" repo_root = setup_policy_repo(tmp_path) write_text(repo_root / "tools" / "policy" / "oversized_allowlist.yaml", f"files:\n - {_LOCKED_PATH}\n") - write_text(repo_root / _LOCKED_PATH, "x = 1\n" * 100) # well under the 600-line cap + write_text(repo_root / _LOCKED_PATH, "x = 1\n" * 100) # well under the 500-line cap failures = evaluate_repo_policy( repo_root, diff --git a/implementations/python/tests/test_run_300_lifecycle.py b/implementations/python/tests/test_run_300_lifecycle.py index d1f916bf7..70dda7c8b 100644 --- a/implementations/python/tests/test_run_300_lifecycle.py +++ b/implementations/python/tests/test_run_300_lifecycle.py @@ -85,12 +85,20 @@ def _raw_scenario(): roles: {{ops: operator}} conditions: health: {{command: /bin/true, interval: 15}} + propositions: + health: + 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}} + assertions: + health: {{proposition: health, role: postcondition, polarity: positive}} entities: blue: {{role: blue}} objectives: validate: entity: blue - success: {{conditions: [health]}} + success: {{assertions: [health]}} workflows: response: start: run @@ -98,7 +106,7 @@ def _raw_scenario(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {{type: end}} """ ) @@ -140,13 +148,12 @@ def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): "not a loose dict — typed contracts are how RUN-300 preserves " "meaning across stages." ) - assert instantiated.instantiation_parameters == { + assert instantiated.instantiation_provenance.root_binding_values == { "os_kind": PARAM_OS_KIND, "cpu_count": PARAM_CPU_COUNT, }, ( - "Instantiation parameters must be captured on the concrete " - "scenario so downstream stages can trace provenance back to " - "the authoring inputs." + "Instantiation parameters must be captured in portable provenance " + "so downstream stages can trace them to the authoring inputs." ) instantiated_payload = instantiated.model_dump(mode="python", by_alias=True) assert _no_variable_tokens(instantiated_payload), ( 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 0bd934bfa..a13b7e2b4 100644 --- a/implementations/python/tests/test_run_307_shared_operational_state.py +++ b/implementations/python/tests/test_run_307_shared_operational_state.py @@ -290,7 +290,7 @@ def _backend_apply(_request: object, snapshot: RuntimeSnapshot) -> ApplyResult: ) assert result.success is False - assert any("shared_state_records" in diagnostic.message for diagnostic in result.diagnostics) + assert any(diagnostic.code == "runtime.backend-contract-invalid" for diagnostic in result.diagnostics) def test_backend_apply_rejects_shared_state_history_rewrite() -> None: diff --git a/implementations/python/tests/test_runtime_conformance.py b/implementations/python/tests/test_runtime_conformance.py index b1605042a..1c5e953be 100644 --- a/implementations/python/tests/test_runtime_conformance.py +++ b/implementations/python/tests/test_runtime_conformance.py @@ -1033,8 +1033,8 @@ def test_run_target_conformance_surfaces_profile_load_failure(tmp_path: Path): codes = {diag.code for diag in report.diagnostics} assert "conformance.profile-load-failed" in codes case_names = {case.name for case in report.cases} - assert "live-manifest" not in case_names - assert "live-snapshot" not in case_names + assert "target-manifest" not in case_names + assert "target-snapshot" not in case_names assert "participant-initialize" not in case_names @@ -1070,8 +1070,8 @@ def test_run_target_conformance_refuses_unknown_profile_id(tmp_path: Path): codes = {diag.code for diag in report.diagnostics} assert "conformance.profile-runtime-surface-unknown" in codes case_names = {case.name for case in report.cases} - assert "live-manifest" not in case_names - assert "live-snapshot" not in case_names + assert "target-manifest" not in case_names + assert "target-snapshot" not in case_names def test_run_fixture_suite_path_traversal_id_surfaces_as_load_diagnostic(tmp_path: Path): @@ -1230,15 +1230,23 @@ def _reference_scenario(node_name: str, *, os_family: str = "linux") -> str: roles: {{ops: operator}} conditions: health: {{command: /bin/true, interval: 15}} +propositions: + health: + 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}} +assertions: + health: {{proposition: health, role: postcondition, polarity: positive}} entities: blue: {{role: blue}} objectives: - validate: {{entity: blue, success: {{conditions: [health]}}}} + validate: {{entity: blue, success: {{assertions: [health]}}}} workflows: response: start: run steps: - run: {{type: objective, objective: validate, on-success: finish}} + run: {{type: objective, objective: validate, on_success: finish}} finish: {{type: end}} """ @@ -1263,7 +1271,7 @@ def test_target_conformance_default_scenario_fails_fixed_topology_backend(): assert report.profile == BackendCapabilityProfile.PROVISIONING_ONLY assert report.passed is False - provisioning = next(case for case in report.cases if case.name == "live-provisioning") + provisioning = next(case for case in report.cases if case.name == "target-provisioning") assert provisioning.passed is False assert any(diag.code == "conformance.provisioning-failed" for diag in provisioning.diagnostics) @@ -1278,9 +1286,9 @@ def test_target_conformance_accepts_supplied_reference_scenario(): ) assert report.passed is True - provisioning = next(case for case in report.cases if case.name == "live-provisioning") + provisioning = next(case for case in report.cases if case.name == "target-provisioning") assert provisioning.passed is True - snapshot_case = next(case for case in report.cases if case.name == "live-snapshot") + snapshot_case = next(case for case in report.cases if case.name == "target-snapshot") assert snapshot_case.passed is True @@ -1294,8 +1302,8 @@ def test_supplied_reference_scenario_still_enforces_mutation_guard(): ) assert report.passed is False - provisioning = next(case for case in report.cases if case.name == "live-provisioning") - snapshot_case = next(case for case in report.cases if case.name == "live-snapshot") + provisioning = next(case for case in report.cases if case.name == "target-provisioning") + snapshot_case = next(case for case in report.cases if case.name == "target-snapshot") assert provisioning.passed is False assert snapshot_case.passed is False codes = {diag.code for case in report.cases for diag in case.diagnostics} diff --git a/implementations/python/tests/test_runtime_contracts.py b/implementations/python/tests/test_runtime_contracts.py index 2ad637cf8..db8e63f33 100644 --- a/implementations/python/tests/test_runtime_contracts.py +++ b/implementations/python/tests/test_runtime_contracts.py @@ -18,6 +18,7 @@ ExperimentEvidenceRecordModel, ExperimentRunModel, ExperimentRunTraceabilityModel, + ExperimentSpecModel, ExperimentStudyModel, ExperimentTaskModel, ParticipantImplementationManifestModel, @@ -43,6 +44,7 @@ EXPERIMENT_CORE_FIXTURE_MODELS = { "experiment-apparatus-context-v1": ExperimentApparatusContextModel, + "experiment-authoring-input-v1": ExperimentSpecModel, "experiment-capture-spec-v1": ExperimentCaptureSpecModel, "experiment-derived-measure-v1": ExperimentDerivedMeasureModel, "experiment-evidence-record-v1": ExperimentEvidenceRecordModel, @@ -626,6 +628,20 @@ def test_experiment_core_invalid_fixtures_fail_schema_and_model_validation(): model_cls.model_validate(payload) +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 + # that portable JSON Schema cannot express (issue #675 codex review). + payload = _experiment_fixture("experiment-authoring-input-v1") + payload["factors"] = {} + payload["run_plan"]["allocation"]["blocking_factors"] = ["undeclared-factor"] + + # Schema-valid (blocking_factors is a list of strings) but model-invalid. + assert not list(Draft202012Validator(schema_bundle()["experiment-authoring-input-v1"]).iter_errors(payload)) + with pytest.raises(ValidationError): + ExperimentSpecModel.model_validate(payload) + + def test_experiment_core_requires_schema_versions_on_wire_artifacts(): for contract_id, model_cls in EXPERIMENT_CORE_FIXTURE_MODELS.items(): payload = _experiment_fixture(contract_id) diff --git a/implementations/python/tests/test_runtime_control_plane.py b/implementations/python/tests/test_runtime_control_plane.py index 72306398d..7596e6555 100644 --- a/implementations/python/tests/test_runtime_control_plane.py +++ b/implementations/python/tests/test_runtime_control_plane.py @@ -14,7 +14,8 @@ ParticipantStatusViewModel, ) from aces_contracts.participant_binding import ParticipantActionAdmissionRequest -from aces_contracts.runtime_state import RuntimeSnapshot +from aces_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp +from aces_contracts.runtime_state import RuntimeSnapshot, SnapshotEntry from aces_processor.models import ( iter_participant_behavior_history_violations, iter_participant_episode_snapshot_violations, @@ -51,35 +52,35 @@ def _participant_binding_scenario_yaml() -> str: entities: red-team: role: red -action-contracts: +action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: governed service discovery - realization-profile: backend-declared - fidelity-claim: records participant discovery intent and terminal observation + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: governed service discovery + realization_profile: backend-declared + fidelity_claim: records participant discovery intent and terminal observation preconditions: - - precondition-id: authority-in-scope - precondition-class: authority + - precondition_id: authority-in-scope + precondition_class: authority description: red participant is authorized to scan the web service effects: - - effect-id: terminal-scan-observation - effect-class: observation_effect + - effect_id: terminal-scan-observation + effect_class: observation_effect description: terminal scan observation - evidence-refs: [evidence.scan-output] - failure-classes: [backend_error, unknown] -observation-boundaries: + evidence_refs: [evidence.scan-output] + failure_classes: [backend_error, unknown] +observation_boundaries: red-view: - projection-basis: participant-local projection over observed services - evidence-refs: [evidence.scan-output] - redaction-policy: hidden refs never project without explicit disclosure - latency-profile: terminal observation emitted after state transition commit + projection_basis: participant-local projection over observed services + evidence_refs: [evidence.scan-output] + redaction_policy: hidden refs never project without explicit disclosure + latency_profile: terminal observation emitted after state transition commit agents: red-agent: entity: red-team actions: [scan] - observation-boundaries: [red-view] + observation_boundaries: [red-view] """ @@ -281,6 +282,56 @@ def test_control_plane_submits_provisioning_and_updates_snapshot(): assert snapshot.snapshot.entries +def test_control_plane_rejects_dependency_outside_plan_and_snapshot() -> None: + plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address="provision.node.vm", + resource_type="node", + payload={}, + ordering_dependencies=("provision.network.missing",), + ) + ] + ) + control_plane = RuntimeControlPlane(create_stub_target()) + + receipt = control_plane.submit_provisioning(plan) + + assert receipt.accepted is False + assert [diagnostic.code for diagnostic in receipt.diagnostics] == ["runtime.plan-dependency-unresolved"] + + +def test_control_plane_rejects_snapshot_resource_identity_disagreement() -> None: + address = "provision.node.vm" + snapshot = RuntimeSnapshot( + entries={ + address: SnapshotEntry( + address=address, + domain=RuntimeDomain.EVALUATION, + resource_type="objective", + payload={}, + ) + } + ) + plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.UPDATE, + address=address, + resource_type="node", + payload={}, + ) + ] + ) + control_plane = RuntimeControlPlane(create_stub_target(), initial_snapshot=snapshot) + + receipt = control_plane.submit_provisioning(plan) + + assert receipt.accepted is False + assert [diagnostic.code for diagnostic in receipt.diagnostics] == ["runtime.plan-resource-incoherent"] + + def test_control_plane_submits_orchestration_with_portable_workflow_state(): scenario = _scenario(""" name: workflow @@ -293,12 +344,20 @@ def test_control_plane_submits_orchestration_with_portable_workflow_state(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -306,13 +365,17 @@ def test_control_plane_submits_orchestration_with_portable_workflow_state(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """) target = create_stub_target() execution_plan = plan(compile_runtime_model(scenario), target.manifest) control_plane = RuntimeControlPlane(target) + provisioning_receipt = control_plane.submit_provisioning(execution_plan.provisioning) + assert provisioning_receipt.accepted is True + evaluation_receipt = control_plane.submit_evaluation(execution_plan.evaluation) + assert evaluation_receipt.accepted is True receipt = control_plane.submit_orchestration(execution_plan.orchestration) status = control_plane.get_operation(receipt.operation_id) snapshot = control_plane.get_snapshot() diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index c45b8bc44..4a62cfcbb 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -34,6 +34,13 @@ def _scenario(yaml_str: str): return parse_sdl(textwrap.dedent(yaml_str)) +def _admit_workflow_prerequisites(control_plane: RuntimeControlPlane, execution_plan: object) -> None: + provisioning = control_plane.submit_provisioning(execution_plan.provisioning) + evaluation = control_plane.submit_evaluation(execution_plan.evaluation) + assert provisioning.accepted, provisioning.diagnostics + assert evaluation.accepted, evaluation.diagnostics + + def _participant_operation_record(operation_id: str, participant_address: str) -> ControlPlaneOperationRecord: submitted_at = "2026-06-05T10:00:00Z" return ControlPlaneOperationRecord( @@ -154,12 +161,20 @@ def test_control_plane_api_accepts_orchestration_plan_and_exposes_snapshot(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -167,12 +182,13 @@ def test_control_plane_api_accepts_orchestration_plan_and_exposes_snapshot(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """) target = create_stub_target() execution_plan = plan(compile_runtime_model(scenario), target.manifest) control_plane = RuntimeControlPlane(target) + _admit_workflow_prerequisites(control_plane, execution_plan) app = create_control_plane_app( control_plane, security=_test_security(target.name), @@ -228,12 +244,20 @@ def test_control_plane_api_exposes_operational_apparatus_summary_to_auditors(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -241,12 +265,13 @@ def test_control_plane_api_exposes_operational_apparatus_summary_to_auditors(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """) target = create_stub_target() execution_plan = plan(compile_runtime_model(scenario), target.manifest) control_plane = RuntimeControlPlane(target) + _admit_workflow_prerequisites(control_plane, execution_plan) app = create_control_plane_app( control_plane, security=_test_security(target.name), @@ -284,11 +309,13 @@ def test_control_plane_api_exposes_operational_apparatus_summary_to_auditors(): assert summary["target"] == target.name assert summary["resources"]["total"] >= 1 assert summary["resources"]["by_domain"]["orchestration"] >= 1 - assert summary["operations"]["by_state"]["succeeded"] == 1 - assert summary["operations"]["recent"][0]["operation_id"] == receipt["operation_id"] - assert summary["operations"]["recent"][0]["diagnostic_count"] == 0 - assert summary["operations"]["recent"][0]["diagnostic_codes"] == [] - assert summary["operations"]["recent"][0]["changed_addresses"] + assert summary["operations"]["by_state"]["succeeded"] == 3 + orchestration_record = next( + record for record in summary["operations"]["recent"] if record["operation_id"] == receipt["operation_id"] + ) + assert orchestration_record["diagnostic_count"] == 0 + assert orchestration_record["diagnostic_codes"] == [] + assert orchestration_record["changed_addresses"] assert summary["runtime_surfaces"]["orchestration_results"] >= 1 assert summary["runtime_surfaces"]["orchestration_history"] >= 1 assert summary["audit"]["allowed"] >= 2 @@ -522,12 +549,20 @@ def test_control_plane_api_cancels_workflow_runs(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -535,12 +570,13 @@ def test_control_plane_api_cancels_workflow_runs(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """) target = create_stub_target() execution_plan = plan(compile_runtime_model(scenario), target.manifest) control_plane = RuntimeControlPlane(target) + _admit_workflow_prerequisites(control_plane, execution_plan) app = create_control_plane_app( control_plane, security=_test_security(target.name), @@ -595,12 +631,20 @@ def test_control_plane_api_reconciles_workflow_timeouts(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -609,12 +653,13 @@ def test_control_plane_api_reconciles_workflow_timeouts(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """) target = create_stub_target() execution_plan = plan(compile_runtime_model(scenario), target.manifest) control_plane = RuntimeControlPlane(target) + _admit_workflow_prerequisites(control_plane, execution_plan) app = create_control_plane_app( control_plane, security=_test_security(target.name), @@ -679,12 +724,20 @@ def test_control_plane_api_cancellation_triggers_compensation_history(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: rollback: start: finish @@ -699,14 +752,15 @@ def test_control_plane_api_cancellation_triggers_compensation_history(): run: type: objective objective: validate - compensate-with: rollback - on-success: finish - on-failure: finish + compensate_with: rollback + on_success: finish + on_failure: finish finish: {type: end} """) target = create_stub_target() execution_plan = plan(compile_runtime_model(scenario), target.manifest) control_plane = RuntimeControlPlane(target) + _admit_workflow_prerequisites(control_plane, execution_plan) app = create_control_plane_app( control_plane, security=_test_security(target.name), @@ -796,12 +850,20 @@ def test_control_plane_api_timeout_triggers_compensation_history(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: rollback: start: finish @@ -817,14 +879,15 @@ def test_control_plane_api_timeout_triggers_compensation_history(): run: type: objective objective: validate - compensate-with: rollback - on-success: finish - on-failure: finish + compensate_with: rollback + on_success: finish + on_failure: finish finish: {type: end} """) target = create_stub_target() execution_plan = plan(compile_runtime_model(scenario), target.manifest) control_plane = RuntimeControlPlane(target) + _admit_workflow_prerequisites(control_plane, execution_plan) app = create_control_plane_app( control_plane, security=_test_security(target.name), diff --git a/implementations/python/tests/test_runtime_datastore.py b/implementations/python/tests/test_runtime_datastore.py index e70a1a0eb..595d655c1 100644 --- a/implementations/python/tests/test_runtime_datastore.py +++ b/implementations/python/tests/test_runtime_datastore.py @@ -43,7 +43,11 @@ from aces.core.sdl.scenario import Scenario -_PUBLISHED_SDL_SCHEMA_NAMES = ("instantiated-scenario-v1", "sdl-authoring-input-v1") +_PUBLISHED_SDL_SCHEMA_NAMES = ( + "instantiated-scenario-snapshot-v1", + "instantiated-scenario-v1", + "sdl-authoring-input-v1", +) _DATASTORE_MAPPING_SCHEMA_FIELDS = { "date_detection", "description", @@ -354,30 +358,30 @@ def test_datastore_cardinality_fields_accept_variable_refs() -> None: **_search_index_service( cluster={ "cluster_id": "wazuh-cluster", - "node_count": "${NODE_COUNT}", - "shard_total": "${SHARDS}", - "shard_primaries": "${PRIMARIES}", - "doc_count": "${DOCS}", - "store_size_bytes": "${BYTES}", + "node_count": "${node_count}", + "shard_total": "${shards}", + "shard_primaries": "${primaries}", + "doc_count": "${docs}", + "store_size_bytes": "${bytes}", }, partitions=[ { "partition_id": "wazuh-alerts", "kind": "index", - "shard_count": "${SHARDS}", - "replica_count": "${REPLICAS}", - "doc_count": "${DOCS}", - "doc_count_deleted": "${DELETED}", - "store_size_bytes": "${BYTES}", + "shard_count": "${shards}", + "replica_count": "${replicas}", + "doc_count": "${docs}", + "doc_count_deleted": "${deleted}", + "store_size_bytes": "${bytes}", } ], ) ) assert svc.cluster is not None - assert svc.cluster.doc_count == "${DOCS}" - assert svc.cluster.store_size_bytes == "${BYTES}" - assert svc.partitions[0].doc_count_deleted == "${DELETED}" + assert svc.cluster.doc_count == "${docs}" + assert svc.cluster.store_size_bytes == "${bytes}" + assert svc.partitions[0].doc_count_deleted == "${deleted}" def test_datastore_cluster_rejects_negative_cardinality() -> None: @@ -491,8 +495,8 @@ def test_relational_and_open_tail_impose_no_profile() -> None: def test_variable_ref_data_model_is_exempt_from_guard() -> None: - svc = RuntimeDatastoreService(datastore_service_id="ds-var", data_model="${DATA_MODEL}") - assert svc.data_model == "${DATA_MODEL}" + svc = RuntimeDatastoreService(datastore_service_id="ds-var", data_model="${data_model}") + assert svc.data_model == "${data_model}" def test_variable_refs_for_mapping_and_template_links_are_exempt_from_resolution_guard() -> None: @@ -501,7 +505,7 @@ def test_variable_refs_for_mapping_and_template_links_are_exempt_from_resolution mappings=[ { "mapping_id": "deferred-mapping", - "partition_ref": "${PARTITION_ID}", + "partition_ref": "${partition_id}", "top_level_field_count": 1, "leaf_field_count": 1, } @@ -509,15 +513,15 @@ def test_variable_refs_for_mapping_and_template_links_are_exempt_from_resolution templates=[ { "template_id": "deferred-template", - "mapping_ref": "${MAPPING_ID}", + "mapping_ref": "${mapping_id}", "index_patterns": ["wazuh-*"], } ], ) ) - assert svc.mappings[0].partition_ref == "${PARTITION_ID}" - assert svc.templates[0].mapping_ref == "${MAPPING_ID}" + assert svc.mappings[0].partition_ref == "${partition_id}" + assert svc.templates[0].mapping_ref == "${mapping_id}" # --------------------------------------------------------------------------- # @@ -573,11 +577,11 @@ def test_mapping_and_template_reject_duplicate_local_lists() -> None: def test_mapping_and_template_ids_reject_variable_placeholders() -> None: - with pytest.raises(ValidationError, match="mapping_id must be a stable identifier"): - RuntimeDatastoreMapping(mapping_id="${MAPPING_ID}") + with pytest.raises(ValidationError, match="mapping_id must be a portable SDL identifier"): + RuntimeDatastoreMapping(mapping_id="${mapping_id}") - with pytest.raises(ValidationError, match="template_id must be a stable identifier"): - RuntimeDatastoreTemplate(template_id="${TEMPLATE_ID}") + with pytest.raises(ValidationError, match="template_id must be a portable SDL identifier"): + RuntimeDatastoreTemplate(template_id="${template_id}") def test_secret_named_setting_may_carry_scenario_value() -> None: @@ -671,7 +675,7 @@ def _string_branch(schema_name: str) -> dict: string values (issue #500); the authoring contract still accepts them, so the two schemas now differ on every string branch. """ - if schema_name == "instantiated-scenario-v1": + if schema_name in {"instantiated-scenario-v1", "instantiated-scenario-snapshot-v1"}: return {"type": "string", "not": {"pattern": VARIABLE_TOKEN_PATTERN}} return {"type": "string"} @@ -787,8 +791,8 @@ def test_node_heap_bytes_accept_human_int_and_var() -> None: assert node.heap_init_bytes == 536_870_912 assert node.heap_max_bytes == 1_073_741_824 - var_node = RuntimeDatastoreNode(node_id="n2", heap_max_bytes="${HEAP}") - assert var_node.heap_max_bytes == "${HEAP}" + var_node = RuntimeDatastoreNode(node_id="n2", heap_max_bytes="${heap}") + assert var_node.heap_max_bytes == "${heap}" def test_node_rejects_heap_init_above_max() -> None: @@ -797,13 +801,13 @@ def test_node_rejects_heap_init_above_max() -> None: def test_node_heap_ordering_exempt_for_variable_bounds() -> None: - node = RuntimeDatastoreNode(node_id="n1", heap_init_bytes="${INIT}", heap_max_bytes=1024) - assert node.heap_init_bytes == "${INIT}" + node = RuntimeDatastoreNode(node_id="n1", heap_init_bytes="${init}", heap_max_bytes=1024) + assert node.heap_init_bytes == "${init}" def test_node_memory_locked_parses_bool_and_var() -> None: assert RuntimeDatastoreNode(node_id="n1", memory_locked="true").memory_locked is True - assert RuntimeDatastoreNode(node_id="n2", memory_locked="${MLOCK}").memory_locked == "${MLOCK}" + assert RuntimeDatastoreNode(node_id="n2", memory_locked="${mlock}").memory_locked == "${mlock}" def test_engine_plugin_retains_per_plugin_version() -> None: @@ -816,7 +820,7 @@ def test_engine_plugin_retains_per_plugin_version() -> None: def test_engine_plugin_id_must_be_stable_symbol() -> None: with pytest.raises(ValidationError, match="plugin_id"): - RuntimeDatastoreEnginePlugin(plugin_id="${PLUGIN}", name="x") + RuntimeDatastoreEnginePlugin(plugin_id="${plugin}", name="x") with pytest.raises(ValidationError, match="plugin_id"): RuntimeDatastoreEnginePlugin(plugin_id="", name="x") @@ -846,7 +850,7 @@ def test_endpoint_role_rejects_unrecognized_value() -> None: def test_endpoint_id_must_be_stable_symbol() -> None: with pytest.raises(ValidationError, match="endpoint_id"): - RuntimeDatastoreNodeEndpoint(endpoint_id="${E}") + RuntimeDatastoreNodeEndpoint(endpoint_id="${e}") def test_endpoint_port_range_enforced() -> None: diff --git a/implementations/python/tests/test_runtime_forwarding_agent.py b/implementations/python/tests/test_runtime_forwarding_agent.py index 346adbcd0..f46774271 100644 --- a/implementations/python/tests/test_runtime_forwarding_agent.py +++ b/implementations/python/tests/test_runtime_forwarding_agent.py @@ -60,7 +60,7 @@ def _log_forwarder(**overrides) -> dict: "ship_targets": [ { "target_id": "manager", - "target_node_ref": "wazuh.manager", + "target_node_ref": "wazuh-manager", "ingestion_port": 1514, "enrollment_port": 1515, "protocol": "syslog", @@ -185,8 +185,8 @@ def test_open_tail_kinds_impose_no_profile() -> None: def test_variable_ref_agent_kind_is_exempt_from_guard() -> None: - agent = RuntimeForwardingAgent(forwarding_agent_id="agent-var", agent_kind="${AGENT_KIND}") - assert agent.agent_kind == "${AGENT_KIND}" + agent = RuntimeForwardingAgent(forwarding_agent_id="agent-var", agent_kind="${agent_kind}") + assert agent.agent_kind == "${agent_kind}" def test_invalid_enum_value_rejected() -> None: @@ -195,8 +195,8 @@ def test_invalid_enum_value_rejected() -> None: def test_id_fields_reject_variable_placeholders() -> None: - with pytest.raises(ValidationError, match="forwarding_agent_id must be a stable identifier"): - RuntimeForwardingAgent(forwarding_agent_id="${ID}") + with pytest.raises(ValidationError, match="forwarding_agent_id must be a qualified SDL identifier"): + RuntimeForwardingAgent(forwarding_agent_id="${id}") # --------------------------------------------------------------------------- # @@ -403,10 +403,10 @@ def test_scenario_level_surface_reuses_runtime_forwarding_agent() -> None: def test_log_forwarder_target_node_ref_resolves_to_defined_node() -> None: - # The _log_forwarder fixture ships to target_node_ref "wazuh.manager". + # The _log_forwarder fixture ships to target_node_ref "wazuh-manager". scenario = Scenario( name="forwarding", - nodes={"wazuh.manager": _manager_node(), "sensor": _sensor_node(_log_forwarder())}, + nodes={"wazuh-manager": _manager_node(), "sensor": _sensor_node(_log_forwarder())}, ) assert _validate(scenario) == [] @@ -459,7 +459,7 @@ def test_scenario_level_ship_target_service_ref_requires_target_node_ref() -> No ) scenario = Scenario( name="forwarding", - nodes={"wazuh.manager": _manager_node()}, + nodes={"wazuh-manager": _manager_node()}, forwarding_agents=[agent], ) @@ -486,7 +486,7 @@ def test_forwarding_edge_resolves_scenario_level_forwarder() -> None: ship_targets=[ { "target_id": "manager", - "target_node_ref": "wazuh.manager", + "target_node_ref": "wazuh-manager", "target_service_ref": "wazuh-agent-events", "ingestion_port": 1514, "protocol": "syslog", @@ -503,14 +503,14 @@ def test_forwarding_edge_resolves_scenario_level_forwarder() -> None: "resources": {"ram": "1 gib", "cpu": 1}, "services": [{"port": 5432, "protocol": "tcp", "name": "postgres"}], }, - "wazuh.manager": _manager_node(), + "wazuh-manager": _manager_node(), }, forwarding_agents=[agent], relationships={ "db-logs-forwarded-wazuh": { "type": "connects_to", "source": "db", - "target": "wazuh.manager", + "target": "wazuh-manager", "forwarding_edge": { "forwarder_ref": "db-wazuh-agent", "target_listener_role": "agent_event_ingestion", @@ -532,13 +532,13 @@ def test_forwarding_edge_missing_forwarder_reports_combined_resolution_scope() - "resources": {"ram": "1 gib", "cpu": 1}, "services": [{"port": 5432, "protocol": "tcp", "name": "postgres"}], }, - "wazuh.manager": _manager_node(), + "wazuh-manager": _manager_node(), }, relationships={ "db-logs-forwarded-wazuh": { "type": "connects_to", "source": "db", - "target": "wazuh.manager", + "target": "wazuh-manager", "forwarding_edge": {"forwarder_ref": "missing-agent"}, } }, @@ -606,8 +606,8 @@ def test_forwarding_edge_forwarder_ref_required() -> None: def test_forwarding_edge_forwarder_ref_allows_variable_placeholder() -> None: - edge = RelationshipForwardingEdge(forwarder_ref="${FORWARDER}") - assert edge.forwarder_ref == "${FORWARDER}" + edge = RelationshipForwardingEdge(forwarder_ref="${forwarder}") + assert edge.forwarder_ref == "${forwarder}" def test_forwarding_edge_present_enrollment_identity_must_be_redacted() -> None: @@ -648,6 +648,6 @@ def test_forwarding_edge_variable_classification_defers_check() -> None: edge = RelationshipForwardingEdge( forwarder_ref="agent-1", enrollment_identity_ref="agent-key-001", - enrollment_identity_classification="${CLASS}", + enrollment_identity_classification="${class}", ) - assert edge.enrollment_identity_classification == "${CLASS}" + assert edge.enrollment_identity_classification == "${class}" diff --git a/implementations/python/tests/test_runtime_mail_service.py b/implementations/python/tests/test_runtime_mail_service.py index d302d2541..f4c9fa554 100644 --- a/implementations/python/tests/test_runtime_mail_service.py +++ b/implementations/python/tests/test_runtime_mail_service.py @@ -159,7 +159,7 @@ def test_vm_runtime_mail_service_surface() -> None: assert service.settings[0].provenance == RuntimeMailSettingProvenance.CONFIGURATION_FILE -def test_parser_accepts_kebab_case_runtime_mail_services() -> None: +def test_parser_accepts_canonical_runtime_mail_services() -> None: scenario = parse_sdl( """ name: mail-parser @@ -170,21 +170,21 @@ def test_parser_accepts_kebab_case_runtime_mail_services() -> None: services: - {port: 25, name: smtp} runtime: - mail-services: - - mail-service-id: techvault-mail + mail_services: + - mail_service_id: techvault-mail service: smtp listeners: - - listener-id: smtp-listener + - listener_id: smtp-listener service: smtp protocol: smtp - tls-mode: starttls-available + tls_mode: starttls-available domains: - - domain-id: techvault-domain + - domain_id: techvault-domain name: techvault.local mailboxes: - - mailbox-id: admin-mailbox + - mailbox_id: admin-mailbox address: admin@techvault.local - domain-ref: techvault-domain + domain_ref: techvault-domain """ ) diff --git a/implementations/python/tests/test_runtime_manager.py b/implementations/python/tests/test_runtime_manager.py index 79f9690d0..3d35c8350 100644 --- a/implementations/python/tests/test_runtime_manager.py +++ b/implementations/python/tests/test_runtime_manager.py @@ -64,10 +64,18 @@ def _full_scenario(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + pre-health: {proposition: health, role: precondition, polarity: positive} events: - kickoff: {conditions: [health]} + kickoff: {assertions: [pre-health]} scripts: - timeline: {start-time: 0, end-time: 60, speed: 1, events: {kickoff: 10}} + timeline: {start_time: 0, end_time: 60, speed: 1, events: {kickoff: 10}} stories: main: {scripts: [timeline]} """) @@ -96,12 +104,20 @@ def _workflow_scenario(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: response: start: run @@ -109,7 +125,7 @@ def _workflow_scenario(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} """) @@ -126,12 +142,20 @@ def _workflow_call_scenario(): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: child: start: run @@ -139,7 +163,7 @@ def _workflow_call_scenario(): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} parent: start: delegate @@ -147,7 +171,7 @@ def _workflow_call_scenario(): delegate: type: call workflow: child - on-success: finish + on_success: finish finish: {type: end} """) @@ -560,6 +584,8 @@ def start(self, plan, snapshot: RuntimeSnapshot) -> ApplyResult: for op in plan.operations: if op.action == ChangeAction.DELETE: continue + if op.resource_type in {"proposition", "assertion"}: + continue result_contract = op.payload.get("result_contract", {}) resource_type = str(result_contract.get("resource_type", op.resource_type)) result_payload: dict[str, object] = { diff --git a/implementations/python/tests/test_runtime_models.py b/implementations/python/tests/test_runtime_models.py index 850010a43..1e2e35275 100644 --- a/implementations/python/tests/test_runtime_models.py +++ b/implementations/python/tests/test_runtime_models.py @@ -5,12 +5,14 @@ import textwrap from pathlib import Path +import pytest + from aces.core.runtime.capabilities import ( WorkflowFeature, WorkflowStatePredicateFeature, ) from aces.core.runtime.compiler import compile_runtime_model -from aces.core.sdl import parse_sdl, parse_sdl_file +from aces.core.sdl import SDLInstantiationError, parse_sdl, parse_sdl_file def _scenario(yaml_str: str): @@ -61,77 +63,77 @@ def test_node_runtime_preserves_runtime_configuration_metadata(self): mounts: - target: /shuffle-database source: aptl_shuffle_data - source-sensitivity: plain - source-kind: volume - filesystem-type: ext4 - read-only: false + source_sensitivity: plain + source_kind: volume + filesystem_type: ext4 + read_only: false options: [rw, nosuid] - options-sensitivity: plain + options_sensitivity: plain propagation: rprivate stability: volume-backed - backend-generated: true - filesystem-inventory: + backend_generated: true + filesystem_inventory: - path: /app/app.py - entry-type: file - owner-user: root - owner-group: root + entry_type: file + owner_user: root + owner_group: root uid: "0" gid: "0" mode: "0644" size: "4096" - content-digest: 4f8c2d - digest-algorithm: sha256 - source-path: src/webapp/app.py + content_digest: 4f8c2d + digest_algorithm: sha256 + source_path: src/webapp/app.py provenance: python-package stability: stable sensitivity: plain - path: /var/log/gunicorn/access.log - entry-type: file + entry_type: file mode: "0600" stability: log sensitivity: operator-secret - local-control-interfaces: - - control-interface-id: docker-sock + local_control_interfaces: + - control_interface_id: docker-sock path: /run/docker.sock kind: unix-socket protocol: docker - bind-source-sensitivity: operator-secret + bind_source_sensitivity: operator-secret access: read-write processes: - name: shufflebackend command: ./shufflebackend user: root - working-directory: /app + working_directory: /app - name: supervisord pid: 1 command: supervisord -n role: supervisor - name: gunicorn - parent-pid: 1 + parent_pid: 1 command: [gunicorn, app:app] role: worker environment: - name: TECHVAULT_ADMIN_PASSWORD - value-classification: redacted + value_classification: redacted provenance: operator - name: SCENARIO_FIXTURE_TOKEN value: fixture-token - value-classification: secret-fixture + value_classification: secret-fixture provenance: compose - linux-capabilities: + linux_capabilities: required: [CAP_NET_ADMIN] effective: CAP_NET_ADMIN - operational-policy: + operational_policy: restart: unless-stopped - resource-limits: + resource_limits: memory: 512 MiB cpu: 0.5 pids: 128 container: entrypoint: [/entrypoint.sh] command: [gunicorn, app:app] - log-driver: json-file - log-options: + log_driver: json-file + log_options: max-size: 10m max-file: "3" namespaces: @@ -141,69 +143,69 @@ def test_node_runtime_preserves_runtime_configuration_metadata(self): userns: host uts: private privileged: false - read-only-rootfs: false - publish-all-ports: false + read_only_rootfs: false + publish_all_ports: false autoremove: false - shm-size: 64 MiB - masked-paths: [/proc/acpi, /proc/kcore] - read-only-paths: /proc/sys - cgroup-parent: /docker - runtime-name: runc + shm_size: 64 MiB + masked_paths: [/proc/acpi, /proc/kcore] + read_only_paths: /proc/sys + cgroup_parent: /docker + runtime_name: runc devices: - - host-path: /dev/null - container-path: /dev/null + - host_path: /dev/null + container_path: /dev/null permissions: rwm - device-cgroup-rules: c 1:3 rwm - seccomp-profile: unconfined - security-opt: [seccomp:unconfined, no-new-privileges] - extra-hosts: + device_cgroup_rules: c 1:3 rwm + seccomp_profile: unconfined + security_opt: [seccomp:unconfined, no-new-privileges] + extra_hosts: - hostname: wazuh-manager address: 172.20.0.10 dns: [8.8.8.8] - dns-options: ndots:0 - dns-search: [techvault.local] - group-add: [adm, "101"] + dns_options: ndots:0 + dns_search: [techvault.local] + group_add: [adm, "101"] health: status: healthy - failing-streak: "0" + failing_streak: "0" log: - start: "2026-05-20T12:00:00Z" end: "2026-05-20T12:00:01Z" - exit-code: "0" + exit_code: "0" output: ok packages: - manager: apk name: musl version: 1.2.4-r2 - software-components: - - component-id: shuffle-backend-app + software_components: + - component_id: shuffle-backend-app name: shuffle-backend version: 1.2.3 - component-type: application + component_type: application provenance: scanner ecosystem: go purl: "pkg:golang/github.com/frikky/shuffle@1.2.3" - package-manager: apk - package-name: shuffle-backend - package-version: 1.2.3-r0 - manifest-path: /app/go.mod - installed-paths: [/app/shufflebackend, /app/go.mod] + package_manager: apk + package_name: shuffle-backend + package_version: 1.2.3-r0 + manifest_path: /app/go.mod + installed_paths: [/app/shufflebackend, /app/go.mod] hashes: - algorithm: sha256 value: abc123 - dependency-manifests: + dependency_manifests: - ecosystem: go path: /app/go.mod format: go-module - package-vulnerabilities: + package_vulnerabilities: - id: CVE-2026-12345 - package-name: musl - installed-version: 1.2.4-r2 - fixed-version: 1.2.5-r0 + package_name: musl + installed_version: 1.2.4-r2 + fixed_version: 1.2.5-r0 severity: high scanner: trivy - image-digest: sha256:abc123 - scan-time: "2026-05-20T12:00:00Z" + image_digest: sha256:abc123 + scan_time: "2026-05-20T12:00:00Z" """) ) @@ -295,22 +297,22 @@ def test_node_runtime_preserves_identity_authority_inventory(self): - {port: 389, name: ldap} - {port: 88, name: kerberos} runtime: - identity-authorities: - - identity-authority-id: techvault-domain + identity_authorities: + - identity_authority_id: techvault-domain kind: domain namespace: techvault.local - domain-name: TECHVAULT + domain_name: TECHVAULT realm: TECHVAULT.LOCAL services: - - {service-id: ldap-endpoint, service: ldap, protocol: ldap, port: 389} + - {service_id: ldap-endpoint, service: ldap, protocol: ldap, port: 389} subjects: - - {subject-id: alice, kind: user, name: alice} - - {subject-id: domain-admins, kind: group, name: Domain Admins} + - {subject_id: alice, kind: user, name: alice} + - {subject_id: domain-admins, kind: group, name: Domain Admins} relationships: - - relationship-id: alice-admin - relationship-type: member-of - source-ref: alice - target-ref: domain-admins + - relationship_id: alice-admin + relationship_type: member-of + source_ref: alice + target_ref: domain-admins """) ) @@ -335,66 +337,66 @@ def test_node_runtime_preserves_file_service_inventory(self): services: - {port: 445, name: smb} runtime: - local-identity: + local_identity: users: - {username: svc-fileshare, uid: 1100, primary_gid: 1100, primary_group: svc-fileshare} - file-services: - - file-service-id: fileshare-smb + file_services: + - file_service_id: fileshare-smb service: smb protocol: smb backend: samba-4.x shares: - - share-id: public + - share_id: public name: public kind: disk - backing-path: /srv/samba/public - read-only: true + backing_path: /srv/samba/public + read_only: true browseable: true - guest-ok: true - - share-id: deploy-keys + guest_ok: true + - share_id: deploy-keys name: deploy_keys kind: disk - backing-path: /srv/samba/deploy_keys - read-only: false + backing_path: /srv/samba/deploy_keys + read_only: false browseable: false - guest-ok: false - valid-users: [svc-fileshare] - write-users: [svc-fileshare] + guest_ok: false + valid_users: [svc-fileshare] + write_users: [svc-fileshare] principals: - - principal-id: nobody + - principal_id: nobody kind: guest name: nobody - external-id: S-1-5-21-0-501 + external_id: S-1-5-21-0-501 status: enabled - credential-classification: no_credential + credential_classification: no_credential origin: built_in - - principal-id: svc-fileshare + - principal_id: svc-fileshare kind: service_account name: svc-fileshare status: enabled - credential-classification: redacted + credential_classification: redacted origin: provisioned - local-user-ref: svc-fileshare - access-rules: - - rule-id: public-read - subject-ref: nobody - resource-ref: public + local_user_ref: svc-fileshare + access_rules: + - rule_id: public-read + subject_ref: nobody + resource_ref: public action: read effect: allow basis: share_config - access-observations: - - observation-id: anon-mount-allowed - subject-ref: anonymous - resource-ref: public + access_observations: + - observation_id: anon-mount-allowed + subject_ref: anonymous + resource_ref: public action: browse outcome: allowed basis: observed_probe - filesystem-inventory: + filesystem_inventory: - path: /srv/samba/public - entry-type: directory + entry_type: directory presence: present - path: /srv/samba/deploy_keys/id_ed25519 - entry-type: file + entry_type: file presence: expected_absent description: Expected deploy-key attempted by setup, absent at capture. """) @@ -524,10 +526,19 @@ def test_objective_windows_and_workflows_resolve_refresh_dependencies(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} window: stories: [main] scripts: [timeline] @@ -537,19 +548,19 @@ def test_objective_windows_and_workflows_resolve_refresh_dependencies(self): entities: blue: {role: blue} events: - kickoff: {conditions: [health]} + kickoff: {assertions: [pre-health]} scripts: - timeline: {start-time: 0, end-time: 60, speed: 1, events: {kickoff: 10}} + timeline: {start_time: 0, end_time: 60, speed: 1, events: {kickoff: 10}} stories: main: {scripts: [timeline]} workflows: flow: start: start steps: - start: {type: objective, objective: initial, on-success: branch} + start: {type: objective, objective: initial, on_success: branch} branch: type: decision - when: {conditions: [health]} + when: {assertions: [pre-health]} then: end else: end end: {type: end} @@ -559,7 +570,7 @@ def test_objective_windows_and_workflows_resolve_refresh_dependencies(self): objective = model.objectives["evaluation.objective.initial"] workflow = model.workflows["orchestration.workflow.flow"] - assert "evaluation.condition.vm.health" in objective.success_addresses + assert "evaluation.assertion.health" in objective.success_addresses assert objective.window_story_addresses == ("orchestration.story.main",) assert objective.window_script_addresses == ("orchestration.script.timeline",) assert objective.window_event_addresses == ("orchestration.event.kickoff",) @@ -575,7 +586,7 @@ def test_objective_windows_and_workflows_resolve_refresh_dependencies(self): ] assert objective.window_references[-1].workflow_name == "flow" assert objective.window_references[-1].step_name == "branch" - assert "evaluation.condition.vm.health" in objective.ordering_dependencies + assert "evaluation.assertion.health" in objective.ordering_dependencies assert "orchestration.workflow.flow" in objective.refresh_dependencies assert workflow.referenced_objective_addresses == ("evaluation.objective.initial",) assert workflow.start_step == "start" @@ -592,17 +603,16 @@ def test_objective_windows_and_workflows_resolve_refresh_dependencies(self): assert not workflow.control_steps["branch"].state_contract.state_observable assert workflow.control_edges["start"] == ("branch",) assert workflow.control_edges["branch"] == ("end",) - assert workflow.step_condition_addresses["branch"] == ("evaluation.condition.vm.health",) - assert "evaluation.condition.vm.health" in workflow.step_predicate_addresses["branch"] + assert workflow.step_assertion_addresses["branch"] == ("evaluation.assertion.pre-health",) + assert "evaluation.assertion.pre-health" in workflow.step_predicate_addresses["branch"] assert workflow.ordering_dependencies == () assert "evaluation.objective.initial" in workflow.refresh_dependencies assert model.objectives["evaluation.objective.initial"].result_contract.supports_passed is True assert not model.diagnostics - def test_objective_window_step_outside_window_workflows_emits_diagnostic(self): - model = compile_runtime_model( - parse_sdl( - textwrap.dedent(""" + def test_objective_window_step_outside_window_workflows_fails_admission(self): + scenario = parse_sdl( + textwrap.dedent(""" name: broken-window nodes: vm: @@ -613,12 +623,20 @@ def test_objective_window_step_outside_window_workflows_emits_diagnostic(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} window: workflows: [flow] steps: [other.finish] @@ -632,20 +650,19 @@ def test_objective_window_step_outside_window_workflows_emits_diagnostic(self): steps: finish: {type: end} """), - skip_semantic_validation=True, - ) + skip_semantic_validation=True, ) - diagnostics = {(diag.code, diag.address) for diag in model.diagnostics} - assert ( - "evaluation.workflow-step-ref-workflow-outside-window", - "evaluation.objective.initial", - ) in diagnostics + with pytest.raises(SDLInstantiationError) as exc_info: + compile_runtime_model(scenario) + assert any( + "Objective 'initial' window step 'other.finish' is not part of the referenced workflows" in error + for error in exc_info.value.errors + ) - def test_missing_node_bindings_emit_diagnostics_without_crashing(self): - model = compile_runtime_model( - parse_sdl( - textwrap.dedent(""" + def test_missing_node_bindings_fail_compiler_admission(self): + scenario = parse_sdl( + textwrap.dedent(""" name: broken-bindings nodes: vm: @@ -657,22 +674,20 @@ def test_missing_node_bindings_emit_diagnostics_without_crashing(self): injects: {phish: web} roles: {web: appuser} """), - skip_semantic_validation=True, - ) + skip_semantic_validation=True, ) - codes = {diag.code for diag in model.diagnostics} - assert "provisioning.feature-template-ref-unbound" in codes - assert "evaluation.condition-template-ref-unbound" in codes - assert "orchestration.inject-template-ref-unbound" in codes - assert model.feature_bindings == {} - assert model.condition_bindings == {} - assert model.inject_bindings == {} + with pytest.raises(SDLInstantiationError) as exc_info: + compile_runtime_model(scenario) + assert exc_info.value.errors == [ + "Node 'vm' references undefined feature 'nginx'", + "Node 'vm' references undefined condition 'health'", + "Node 'vm' references undefined inject 'phish'", + ] - def test_missing_runtime_graph_refs_emit_partial_model_diagnostics(self): - model = compile_runtime_model( - parse_sdl( - textwrap.dedent(""" + def test_missing_runtime_graph_refs_fail_compiler_admission(self): + scenario = parse_sdl( + textwrap.dedent(""" name: broken-graph nodes: vm: @@ -687,14 +702,14 @@ def test_missing_runtime_graph_refs_emit_partial_model_diagnostics(self): initial: entity: blue success: - conditions: [missing-condition] + assertions: [missing-assertion] window: workflows: [missing-workflow] steps: [missing-workflow.branch, badstep] entities: blue: {role: blue} scripts: - timeline: {start-time: 0, end-time: 60, speed: 1, events: {missing-event: 10}} + timeline: {start_time: 0, end_time: 60, speed: 1, events: {missing-event: 10}} stories: main: {scripts: [missing-script]} workflows: @@ -703,30 +718,29 @@ def test_missing_runtime_graph_refs_emit_partial_model_diagnostics(self): steps: branch: type: decision - when: {conditions: [missing-condition], objectives: [missing-objective]} + when: {assertions: [missing-assertion], objectives: [missing-objective]} then: finish else: finish finish: {type: end} """), - skip_semantic_validation=True, - ) + skip_semantic_validation=True, ) - codes = {diag.code for diag in model.diagnostics} - assert "orchestration.event-ref-unbound" in codes - assert "orchestration.script-ref-unbound" in codes - assert "evaluation.condition-ref-unbound" in codes - assert "evaluation.workflow-ref-unbound" in codes - assert "evaluation.workflow-step-ref-workflow-unbound" in codes - assert "evaluation.workflow-step-ref-invalid-format" in codes - assert "orchestration.objective-ref-unbound" in codes - - assert model.scripts["orchestration.script.timeline"].event_addresses == () - assert model.stories["orchestration.story.main"].script_addresses == () - assert model.objectives["evaluation.objective.initial"].success_addresses == () - assert model.objectives["evaluation.objective.initial"].window_workflow_addresses == () - assert model.objectives["evaluation.objective.initial"].window_step_refs == () - assert model.workflows["orchestration.workflow.flow"].referenced_objective_addresses == () + with pytest.raises(SDLInstantiationError) as exc_info: + compile_runtime_model(scenario) + errors = exc_info.value.errors + assert any("Script 'timeline' references undefined event 'missing-event'" in error for error in errors) + assert any("Story 'main' references undefined script 'missing-script'" in error for error in errors) + assert any( + "Objective 'initial' references undefined assertion 'missing-assertion'" in error for error in errors + ) + assert any("Objective 'initial' references undefined workflow 'missing-workflow'" in error for error in errors) + assert any("window step 'missing-workflow.branch' references undefined workflow" in error for error in errors) + assert any("window step 'badstep' must use '.' syntax" in error for error in errors) + assert any( + "Workflow 'flow' step 'branch' references undefined objective 'missing-objective'" in error + for error in errors + ) def test_workflow_with_retry_and_step_state_compiles(self): model = compile_runtime_model( @@ -741,15 +755,24 @@ def test_workflow_with_retry_and_step_state_compiles(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} entities: blue: {role: blue} objectives: attempt: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} recover: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: retry: start: attempt-loop @@ -757,13 +780,13 @@ def test_workflow_with_retry_and_step_state_compiles(self): attempt-loop: type: retry objective: attempt - on-success: branch - max-attempts: 3 - on-exhausted: handle-error + on_success: branch + max_attempts: 3 + on_exhausted: handle-error branch: type: decision when: - conditions: [health] + assertions: [pre-health] steps: - step: attempt-loop outcomes: [succeeded] @@ -772,7 +795,7 @@ def test_workflow_with_retry_and_step_state_compiles(self): handle-error: type: objective objective: recover - on-success: done + on_success: done done: {type: end} """) ) @@ -796,7 +819,7 @@ def test_workflow_with_retry_and_step_state_compiles(self): "evaluation.objective.attempt", "evaluation.objective.recover", ) - assert "evaluation.condition.vm.health" in workflow.step_predicate_addresses["branch"] + assert "evaluation.assertion.pre-health" in workflow.step_predicate_addresses["branch"] assert not model.diagnostics def test_parallel_join_compiles_as_barrier_with_typed_predicate(self): @@ -812,18 +835,26 @@ def test_parallel_join_compiles_as_barrier_with_typed_predicate(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: left: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} right: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} recover: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: flow: start: fanout @@ -832,15 +863,15 @@ def test_parallel_join_compiles_as_barrier_with_typed_predicate(self): type: parallel branches: [left-branch, right-branch] join: joined - on-failure: recover-step + on_failure: recover-step left-branch: type: objective objective: left - on-success: joined + on_success: joined right-branch: type: objective objective: right - on-success: joined + on_success: joined joined: type: join next: branch @@ -850,13 +881,13 @@ def test_parallel_join_compiles_as_barrier_with_typed_predicate(self): steps: - step: left-branch outcomes: [succeeded] - min-attempts: 2 + min_attempts: 2 then: finish else: recover-step recover-step: type: objective objective: recover - on-success: finish + on_success: finish finish: {type: end} """) ) @@ -893,12 +924,23 @@ def test_parallel_join_compiles_as_barrier_with_typed_predicate(self): } assert not model.diagnostics - def test_module_expansion_compiles_like_flat_scenario(self, tmp_path: Path): + def test_module_expansion_compiles_namespaced_runtime_addresses(self, tmp_path: Path): imported = tmp_path / "shared.yaml" imported.write_text( """ name: shared version: 1.0.0 +module: + id: aces/shared + version: 1.0.0 + exports: + nodes: [vm] + conditions: [health] + propositions: [health] + assertions: [health] + entities: [blue] + objectives: [validate] + workflows: [response] nodes: vm: type: vm @@ -910,6 +952,14 @@ def test_module_expansion_compiles_like_flat_scenario(self, tmp_path: Path): health: command: /bin/true interval: 15 +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: role: blue @@ -917,7 +967,7 @@ def test_module_expansion_compiles_like_flat_scenario(self, tmp_path: Path): validate: entity: blue success: - conditions: [health] + assertions: [health] workflows: response: start: run @@ -925,7 +975,7 @@ def test_module_expansion_compiles_like_flat_scenario(self, tmp_path: Path): run: type: objective objective: validate - on-success: finish + on_success: finish finish: type: end """, @@ -942,51 +992,11 @@ def test_module_expansion_compiles_like_flat_scenario(self, tmp_path: Path): """, encoding="utf-8", ) - flat = parse_sdl( - textwrap.dedent( - """ - name: root - nodes: - shared.vm: - type: vm - os: linux - resources: {ram: 1 gib, cpu: 1} - conditions: {shared.health: ops} - roles: {ops: operator} - conditions: - shared.health: - command: /bin/true - interval: 15 - entities: - shared.blue: - role: blue - objectives: - shared.validate: - entity: shared.blue - success: - conditions: [shared.health] - workflows: - shared.response: - start: run - steps: - run: - type: objective - objective: shared.validate - on-success: finish - finish: - type: end - """ - ) - ) - expanded_model = compile_runtime_model(parse_sdl_file(root)) - flat_model = compile_runtime_model(flat) assert not expanded_model.diagnostics - assert not flat_model.diagnostics - assert expanded_model.workflows.keys() == flat_model.workflows.keys() - assert expanded_model.objectives.keys() == flat_model.objectives.keys() - assert expanded_model.condition_bindings.keys() == flat_model.condition_bindings.keys() + assert set(expanded_model.workflows) == {"orchestration.workflow.shared.response"} + assert set(expanded_model.objectives) == {"evaluation.objective.shared.validate"} workflow = expanded_model.workflows["orchestration.workflow.shared.response"] assert workflow.referenced_objective_addresses == ("evaluation.objective.shared.validate",) assert workflow.control_steps["run"].objective_address == "evaluation.objective.shared.validate" @@ -1006,12 +1016,21 @@ def test_workflow_switch_call_and_timeout_compile_to_explicit_contracts(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} + propositions: + health: + 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} + assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: child: start: run @@ -1019,7 +1038,7 @@ def test_workflow_switch_call_and_timeout_compile_to_explicit_contracts(self): run: type: objective objective: validate - on-success: finish + on_success: finish finish: {type: end} parent: start: route @@ -1028,13 +1047,13 @@ def test_workflow_switch_call_and_timeout_compile_to_explicit_contracts(self): route: type: switch cases: - - when: {conditions: [health]} + - when: {assertions: [pre-health]} next: delegate default: finish delegate: type: call workflow: child - on-success: finish + on_success: finish finish: {type: end} """ ) @@ -1071,12 +1090,20 @@ def test_workflow_compensation_compiles_to_explicit_contracts(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} + propositions: + health: + 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} + assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: {role: blue} objectives: validate: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: rollback: start: finish @@ -1092,9 +1119,9 @@ def test_workflow_compensation_compiles_to_explicit_contracts(self): run: type: objective objective: validate - compensate-with: rollback - on-success: finish - on-failure: finish + compensate_with: rollback + on_success: finish + on_failure: finish finish: {type: end} """ ) diff --git a/implementations/python/tests/test_runtime_network_detection.py b/implementations/python/tests/test_runtime_network_detection.py index 5253d632a..261c6c1d3 100644 --- a/implementations/python/tests/test_runtime_network_detection.py +++ b/implementations/python/tests/test_runtime_network_detection.py @@ -186,7 +186,7 @@ def test_vm_runtime_network_detection_engine_inventory() -> None: assert engine.control_channels[0].kind == RuntimeNetworkDetectionControlChannelKind.UNIX_SOCKET -def test_parser_accepts_kebab_case_runtime_network_detection_engines() -> None: +def test_parser_accepts_canonical_runtime_network_detection_engines() -> None: scenario = parse_sdl( """ name: detection-engine-parser @@ -196,36 +196,36 @@ def test_parser_accepts_kebab_case_runtime_network_detection_engines() -> None: type: vm resources: {ram: 2 gib, cpu: 2} runtime: - network-sensors: - - network-sensor-id: suricata-sensor + network_sensors: + - network_sensor_id: suricata-sensor implementation: SURICATA - sensor-kind: ids - monitoring-posture: passive - capture-mode: pcap - monitored-network-refs: [dmz-net] - network-detection-engines: - - network-detection-engine-id: suricata-engine + sensor_kind: ids + monitoring_posture: passive + capture_mode: pcap + monitored_network_refs: [dmz-net] + network_detection_engines: + - network_detection_engine_id: suricata-engine implementation: SURICATA - engine-kind: ids - sensor-ref: suricata-sensor - app-layer-protocols: [http, tls, dns] - rule-sources: - - source-id: local-rules + engine_kind: ids + sensor_ref: suricata-sensor + app_layer_protocols: [http, tls, dns] + rule_sources: + - source_id: local-rules kind: local format: suricata-rule - rule-count: "46" - network-sets: - - set-id: home-net + rule_count: "46" + network_sets: + - set_id: home-net kind: home-net name: HOME_NET - network-refs: [dmz-net] - output-streams: - - stream-id: eve-json + network_refs: [dmz-net] + output_streams: + - stream_id: eve-json format: eve-json - event-types: [alert, dns] + event_types: [alert, dns] enabled: true - control-channels: - - channel-id: command-socket + control_channels: + - channel_id: command-socket kind: unix-socket path: /var/run/suricata-command.socket capabilities: rule-reload diff --git a/implementations/python/tests/test_runtime_network_sensor.py b/implementations/python/tests/test_runtime_network_sensor.py index 9757f1c6c..c83f4fca5 100644 --- a/implementations/python/tests/test_runtime_network_sensor.py +++ b/implementations/python/tests/test_runtime_network_sensor.py @@ -107,7 +107,7 @@ def test_vm_runtime_network_sensor_inventory() -> None: assert sensor.monitored_network_refs == ["dmz-net", "internal-net", "security-net"] -def test_parser_accepts_kebab_case_runtime_network_sensors() -> None: +def test_parser_accepts_canonical_runtime_network_sensors() -> None: scenario = parse_sdl( """ name: network-sensor-parser @@ -119,15 +119,15 @@ def test_parser_accepts_kebab_case_runtime_network_sensors() -> None: runtime: network: endpoints: - - {network: dmz-net, ip-address: 172.20.1.50} - network-sensors: - - network-sensor-id: suricata + - {network: dmz-net, ip_address: 172.20.1.50} + network_sensors: + - network_sensor_id: suricata implementation: SURICATA - sensor-kind: ids - monitoring-posture: passive - capture-mode: pcap - capture-interfaces: any - monitored-network-refs: [dmz-net] + sensor_kind: ids + monitoring_posture: passive + capture_mode: pcap + capture_interfaces: any + monitored_network_refs: [dmz-net] infrastructure: dmz-net: 1 suricata: {count: 1, links: [dmz-net]} diff --git a/implementations/python/tests/test_runtime_planner.py b/implementations/python/tests/test_runtime_planner.py index 806f20430..765f2a306 100644 --- a/implementations/python/tests/test_runtime_planner.py +++ b/implementations/python/tests/test_runtime_planner.py @@ -201,11 +201,9 @@ def test_cyclic_provisioning_dependencies_fail_closed(self): assert ("provisioning.ordering-cycle", "provision.node.a") in diagnostics assert not execution_plan.is_valid - def test_cyclic_objective_dependencies_fail_closed(self): - execution_plan = plan( - compile_runtime_model( - parse_sdl( - textwrap.dedent(""" + def test_compiler_admission_rejects_cyclic_objective_dependencies(self): + scenario = parse_sdl( + textwrap.dedent(""" name: objective-cycle nodes: vm: @@ -216,28 +214,32 @@ def test_cyclic_objective_dependencies_fail_closed(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} entities: blue: {role: blue} objectives: first: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} depends_on: [second] second: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} depends_on: [first] """), - skip_semantic_validation=True, - ) - ), - create_stub_manifest(), + skip_semantic_validation=True, ) - diagnostics = {(diag.code, diag.address) for diag in execution_plan.diagnostics} - - assert ("evaluation.ordering-cycle", "evaluation.objective.first") in diagnostics - assert not execution_plan.is_valid + with pytest.raises(SDLInstantiationError, match="Objective dependency graph contains a cycle"): + compile_runtime_model(scenario) def test_dependency_changes_propagate_through_evaluation_graph(self): old_model = compile_runtime_model( @@ -252,12 +254,21 @@ def test_dependency_changes_propagate_through_evaluation_graph(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} entities: blue: {role: blue} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} """) ) old_plan = plan(old_model, create_stub_manifest()) @@ -275,24 +286,32 @@ def test_dependency_changes_propagate_through_evaluation_graph(self): roles: {ops: operator} conditions: health: {command: /bin/false, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} entities: blue: {role: blue} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} """, snapshot, ) eval_actions = {op.address: op.action.value for op in new_plan.evaluation.operations} assert eval_actions["evaluation.condition.vm.health"] == "update" - # Objective success references the condition, so a condition change must - # propagate to the objective evaluation resource (metric->evaluation->tlo->goal - # chain removed by ADR-073). + # The condition and proposition changed independently. The objective + # refresh follows the proposition/assertion dependency chain. assert eval_actions["evaluation.objective.initial"] == "update" - def test_ambiguous_condition_refs_fail_closed(self): + def test_assertion_refs_remain_unambiguous_with_multiple_condition_bindings(self): execution_plan = plan( compile_runtime_model( _scenario(""" @@ -312,16 +331,25 @@ def test_ambiguous_condition_refs_fail_closed(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} events: - kickoff: {conditions: [health]} + kickoff: {assertions: [pre-health]} """) ), create_stub_manifest(), ) - codes = {diag.code for diag in execution_plan.diagnostics} - assert "orchestration.condition-ref-ambiguous" in codes - assert not execution_plan.is_valid + kickoff = execution_plan.model.events["orchestration.event.kickoff"] + assert kickoff.assertion_addresses == ("evaluation.assertion.pre-health",) + assert execution_plan.is_valid def test_top_level_inject_refs_resolve_directly(self): execution_plan = plan( @@ -346,7 +374,7 @@ def test_top_level_inject_refs_resolve_directly(self): assert kickoff.inject_addresses == ("orchestration.inject.mail",) assert execution_plan.is_valid - def test_unbound_condition_and_inject_refs_invalidate_plan(self): + def test_compiler_admission_rejects_unbound_inject_refs(self): scenario = _scenario(""" name: unbound nodes: @@ -356,6 +384,15 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): resources: {ram: 1 gib, cpu: 1} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} injects: mail: {source: inbox} entities: @@ -363,22 +400,14 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): objectives: check: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} events: - kickoff: {conditions: [health], injects: [mail]} + kickoff: {assertions: [pre-health], injects: [mail]} """) scenario.injects = {} - execution_plan = plan( - compile_runtime_model(scenario), - create_stub_manifest(), - ) - - codes = {diag.code for diag in execution_plan.diagnostics} - assert "evaluation.condition-ref-unbound" in codes - assert "orchestration.condition-ref-unbound" in codes - assert "orchestration.inject-ref-unbound" in codes - assert not execution_plan.is_valid + with pytest.raises(SDLInstantiationError, match="references undefined inject 'mail'"): + compile_runtime_model(scenario) @pytest.mark.parametrize( ("orchestrator_kwargs", "scenario_yaml", "expected_code"), @@ -387,7 +416,7 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): { "supported_sections": frozenset({"workflows"}), "supports_workflows": True, - "supports_condition_refs": False, + "supports_assertion_refs": False, "supported_workflow_features": frozenset({WorkflowFeature.DECISION}), }, """ @@ -401,19 +430,28 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} workflows: flow: start: branch steps: branch: type: decision - when: {conditions: [health]} + when: {assertions: [pre-health]} then: finish else: finish finish: {type: end} """, - "orchestrator.condition-refs-unsupported", - id="condition-refs-unsupported", + "orchestrator.assertion-refs-unsupported", + id="assertion-refs-unsupported", ), pytest.param( { @@ -434,10 +472,19 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): blue: {role: blue} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: defend: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: flow: start: attempt @@ -445,8 +492,8 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): attempt: type: retry objective: defend - on-success: finish - max-attempts: 3 + on_success: finish + max_attempts: 3 finish: {type: end} """, "orchestrator.workflow-feature-unsupported", @@ -465,10 +512,19 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): blue: {role: blue} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: defend: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: flow: start: validate @@ -476,7 +532,7 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): validate: type: objective objective: defend - on-success: branch + on_success: branch branch: type: decision when: @@ -503,10 +559,19 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): blue: {role: blue} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: defend: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: flow: start: validate @@ -514,15 +579,15 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): validate: type: retry objective: defend - on-success: branch - max-attempts: 3 + on_success: branch + max_attempts: 3 branch: type: decision when: steps: - step: validate outcomes: [succeeded] - min-attempts: 2 + min_attempts: 2 then: finish else: finish finish: {type: end} @@ -542,13 +607,22 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): blue: {role: blue} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: left: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} right: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} workflows: flow: start: fanout @@ -560,11 +634,11 @@ def test_unbound_condition_and_inject_refs_invalidate_plan(self): left-branch: type: objective objective: left - on-success: joined + on_success: joined right-branch: type: objective objective: right - on-success: joined + on_success: joined joined: type: join next: finish @@ -592,7 +666,7 @@ def test_workflow_capability_requires_orchestrator_support(self, orchestrator_kw assert expected_code in codes assert not execution_plan.is_valid - def test_workflow_condition_bindings_force_workflow_refresh(self): + def test_workflow_assertion_changes_force_workflow_refresh(self): old_model = compile_runtime_model( _scenario(""" name: workflow @@ -605,10 +679,19 @@ def test_workflow_condition_bindings_force_workflow_refresh(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} entities: blue: {role: blue} workflows: @@ -617,7 +700,7 @@ def test_workflow_condition_bindings_force_workflow_refresh(self): steps: branch: type: decision - when: {conditions: [health]} + when: {assertions: [pre-health]} then: finish else: finish finish: {type: end} @@ -638,10 +721,19 @@ def test_workflow_condition_bindings_force_workflow_refresh(self): roles: {ops: operator} conditions: health: {command: /bin/false, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} entities: blue: {role: blue} workflows: @@ -650,7 +742,7 @@ def test_workflow_condition_bindings_force_workflow_refresh(self): steps: branch: type: decision - when: {conditions: [health]} + when: {assertions: [pre-health]} then: finish else: finish finish: {type: end} @@ -674,21 +766,33 @@ def test_cross_domain_refresh_dependencies_do_not_drive_ordering(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} injects: mail: {source: inbox} events: - kickoff: {conditions: [health], injects: [mail]} + kickoff: {assertions: [pre-health], injects: [mail]} """ old_model = compile_runtime_model(_scenario(base)) kickoff = old_model.events["orchestration.event.kickoff"] - assert "evaluation.condition.vm.health" not in kickoff.ordering_dependencies - assert "evaluation.condition.vm.health" in kickoff.refresh_dependencies + assert "evaluation.assertion.pre-health" not in kickoff.ordering_dependencies + assert "evaluation.assertion.pre-health" in kickoff.refresh_dependencies assert "orchestration.inject.mail" in kickoff.ordering_dependencies old_plan = plan(old_model, create_stub_manifest()) snapshot = _snapshot_from_plan(old_plan) new_plan = _plan_with_snapshot( - base.replace("/bin/true", "/bin/false"), + base.replace( + "urn:aces:declared-property:runtime", + "urn:aces:declared-property:runtime-v2", + ), snapshot, ) @@ -708,10 +812,19 @@ def test_objective_window_refs_are_refresh_only(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} window: workflows: [flow] steps: [flow.branch] @@ -723,7 +836,7 @@ def test_objective_window_refs_are_refresh_only(self): steps: branch: type: decision - when: {conditions: [health]} + when: {assertions: [pre-health]} then: finish else: finish finish: {type: end} @@ -737,6 +850,9 @@ def test_objective_window_refs_are_refresh_only(self): assert "orchestration.workflow.flow" in objective.refresh_dependencies assert execution_plan.evaluation.startup_order == [ "evaluation.condition.vm.health", + "evaluation.proposition.health", + "evaluation.assertion.health", + "evaluation.assertion.pre-health", "evaluation.objective.initial", ] @@ -752,10 +868,19 @@ def test_objective_updates_when_window_dependencies_change(self): roles: {ops: operator} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} window: scripts: [timeline] events: [kickoff] @@ -764,9 +889,9 @@ def test_objective_updates_when_window_dependencies_change(self): entities: blue: {role: blue} events: - kickoff: {conditions: [health], description: kickoff} + kickoff: {assertions: [pre-health], description: kickoff} scripts: - timeline: {start-time: 0, end-time: 60, speed: 1, events: {kickoff: 10}} + timeline: {start_time: 0, end_time: 60, speed: 1, events: {kickoff: 10}} workflows: flow: description: primary @@ -774,7 +899,7 @@ def test_objective_updates_when_window_dependencies_change(self): steps: branch: type: decision - when: {conditions: [health]} + when: {assertions: [pre-health]} then: finish else: finish finish: {type: end} @@ -783,7 +908,7 @@ def test_objective_updates_when_window_dependencies_change(self): snapshot = _snapshot_from_plan(old_plan) changed_variants = [ - base.replace("end-time: 60", "end-time: 120"), + base.replace("end_time: 60", "end_time: 120"), base.replace("description: kickoff", "description: changed"), base.replace("description: primary", "description: updated"), base.replace("then: finish", "then: finish\n description: changed"), @@ -905,29 +1030,38 @@ def test_semantic_capability_validation_catches_real_requirements(self): count: 1 properties: {cidr: 10.0.0.0/24, gateway: 10.0.0.1} acls: - - {direction: in, from-net: corp, action: allow} + - {direction: in, from_net: corp, action: allow} dc: {count: 1, links: [corp]} accounts: admin: {username: administrator, node: dc, spn: LDAP/dc.example.local} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} objectives: defend: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} entities: blue: {role: blue} events: - kickoff: {conditions: [health]} + kickoff: {assertions: [pre-health]} scripts: - timeline: {start-time: 0, end-time: 60, speed: 1, events: {kickoff: 10}} + timeline: {start_time: 0, end_time: 60, speed: 1, events: {kickoff: 10}} stories: main: {scripts: [timeline]} workflows: flow: start: start steps: - start: {type: objective, objective: defend, on-success: end} + start: {type: objective, objective: defend, on_success: end} end: {type: end} """) ) @@ -1020,7 +1154,7 @@ def test_variable_backed_os_defaults_must_be_valid_for_nodes_os(self): vm: {type: vm, os: '${os_name}', resources: {ram: 1 gib, cpu: 1}} """) ) - assert "nodes.vm.os" in str(exc.value) + assert "/nodes/vm/os" in str(exc.value) def test_variable_backed_os_without_allowed_values_uses_instantiated_default(self): manifest = _limited_backend_manifest( @@ -1126,6 +1260,12 @@ def test_imported_module_allowed_values_enforce_against_backend(self, tmp_path): """ name: shared version: 1.0.0 +module: + id: aces/shared + version: 1.0.0 + parameters: [os_name] + exports: + nodes: [vm] variables: os_name: type: string @@ -1193,7 +1333,7 @@ def test_variable_backed_counts_defaults_must_be_valid_for_infrastructure_count( vm: ${node_count} """) ) - assert "infrastructure.vm.count" in str(exc.value) + assert "/infrastructure/vm/count" in str(exc.value) def test_variable_backed_os_allowed_values_fail_when_pre_instantiated(self): # Manager-path coverage: when the caller instantiates upstream and @@ -1222,7 +1362,6 @@ def test_variable_backed_os_allowed_values_fail_when_pre_instantiated(self): nodes: vm: {type: vm, os: '${os_name}', resources: {ram: 1 gib, cpu: 1}} """), - validate_semantics=False, ) execution_plan = plan(compile_runtime_model(instantiated), manifest) @@ -1354,25 +1493,34 @@ def test_dependency_ordering_across_domain_plans(self): admin: {username: admin, node: web} conditions: health: {command: /bin/true, interval: 15} +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} + pre-health: {proposition: health, role: precondition, polarity: positive} injects: mail: {source: inbox} objectives: initial: entity: blue - success: {conditions: [health]} + success: {assertions: [health]} entities: blue: {role: blue} events: - kickoff: {conditions: [health], injects: [mail]} + kickoff: {assertions: [pre-health], injects: [mail]} scripts: - timeline: {start-time: 0, end-time: 60, speed: 1, events: {kickoff: 10}} + timeline: {start_time: 0, end_time: 60, speed: 1, events: {kickoff: 10}} stories: main: {scripts: [timeline]} workflows: flow: start: start steps: - start: {type: objective, objective: initial, on-success: end} + start: {type: objective, objective: initial, on_success: end} end: {type: end} """) ), diff --git a/implementations/python/tests/test_runtime_proposition_truth.py b/implementations/python/tests/test_runtime_proposition_truth.py new file mode 100644 index 000000000..c25e3ff3d --- /dev/null +++ b/implementations/python/tests/test_runtime_proposition_truth.py @@ -0,0 +1,128 @@ +"""Reference runtime admission for portable proposition truth results.""" + +from __future__ import annotations + +from copy import deepcopy + +from aces_contracts.planning import RuntimeDomain +from aces_contracts.runtime_state import RuntimeSnapshot, SnapshotEntry +from aces_runtime.proposition_truth_contracts import proposition_truth_contract_diagnostics +from test_truth_result_contracts import _observed_result + + +def _semantic_entries(payload: dict[str, object]) -> dict[str, SnapshotEntry]: + proposition_address = str(payload["proposition_address"]) + assertion_address = str(payload["assertion_address"]) + polarity = str(payload["assertion_polarity"]) + return { + proposition_address: SnapshotEntry( + address=proposition_address, + domain=RuntimeDomain.EVALUATION, + resource_type="proposition", + payload={"name": "service-available", "evaluation_basis": payload["evaluation_basis"]}, + ), + assertion_address: SnapshotEntry( + address=assertion_address, + domain=RuntimeDomain.EVALUATION, + resource_type="assertion", + payload={"proposition_address": proposition_address, "polarity": polarity}, + ), + } + + +def test_runtime_snapshot_accepts_typed_truth_result_separate_from_lifecycle_result() -> None: + payload = _observed_result() + snapshot = RuntimeSnapshot( + entries=_semantic_entries(payload), + proposition_truth_results={payload["assertion_address"]: payload}, + evaluation_results={ + "evaluation.objective.restore-service": { + "state_schema_version": "evaluation-result-state/v1", + "resource_type": "objective", + "run_id": "run-42", + "status": "ready", + "observed_at": "2026-07-12T08:00:00Z", + "updated_at": "2026-07-12T08:00:00Z", + "passed": True, + } + }, + ) + + assert proposition_truth_contract_diagnostics(snapshot) == [] + assert snapshot.proposition_truth_results != snapshot.evaluation_results + + +def test_runtime_rejects_truth_result_key_that_hides_assertion_identity() -> None: + payload = _observed_result() + snapshot = RuntimeSnapshot(proposition_truth_results={"opaque-result-key": payload}) + + diagnostics = proposition_truth_contract_diagnostics(snapshot) + assert [diagnostic.code for diagnostic in diagnostics] == ["runtime.proposition-truth-contract-invalid"] + assert "must equal assertion_address" in diagnostics[0].message + + +def test_runtime_rejects_backend_true_without_required_evidence() -> None: + payload = deepcopy(_observed_result()) + payload["evidence_refs"] = [] + snapshot = RuntimeSnapshot(proposition_truth_results={payload["assertion_address"]: payload}) + + diagnostics = proposition_truth_contract_diagnostics(snapshot) + assert len(diagnostics) == 1 + assert "evidence_refs" in diagnostics[0].message + + +def test_runtime_rejects_truth_result_for_unadmitted_semantic_entries() -> None: + payload = _observed_result() + snapshot = RuntimeSnapshot(proposition_truth_results={payload["assertion_address"]: payload}) + + diagnostics = proposition_truth_contract_diagnostics(snapshot) + assert {diagnostic.message for diagnostic in diagnostics} == { + "proposition_address must reference an admitted proposition entry.", + "assertion_address must reference an admitted assertion entry.", + } + + +def test_runtime_rejects_assertion_entry_that_changes_meaning() -> None: + payload = _observed_result() + entries = _semantic_entries(payload) + assertion_address = str(payload["assertion_address"]) + entries[assertion_address] = SnapshotEntry( + address=assertion_address, + domain=RuntimeDomain.EVALUATION, + resource_type="assertion", + payload={ + "proposition_address": "evaluation.proposition.other", + "polarity": "negative", + }, + ) + snapshot = RuntimeSnapshot( + entries=entries, + proposition_truth_results={assertion_address: payload}, + ) + + diagnostics = proposition_truth_contract_diagnostics(snapshot) + assert {diagnostic.message for diagnostic in diagnostics} == { + "assertion entry proposition_address must match the truth result.", + "assertion entry polarity must match the truth result.", + } + + +def test_runtime_rejects_truth_result_that_changes_proposition_basis() -> None: + payload = _observed_result() + entries = _semantic_entries(payload) + proposition_address = str(payload["proposition_address"]) + entries[proposition_address] = SnapshotEntry( + address=proposition_address, + domain=RuntimeDomain.EVALUATION, + resource_type="proposition", + payload={"name": "service-available", "evaluation_basis": "declared_state"}, + ) + snapshot = RuntimeSnapshot( + entries=entries, + proposition_truth_results={payload["assertion_address"]: payload}, + ) + + diagnostics = proposition_truth_contract_diagnostics(snapshot) + assert [diagnostic.message for diagnostic in diagnostics] == [ + "proposition entry evaluation_basis must match the truth result." + ] diff --git a/implementations/python/tests/test_runtime_scheduled_job.py b/implementations/python/tests/test_runtime_scheduled_job.py index 9e7c2b511..182346f49 100644 --- a/implementations/python/tests/test_runtime_scheduled_job.py +++ b/implementations/python/tests/test_runtime_scheduled_job.py @@ -42,12 +42,12 @@ def test_scheduled_job_full_inventory() -> None: def test_scheduled_job_id_rejects_empty() -> None: - with pytest.raises(ValidationError, match="scheduled_job_id must be a non-empty string"): + with pytest.raises(ValidationError, match="scheduled_job_id must be a portable SDL identifier"): RuntimeScheduledJob(**_job(scheduled_job_id="")) def test_scheduled_job_id_rejects_variable_placeholder() -> None: - with pytest.raises(ValidationError, match="scheduled_job_id must be a stable identifier"): + with pytest.raises(ValidationError, match="scheduled_job_id must be a portable SDL identifier"): RuntimeScheduledJob(**_job(scheduled_job_id="${job_id}")) diff --git a/implementations/python/tests/test_runtime_security_monitoring.py b/implementations/python/tests/test_runtime_security_monitoring.py index 6fd7573de..121c6ae61 100644 --- a/implementations/python/tests/test_runtime_security_monitoring.py +++ b/implementations/python/tests/test_runtime_security_monitoring.py @@ -292,7 +292,7 @@ def test_detection_definition_model_preserves_wazuh_semantics() -> None: assert definition.mitre_attack_ids == ["T1558.003"] -def test_parser_accepts_kebab_case_runtime_security_monitoring_managers() -> None: +def test_parser_accepts_canonical_runtime_security_monitoring_managers() -> None: scenario = parse_sdl( """ name: security-monitoring-parser @@ -303,40 +303,40 @@ def test_parser_accepts_kebab_case_runtime_security_monitoring_managers() -> Non services: - {port: 55000, name: wazuh-api} runtime: - security-monitoring-managers: - - security-monitoring-manager-id: techvault-wazuh + security_monitoring_managers: + - security_monitoring_manager_id: techvault-wazuh service: wazuh-api implementation: WAZUH - manager-kind: siem + manager_kind: siem listeners: - - listener-id: manager-api + - listener_id: manager-api service: wazuh-api role: api - auth-required: true - content-sets: - - content-id: wazuh-ruleset + auth_required: true + content_sets: + - content_id: wazuh-ruleset kind: rule-corpus format: wazuh-rule-xml - file-count: 173 - detection-definitions: - - definition-id: rule-301010 + file_count: 173 + detection_definitions: + - definition_id: rule-301010 engine: WAZUH - definition-kind: correlation-rule - native-id: "301010" - content-set-ref: wazuh-ruleset - source-file-ref: /var/ossec/etc/rules/ad_rules.xml - source-start-line: 12 - source-end-line: 35 - digest-algorithm: sha256 - canonical-digest: "1111111111111111111111111111111111111111111111111111111111111111" + definition_kind: correlation-rule + native_id: "301010" + content_set_ref: wazuh-ruleset + source_file_ref: /var/ossec/etc/rules/ad_rules.xml + source_start_line: 12 + source_end_line: 35 + digest_algorithm: sha256 + canonical_digest: "1111111111111111111111111111111111111111111111111111111111111111" loaded: true - parser-accepted: true + parser_accepted: true level: 10 - field-predicates: + field_predicates: - field: win.system.eventID operator: equals value: "4769" - mitre-attack-ids: [T1558.003] + mitre_attack_ids: [T1558.003] """ ) diff --git a/implementations/python/tests/test_runtime_service_listeners.py b/implementations/python/tests/test_runtime_service_listeners.py index e8eb525f9..a17d8b8a8 100644 --- a/implementations/python/tests/test_runtime_service_listeners.py +++ b/implementations/python/tests/test_runtime_service_listeners.py @@ -99,7 +99,7 @@ def test_vm_runtime_service_listener_surface() -> None: assert listener.readiness.criteria == "HTTP 200" -def test_parser_accepts_kebab_case_runtime_service_listeners() -> None: +def test_parser_accepts_canonical_runtime_service_listeners() -> None: scenario = parse_sdl( """ name: listener-parser @@ -112,15 +112,15 @@ def test_parser_accepts_kebab_case_runtime_service_listeners() -> None: runtime: processes: - {name: nginx, pid: 42} - service-listeners: - - service-listener-id: nginx-http-ipv4 + service_listeners: + - service_listener_id: nginx-http-ipv4 service: http address: 0.0.0.0 port: 80 protocol: TCP - address-family: ipv4 + address_family: ipv4 scope: wildcard - process-ref: nginx + process_ref: nginx readiness: probe: GET / criteria: HTTP 200 @@ -248,10 +248,10 @@ def test_runtime_service_listener_published_port_match_defers_unresolved_listene scenario = Scenario( name="listeners", variables={ - "HTTP_PORT": {"type": "integer", "default": 80}, - "TRANSPORT": {"type": "string", "default": "tcp"}, + "http_port": {"type": "integer", "default": 80}, + "transport": {"type": "string", "default": "tcp"}, }, - nodes={"misp": _node(_listener(port="${HTTP_PORT}", protocol="${TRANSPORT}"))}, + nodes={"misp": _node(_listener(port="${http_port}", protocol="${transport}"))}, ) assert _validate(scenario) == [] @@ -273,66 +273,66 @@ def test_runtime_service_listeners_encode_misp_listener_facts() -> None: - {name: nginx, pid: 11} - {name: supervisord, pid: 1} network: - published-ports: - - {container-port: 80, protocol: tcp, host-ip: 0.0.0.0, host-port: 80} - - {container-port: 443, protocol: tcp, host-ip: 0.0.0.0, host-port: 443} - service-listeners: - - service-listener-id: nginx-http-ipv4 + published_ports: + - {container_port: 80, protocol: tcp, host_ip: 0.0.0.0, host_port: 80} + - {container_port: 443, protocol: tcp, host_ip: 0.0.0.0, host_port: 443} + service_listeners: + - service_listener_id: nginx-http-ipv4 service: http address: 0.0.0.0 port: 80 protocol: tcp - address-family: ipv4 + address_family: ipv4 scope: wildcard - process-ref: nginx - process-name: nginx - published-port-refs: - - {container-port: 80, protocol: tcp, host-ip: 0.0.0.0, host-port: 80} - - service-listener-id: nginx-http-ipv6 + process_ref: nginx + process_name: nginx + published_port_refs: + - {container_port: 80, protocol: tcp, host_ip: 0.0.0.0, host_port: 80} + - service_listener_id: nginx-http-ipv6 service: http address: "::" port: 80 protocol: tcp - address-family: ipv6 + address_family: ipv6 scope: wildcard - process-ref: nginx - - service-listener-id: nginx-https-ipv4 + process_ref: nginx + - service_listener_id: nginx-https-ipv4 service: https address: 0.0.0.0 port: 443 protocol: tcp - address-family: ipv4 + address_family: ipv4 scope: wildcard - process-ref: nginx - - service-listener-id: nginx-https-ipv6 + process_ref: nginx + - service_listener_id: nginx-https-ipv6 service: https address: "::" port: 443 protocol: tcp - address-family: ipv6 + address_family: ipv6 scope: wildcard - process-ref: nginx - - service-listener-id: supervisord-loopback + process_ref: nginx + - service_listener_id: supervisord-loopback address: 127.0.0.1 port: 9001 protocol: tcp - address-family: ipv4 + address_family: ipv4 scope: loopback-only - process-ref: supervisord - - service-listener-id: local-runtime-loopback + process_ref: supervisord + - service_listener_id: local-runtime-loopback address: 127.0.0.1 port: 50000 protocol: tcp - address-family: ipv4 + address_family: ipv4 scope: loopback-only - process-name: runtime-local - - service-listener-id: docker-dns + process_name: runtime-local + - service_listener_id: docker-dns address: 127.0.0.11 port: 53 protocol: udp - address-family: ipv4 + address_family: ipv4 scope: loopback-only - process-name: docker-embedded-dns + process_name: docker-embedded-dns """ ) diff --git a/implementations/python/tests/test_runtime_service_units.py b/implementations/python/tests/test_runtime_service_units.py index d0e59665a..9357a25c5 100644 --- a/implementations/python/tests/test_runtime_service_units.py +++ b/implementations/python/tests/test_runtime_service_units.py @@ -476,7 +476,13 @@ def test_unknown_service_ref_rejected(self): def test_qualified_service_ref_other_node_rejected(self): s = _scenario_with_units( [{**_BASE_UNIT, "service": "nodes.other.services.ssh"}], - nodes={"other": {"type": "vm", "resources": {"ram": "1 gib", "cpu": 1}}}, + nodes={ + "other": { + "type": "vm", + "resources": {"ram": "1 gib", "cpu": 1}, + "services": [{"port": 22, "name": "ssh"}], + } + }, ) errors = _validate(s) assert any("same node" in e for e in errors) @@ -486,14 +492,14 @@ def test_variable_service_ref_deferred(self): s = Scenario( name="t", - variables={"SVC": {"type": "string", "required": True}}, + variables={"svc": {"type": "string", "required": True}}, nodes={ "box": { "type": "vm", "resources": {"ram": "1 gib", "cpu": 1}, "services": [{"port": 22, "name": "ssh"}], "runtime": { - "service_manager_units": [{**_BASE_UNIT, "service": "${SVC}"}], + "service_manager_units": [{**_BASE_UNIT, "service": "${svc}"}], }, }, }, @@ -539,14 +545,14 @@ def test_variable_unit_file_path_deferred(self): s = Scenario( name="t", - variables={"UNIT_PATH": {"type": "string", "required": True}}, + variables={"unit_path": {"type": "string", "required": True}}, nodes={ "box": { "type": "vm", "resources": {"ram": "1 gib", "cpu": 1}, "services": [{"port": 22, "name": "ssh"}], "runtime": { - "service_manager_units": [{**_BASE_UNIT, "unit_file_path": "${UNIT_PATH}"}], + "service_manager_units": [{**_BASE_UNIT, "unit_file_path": "${unit_path}"}], "filesystem_inventory": [ { "path": "/etc/systemd/system/sshd.service", diff --git a/implementations/python/tests/test_runtime_ssh_server.py b/implementations/python/tests/test_runtime_ssh_server.py index 7d229b9fa..1f9553bf3 100644 --- a/implementations/python/tests/test_runtime_ssh_server.py +++ b/implementations/python/tests/test_runtime_ssh_server.py @@ -260,7 +260,7 @@ def test_whitespace_match_id_rejected(self): def test_variable_ref_match_id_rejected(self): with pytest.raises(ValidationError) as exc: SshMatchRule( - match_id="${MATCH_ID}", + match_id="${match_id}", criteria=[SshMatchCriterion(kind=SshMatchCriterionKind.USER, pattern="kali")], ) assert "match_id" in str(exc.value) @@ -341,7 +341,7 @@ def test_whitespace_server_id_rejected(self): def test_variable_ref_server_id_rejected(self): with pytest.raises(ValidationError) as exc: - RuntimeSshServer(ssh_server_id="${SERVER_ID}", service="ssh") + RuntimeSshServer(ssh_server_id="${server_id}", service="ssh") assert "ssh_server_id" in str(exc.value) def test_service_required(self): @@ -621,17 +621,23 @@ def test_ssh_runtime_refs_rewrite_on_module_import(self, tmp_path): """ name: shared-ssh version: 1.0.0 +module: + id: aces/shared-ssh + version: 1.0.0 + exports: + nodes: [kali] + relationships: [ssh-policy] nodes: kali: type: vm services: - {port: 22, name: ssh} runtime: - ssh-servers: - - ssh-server-id: sshd-default + ssh_servers: + - ssh_server_id: sshd-default service: ssh - match-rules: - - match-id: m-kali + match_rules: + - match_id: m-kali criteria: - {kind: user, pattern: kali} relationships: diff --git a/implementations/python/tests/test_scenarios.py b/implementations/python/tests/test_scenarios.py index ab710eb0d..a635c00ac 100644 --- a/implementations/python/tests/test_scenarios.py +++ b/implementations/python/tests/test_scenarios.py @@ -114,6 +114,11 @@ def test_returns_empty_for_missing_directory(self, tmp_path): assert find_scenarios(tmp_path / "missing") == [] +def test_example_scenario_corpus_is_nonempty(): + """A stale corpus root must fail loudly, not collect zero parametrized cases.""" + assert EXAMPLE_SCENARIOS, f"No example SDL scenarios found under {EXAMPLES_DIR} (glob '*.sdl.yaml')" + + @pytest.mark.parametrize("path", EXAMPLE_SCENARIOS, ids=lambda path: path.name) def test_example_scenarios_load(path): """Every example SDL should load successfully from disk.""" @@ -137,8 +142,10 @@ def test_complex_examples_have_experiment_semantics(path): assert scenario.relationships assert scenario.content assert scenario.stories - # Post ADR-073: objective success references observable conditions (no scoring sections). - assert any(objective.success.conditions for objective in scenario.objectives.values()) + # Post ADR-079: objective success references backend-neutral assertions. + assert scenario.propositions + assert scenario.assertions + assert any(objective.success.assertions for objective in scenario.objectives.values()) def test_complex_examples_cover_new_sdl_surfaces(): diff --git a/implementations/python/tests/test_scientific_scenario_completeness.py b/implementations/python/tests/test_scientific_scenario_completeness.py new file mode 100644 index 000000000..0984badb7 --- /dev/null +++ b/implementations/python/tests/test_scientific_scenario_completeness.py @@ -0,0 +1,361 @@ +"""REV1 scientific-scenario completeness profile contract tests.""" + +from __future__ import annotations + +import json +import sys +from copy import deepcopy +from pathlib import Path + +import pytest +from pydantic import ValidationError + +REPO_ROOT = Path(__file__).resolve().parents[3] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from aces_conformance.conformance import ( # noqa: E402 + _fixture_case_diagnostics, + validate_contract_payload, +) +from aces_contracts.contracts import schema_bundle # noqa: E402 +from aces_contracts.scientific_completeness import ( # noqa: E402 + DeliveryStatus, + ProfileDisposition, + ScientificCompletenessAssessmentModel, + ScientificCompletenessTaxonomyModel, + evaluate_profile_completeness, + load_scientific_completeness_assessment, + load_scientific_completeness_taxonomy, +) +from aces_sdl.parser import parse_sdl # noqa: E402 +from tools.check_scientific_scenario_completeness import ( # noqa: E402 + _validate_contract_evidence, + _validate_evidence_paths, + _validate_nonclaims, + _validate_profile_examples, + _validate_profile_set, + _validate_required_exclusions, + _validate_summary, +) +from tools.check_scientific_scenario_completeness import ( + evaluate as evaluate_policy, +) + +TAXONOMY_PATH = REPO_ROOT / "contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json" +ASSESSMENT_PATH = REPO_ROOT / "contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json" + + +def _taxonomy_payload() -> dict[str, object]: + return json.loads(TAXONOMY_PATH.read_text(encoding="utf-8")) + + +def _assessment_payload() -> dict[str, object]: + return json.loads(ASSESSMENT_PATH.read_text(encoding="utf-8")) + + +def test_rev1_taxonomy_and_assessment_are_valid_and_join_exactly() -> None: + taxonomy = load_scientific_completeness_taxonomy() + assessment = load_scientific_completeness_assessment() + + assert taxonomy.profile_family == "scientific-scenario-completeness" + assert taxonomy.revision == "rev1" + assert assessment.profile_family == taxonomy.profile_family + assert assessment.taxonomy_revision == taxonomy.revision + assert {profile.profile_id for profile in taxonomy.profiles} == { + "valid-sdl-fragment", + "deployable-scenario-intent", + "participant-evaluation-scenario", + "controlled-experiment-scenario", + "reproducible-benchmark-study-input", + } + assert {item.concern_id for item in assessment.concerns} == {concern.concern_id for concern in taxonomy.concerns} + + +def test_both_completeness_contracts_are_in_the_published_schema_bundle() -> None: + 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"]} + assessment_invariants = { + item["id"] for item in bundle["scientific-completeness-assessment-v1"]["x-aces-invariants"] + } + assert taxonomy_invariants == {"scientific-completeness-taxonomy-rectangular"} + assert assessment_invariants == { + "scientific-completeness-assessment-status-evidence", + "scientific-completeness-taxonomy-assessment-join", + } + + +@pytest.mark.parametrize( + ("contract_id", "fixture", "valid"), + [ + ("scientific-completeness-taxonomy-v1", "valid/minimal.json", True), + ("scientific-completeness-taxonomy-v1", "invalid/missing-disposition.json", False), + ("scientific-completeness-assessment-v1", "valid/minimal.json", True), + ("scientific-completeness-assessment-v1", "invalid/external-without-binding.json", False), + ], +) +def test_published_completeness_fixtures_exercise_semantic_validation( + contract_id: str, + fixture: str, + valid: bool, +) -> None: + path = REPO_ROOT / "contracts/fixtures/profiles" / contract_id / fixture + diagnostics = _fixture_case_diagnostics(contract_id, json.loads(path.read_text(encoding="utf-8"))) + assert (not diagnostics) is valid + + +@pytest.mark.parametrize( + "concern_id", + [ + "authored-observed-state-separation", + "scoped-specificity-open-world-intent", + "parameter-typing", + "factor-declaration", + "controlled-allocation", + "randomization-policy", + "seed-preservation", + "propositions", + "assertions", + "workflow-compensation", + "backend-teardown-reconciliation", + "manual-rollback", + "time-domain-declaration", + "clock-declaration", + "temporal-ordering-causality", + "deadlines-and-windows", + "pacing-and-synchronization", + "participant-episode-reset", + "participant-budgets", + "reference-trajectories", + "hidden-benchmark-assets", + "verifier-and-adjudication", + "graded-reward-in-sdl", + "credential-intent", + "credential-materialization", + "host-architecture-constraints", + "substrate-constraints", + "vulnerability-inventory", + "weakness-exploitability-semantics", + "flexible-step-tooling", + "portable-behavior-contracts", + "behavioral-relation-taxonomy", + ], +) +def test_issue_required_concerns_are_atomic_and_present(concern_id: str) -> None: + taxonomy = load_scientific_completeness_taxonomy() + assert concern_id in {concern.concern_id for concern in taxonomy.concerns} + + +def test_completeness_is_computed_and_does_not_overclaim_stronger_profiles() -> None: + outcomes = { + result.profile_id: result + for result in evaluate_profile_completeness( + load_scientific_completeness_taxonomy(), + load_scientific_completeness_assessment(), + ) + } + + assert outcomes["valid-sdl-fragment"].complete + assert outcomes["valid-sdl-fragment"].blocking_concerns == () + for profile_id in { + "deployable-scenario-intent", + "participant-evaluation-scenario", + "controlled-experiment-scenario", + "reproducible-benchmark-study-input", + }: + assert not outcomes[profile_id].complete + assert outcomes[profile_id].blocking_concerns + + +def test_every_complete_profile_has_a_production_validated_minimal_example() -> None: + taxonomy = load_scientific_completeness_taxonomy() + outcomes = { + result.profile_id: result + for result in evaluate_profile_completeness(taxonomy, load_scientific_completeness_assessment()) + } + for profile in taxonomy.profiles: + if not outcomes[profile.profile_id].complete: + continue + assert profile.example_refs + for example_ref in profile.example_refs: + scenario = parse_sdl((REPO_ROOT / example_ref).read_text(encoding="utf-8")) + assert scenario.name + + +def test_taxonomy_rejects_missing_rectangular_disposition() -> None: + payload = _taxonomy_payload() + missing_id = payload["concerns"][0]["concern_id"] + del payload["profiles"][0]["dispositions"][missing_id] + with pytest.raises(ValidationError, match="exactly cover taxonomy concerns"): + ScientificCompletenessTaxonomyModel.model_validate(payload) + + +def test_assessment_rejects_duplicate_or_missing_concern_coverage() -> None: + payload = _assessment_payload() + payload["concerns"].append(deepcopy(payload["concerns"][0])) + with pytest.raises(ValidationError, match="assessment concern ids must be unique"): + ScientificCompletenessAssessmentModel.model_validate(payload) + + +def test_join_rejects_missing_assessment_concern() -> None: + assessment_payload = _assessment_payload() + assessment_payload["concerns"].pop() + assessment = ScientificCompletenessAssessmentModel.model_validate(assessment_payload) + with pytest.raises(ValueError, match="exactly cover taxonomy concerns"): + evaluate_profile_completeness(load_scientific_completeness_taxonomy(), assessment) + + +def test_external_contract_status_requires_named_binding_and_contract_evidence() -> None: + payload = _assessment_payload() + item = next(item for item in payload["concerns"] if item["status"] == DeliveryStatus.EXTERNAL_CONTRACT) + item["binding_obligation"] = None + item["external_contract_refs"] = [] + with pytest.raises(ValidationError, match="external-contract status requires"): + ScientificCompletenessAssessmentModel.model_validate(payload) + + +def test_external_contract_status_requires_a_satisfiability_witness() -> None: + payload = _assessment_payload() + item = next(item for item in payload["concerns"] if item["status"] == DeliveryStatus.EXTERNAL_CONTRACT) + item["satisfiability_witness_refs"] = {} + with pytest.raises(ValidationError, match="satisfiability witnesses"): + ScientificCompletenessAssessmentModel.model_validate(payload) + + +def test_external_contract_witnesses_are_exactly_bound_and_conforming() -> None: + assessment = load_scientific_completeness_assessment() + for item in assessment.concerns: + if item.status is not DeliveryStatus.EXTERNAL_CONTRACT: + continue + assert set(item.satisfiability_witness_refs) == set(item.external_contract_refs) + for contract_id, witness_ref in item.satisfiability_witness_refs.items(): + payload = json.loads((REPO_ROOT / witness_ref).read_text(encoding="utf-8")) + assert validate_contract_payload(contract_id, payload) == () + + +def test_external_contract_rejects_a_witness_bound_to_the_wrong_contract() -> None: + payload = _assessment_payload() + item = next(item for item in payload["concerns"] if item["status"] == DeliveryStatus.EXTERNAL_CONTRACT) + witness = next(iter(item["satisfiability_witness_refs"].values())) + item["satisfiability_witness_refs"] = {"experiment-run-v1": witness} + with pytest.raises(ValidationError, match="exactly one satisfiability witness"): + ScientificCompletenessAssessmentModel.model_validate(payload) + + +def test_implemented_status_requires_executable_evidence() -> None: + payload = _assessment_payload() + item = next(item for item in payload["concerns"] if item["status"] == DeliveryStatus.IMPLEMENTED) + item["evidence"] = [ + { + "kind": "normative-spec", + "path": "specs/sdl/document-model.md", + "claim": "Meaning only, not executable delivery.", + } + ] + with pytest.raises(ValidationError, match="implemented status requires executable evidence"): + ScientificCompletenessAssessmentModel.model_validate(payload) + + +def test_deliberate_exclusion_cannot_satisfy_a_required_profile_concern() -> None: + taxonomy_payload = _taxonomy_payload() + assessment_payload = _assessment_payload() + concern = next(item for item in assessment_payload["concerns"] if item["status"] == "deliberately-excluded") + profile = taxonomy_payload["profiles"][0] + profile["dispositions"][concern["concern_id"]] = ProfileDisposition.REQUIRED + taxonomy = ScientificCompletenessTaxonomyModel.model_validate(taxonomy_payload) + assessment = ScientificCompletenessAssessmentModel.model_validate(assessment_payload) + outcome = next( + item for item in evaluate_profile_completeness(taxonomy, assessment) if item.profile_id == profile["profile_id"] + ) + assert not outcome.complete + assert concern["concern_id"] in outcome.blocking_concerns + + +def test_repository_policy_gate_accepts_current_matrix_and_nonclaims() -> None: + assert evaluate_policy(REPO_ROOT) == [] + + +def test_policy_rejects_an_incomplete_rev1_profile_set() -> None: + payload = _taxonomy_payload() + payload["profiles"].pop() + taxonomy = ScientificCompletenessTaxonomyModel.model_validate(payload) + + failures = _validate_profile_set(taxonomy) + + assert {failure.rule_id for failure in failures} == {"scientific-completeness-profile-set"} + + +def test_policy_rejects_a_missing_evidence_path() -> None: + payload = _assessment_payload() + item = next(item for item in payload["concerns"] if item["evidence"]) + item["evidence"][0]["path"] = "contracts/does-not-exist.json" + assessment = ScientificCompletenessAssessmentModel.model_validate(payload) + + failures = _validate_evidence_paths(REPO_ROOT, assessment) + + assert "scientific-completeness-evidence-missing" in {failure.rule_id for failure in failures} + + +def test_policy_rejects_contract_evidence_at_the_wrong_published_path() -> None: + payload = _assessment_payload() + item = next(item for item in payload["concerns"] if item["status"] == DeliveryStatus.EXTERNAL_CONTRACT) + item["evidence"][0]["path"] = "contracts/README.md" + assessment = ScientificCompletenessAssessmentModel.model_validate(payload) + + failures = _validate_contract_evidence(REPO_ROOT, assessment) + + assert "scientific-completeness-contract-evidence-mismatch" in {failure.rule_id for failure in failures} + + +def test_policy_enforces_complete_and_incomplete_profile_example_rules() -> None: + payload = _taxonomy_payload() + payload["profiles"][0]["example_refs"] = [] + payload["profiles"][1]["example_refs"] = ["examples/completeness/rev1/valid-sdl-fragment/minimal.sdl.yaml"] + taxonomy = ScientificCompletenessTaxonomyModel.model_validate(payload) + outcomes = evaluate_profile_completeness(taxonomy, load_scientific_completeness_assessment()) + + failures = _validate_profile_examples(REPO_ROOT, taxonomy, outcomes) + + assert { + "scientific-completeness-example-required", + "scientific-completeness-example-overclaim", + }.issubset({failure.rule_id for failure in failures}) + + +def test_policy_rejects_eroded_explicit_nonclaims() -> None: + payload = _taxonomy_payload() + for profile in payload["profiles"]: + profile["explicit_non_claims"] = ["No additional claim."] + taxonomy = ScientificCompletenessTaxonomyModel.model_validate(payload) + + failures = _validate_nonclaims(taxonomy) + + assert {failure.rule_id for failure in failures} == {"scientific-completeness-nonclaim-coverage"} + + +def test_policy_rejects_reader_summary_drift(tmp_path: Path) -> None: + spec_path = tmp_path / "specs/sdl/scientific-scenario-completeness.md" + spec_path.parent.mkdir(parents=True) + spec_path.write_text("# Stale summary\n", encoding="utf-8") + outcomes = evaluate_profile_completeness( + load_scientific_completeness_taxonomy(), + load_scientific_completeness_assessment(), + ) + + failures = _validate_summary(tmp_path, outcomes) + + assert {failure.rule_id for failure in failures} == {"scientific-completeness-summary-drift"} + + +def test_policy_rejects_a_required_deliberate_exclusion() -> None: + payload = _assessment_payload() + item = next(item for item in payload["concerns"] if item["concern_id"] == "source-profile-validity") + item["status"] = DeliveryStatus.DELIBERATELY_EXCLUDED + item["exclusion_rationale"] = "Mutation fixture for the policy rule." + assessment = ScientificCompletenessAssessmentModel.model_validate(payload) + + failures = _validate_required_exclusions(load_scientific_completeness_taxonomy(), assessment) + + assert {failure.rule_id for failure in failures} == {"scientific-completeness-required-exclusion"} diff --git a/implementations/python/tests/test_sdl_canonicalization.py b/implementations/python/tests/test_sdl_canonicalization.py new file mode 100644 index 000000000..7b7b81f10 --- /dev/null +++ b/implementations/python/tests/test_sdl_canonicalization.py @@ -0,0 +1,217 @@ +"""Canonical semantic identity tests for ``aces-sdl-semantic/v1``.""" + +from __future__ import annotations + +import textwrap +from pathlib import Path + +import pytest +from aces_sdl import ( + SDL_CANONICAL_PROFILE, + SDLMigrationPolicy, + SDLParseError, + canonical_sdl_bytes, + canonical_sdl_digest, + format_sdl_source, + instantiate_scenario, + parse_sdl, + parse_sdl_file, +) + + +def test_semantically_identical_source_spellings_have_identical_canonical_bytes() -> None: + canonical = parse_sdl( + textwrap.dedent( + """ + name: equivalent + description: same + nodes: + web: {type: switch} + """ + ) + ) + migrated = parse_sdl( + textwrap.dedent( + """ + Description: same + Name: equivalent + nodes: + web: {Type: SWITCH} + """ + ), + migration_policy=SDLMigrationPolicy.ACCEPT, + ) + + assert canonical_sdl_bytes(canonical) == canonical_sdl_bytes(migrated) + + +def test_format_round_trip_preserves_semantic_identity_and_canonical_bytes() -> None: + source = textwrap.dedent( + """ + Description: same + Name: round-trip + nodes: + web-app: {Type: SWITCH} + """ + ) + before = parse_sdl(source, migration_policy=SDLMigrationPolicy.ACCEPT) + + formatted = format_sdl_source(source).content + after = parse_sdl(formatted) + + assert canonical_sdl_bytes(after) == canonical_sdl_bytes(before) + assert format_sdl_source(formatted).content == formatted + + +def test_canonical_digest_is_profile_labelled_and_repeatable() -> None: + scenario = parse_sdl("name: digest-example\n") + + first = canonical_sdl_digest(scenario) + second = canonical_sdl_digest(scenario) + + assert first == second + assert first.profile == SDL_CANONICAL_PROFILE == "aces-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", + "algorithm": "sha256", + "value": first.value, + } + + +def test_canonical_bytes_preserve_authored_field_presence() -> None: + omitted = parse_sdl("name: presence\n") + explicit = parse_sdl("name: presence\ndescription: ''\n") + + assert canonical_sdl_bytes(omitted) != canonical_sdl_bytes(explicit) + + +def test_canonical_bytes_do_not_normalize_unicode() -> None: + composed = parse_sdl("name: unicode\ndescription: caf\N{LATIN SMALL LETTER E WITH ACUTE}\n") + decomposed = parse_sdl("name: unicode\ndescription: cafe\N{COMBINING ACUTE ACCENT}\n") + + assert canonical_sdl_bytes(composed) != canonical_sdl_bytes(decomposed) + + +def test_canonical_bytes_are_map_order_independent_and_array_order_sensitive() -> None: + first = parse_sdl( + textwrap.dedent( + """ + name: ordering + module: + id: aces/ordering + version: 1.0.0 + parameters: [alpha, beta] + """ + ) + ) + reordered_map = parse_sdl( + textwrap.dedent( + """ + module: + parameters: [alpha, beta] + version: 1.0.0 + id: aces/ordering + name: ordering + """ + ) + ) + reordered_array = parse_sdl( + textwrap.dedent( + """ + name: ordering + module: + id: aces/ordering + version: 1.0.0 + parameters: [beta, alpha] + """ + ) + ) + + assert canonical_sdl_bytes(first) == canonical_sdl_bytes(reordered_map) + assert canonical_sdl_bytes(first) != canonical_sdl_bytes(reordered_array) + + +def test_canonical_identity_requires_validated_authoring_scenario() -> None: + unvalidated = parse_sdl("name: unvalidated\n", skip_semantic_validation=True) + with pytest.raises(SDLParseError, match="semantic validation"): + canonical_sdl_bytes(unvalidated) + + validated = parse_sdl("name: instantiated\n") + instantiated = instantiate_scenario(validated) + with pytest.raises(SDLParseError, match="authoring scenario"): + canonical_sdl_bytes(instantiated) + + +def test_canonical_payload_carries_profile_and_module_provenance_channels() -> None: + payload = canonical_sdl_bytes(parse_sdl("name: envelope\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'"scenario":{"name":"envelope"}' in payload + + +def test_canonical_identity_rejects_values_outside_the_jcs_integer_domain() -> None: + scenario = parse_sdl( + """\ +name: unsafe-integer +variables: + too_large: + type: integer + default: 9007199254740992 +""" + ) + + with pytest.raises(SDLParseError, match="canonicalization failed"): + canonical_sdl_bytes(scenario) + + +def test_canonical_payload_commits_to_imported_variable_provenance(tmp_path: Path) -> None: + module = tmp_path / "module.yaml" + module.write_text( + """\ +name: module +module: + id: acme/module + version: 1.0.0 + parameters: [image_os] + exports: + nodes: [host] + infrastructure: [host] +variables: + image_os: + type: string + default: linux + allowed_values: [linux] +nodes: + host: + type: vm + os: ${image_os} + resources: {ram: 1 gib, cpu: 1} +infrastructure: + host: {count: 1} +""", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + """\ +name: root +imports: + - path: module.yaml + namespace: imported +""", + encoding="utf-8", + ) + + scenario = parse_sdl_file(root) + payload = canonical_sdl_bytes(scenario) + + variable_name = "imported.__private.image_os" + assert scenario.module_variable_specs[variable_name]["allowed_values"] == ["linux"] + assert scenario.module_node_variable_refs["imported.host"]["os"] == variable_name + assert b'"module_variable_specs":{"imported.__private.image_os"' in payload + assert b'"module_node_variable_refs":{"imported.host"' in payload diff --git a/implementations/python/tests/test_sdl_catalog_parity.py b/implementations/python/tests/test_sdl_catalog_parity.py new file mode 100644 index 000000000..80ec4b79b --- /dev/null +++ b/implementations/python/tests/test_sdl_catalog_parity.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +import shutil +import sys +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)) + +from tools.check_sdl_catalog_parity import ( # noqa: E402 + CatalogParseError, + evaluate_sdl_catalog_parity, + main, + parse_top_level_catalog, +) + +_CATALOG_PATHS = ( + "specs/sdl/sections.md", + "specs/sdl/references.md", + "specs/sdl/runtime-inventory.md", + "contracts/schemas/sdl/sdl-authoring-input-v1.json", +) + + +def _seed_repo(tmp_path: Path) -> Path: + for relative in _CATALOG_PATHS: + source = REPO_ROOT / relative + target = tmp_path / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + return tmp_path + + +def _replace(tmp_path: Path, relative: str, old: str, new: str) -> None: + path = tmp_path / relative + text = path.read_text(encoding="utf-8") + assert old in text + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def _rule_ids(tmp_path: Path) -> set[str]: + return {failure.rule_id for failure in evaluate_sdl_catalog_parity(tmp_path)} + + +@pytest.mark.parametrize( + ("old", "new", "rule_id"), + [ + ("| `behavior_specifications` |", "| `behavior_profiles` |", "sdl-catalog-field-set"), + ("| `nodes` | section | map |", "| `nodes` | section | list |", "sdl-catalog-field-shape"), + ("| `version` | metadata | scalar |", "| `version` | metadata | map |", "sdl-catalog-field-shape"), + ("optional; default `*`", "required", "sdl-catalog-field-default"), + ], +) +def test_top_level_catalog_drift_is_flagged(tmp_path: Path, old: str, new: str, rule_id: str) -> None: + repo = _seed_repo(tmp_path) + _replace(repo, "specs/sdl/sections.md", old, new) + assert rule_id in _rule_ids(repo) + + +def test_checked_summary_drift_is_flagged(tmp_path: Path) -> None: + repo = _seed_repo(tmp_path) + _replace(repo, "specs/sdl/sections.md", "sections=25", "sections=24") + assert "sdl-catalog-summary" in _rule_ids(repo) + + +def test_identity_classification_drift_is_flagged(tmp_path: Path) -> None: + repo = _seed_repo(tmp_path) + _replace(repo, "specs/sdl/sections.md", "| `map_key` | catalogued |", "| `node_id` | catalogued |") + assert "sdl-catalog-field-identity" in _rule_ids(repo) + + +def test_reference_domain_drift_is_flagged(tmp_path: Path) -> None: + repo = _seed_repo(tmp_path) + _replace( + repo, + "specs/sdl/references.md", + "| `behavior_specifications.*.authority_scope_refs[]` | `targetable` |", + "| `behavior_specifications.*.authority_scope_refs[]` | `any` |", + ) + assert "sdl-catalog-reference-domain" in _rule_ids(repo) + + +def test_non_completion_reference_domain_drift_is_flagged(tmp_path: Path) -> None: + repo = _seed_repo(tmp_path) + _replace( + repo, + "specs/sdl/references.md", + "| `action_contracts.*.interactions.*.related_action_ref` | `action_contracts` |", + "| `action_contracts.*.interactions.*.related_action_ref` | `any` |", + ) + assert "sdl-catalog-reference-row" in _rule_ids(repo) + + +@pytest.mark.parametrize( + ("old", "new"), + [ + ("| `features` | semantic validation |", "| `features` | |"), + ("| fatal dangling or ambiguous | [node validator]", "| | [node validator]"), + ("[node validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py)", ""), + ], +) +def test_reference_row_classification_drift_is_flagged(tmp_path: Path, old: str, new: str) -> None: + repo = _seed_repo(tmp_path) + _replace(repo, "specs/sdl/references.md", old, new) + assert "sdl-catalog-reference-row" in _rule_ids(repo) + + +def test_missing_behavior_reference_edge_is_flagged(tmp_path: Path) -> None: + repo = _seed_repo(tmp_path) + _replace( + repo, + "specs/sdl/references.md", + "| `behavior_specifications.*.participant_refs[]` |", + "| `behavior_profiles.*.participant_refs[]` |", + ) + assert "sdl-catalog-behavior-edge" in _rule_ids(repo) + + +def test_runtime_child_tree_drift_is_flagged(tmp_path: Path) -> None: + repo = _seed_repo(tmp_path) + _replace( + repo, + "specs/sdl/runtime-inventory.md", + "zones:zone_id/rrsets:rrset_id", + "zones:zone_id/records:record_id", + ) + assert "sdl-catalog-runtime-family" in _rule_ids(repo) + + +def test_duplicate_top_level_row_is_rejected() -> None: + body = """# Catalog + +## Complete top-level field catalog + +| Field | Kind | Shape | Lifecycle | Presence/default | Identity | References | Semantic owner | +| --- | --- | --- | --- | --- | --- | --- | --- | +| `name` | metadata | scalar | normalized | required | scenario_name | none | `specs/sdl/document-model.md` | +| `name` | metadata | scalar | normalized | required | scenario_name | none | `specs/sdl/document-model.md` | +""" + with pytest.raises(CatalogParseError, match="duplicate"): + parse_top_level_catalog(body) + + +def test_catalog_parser_rejects_oversized_input() -> None: + body = "x" * (512 * 1024 + 1) + with pytest.raises(CatalogParseError, match="size limit"): + parse_top_level_catalog(body) + + +def test_cli_reports_json_failure(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + repo = _seed_repo(tmp_path) + _replace(repo, "specs/sdl/sections.md", "sections=25", "sections=24") + assert main(["--repo-root", str(repo), "--json"]) == 1 + assert '"rule_id": "sdl-catalog-summary"' in capsys.readouterr().out + + +@pytest.mark.integration +def test_live_sdl_catalogs_match_authorities() -> None: + assert evaluate_sdl_catalog_parity(REPO_ROOT) == [] diff --git a/implementations/python/tests/test_sdl_format_cli.py b/implementations/python/tests/test_sdl_format_cli.py new file mode 100644 index 000000000..06a8554ba --- /dev/null +++ b/implementations/python/tests/test_sdl_format_cli.py @@ -0,0 +1,52 @@ +"""Tests for canonical SDL formatting and migration CLI surfaces.""" + +from __future__ import annotations + +from aces_cli.main import app +from aces_sdl import format_sdl_source +from typer.testing import CliRunner + + +def test_format_api_migrates_fields_and_expands_shorthands() -> None: + result = format_sdl_source( + """\ +Name: migrate-me +Nodes: + web-app: + Type: VM + roles: {admin: operator} +infrastructure: + web-app: 1 +""" + ) + + assert result.content.startswith("name: migrate-me\nnodes:\n web-app:\n type: vm\n") + assert "username: operator" in result.content + assert "count: 1" in result.content + assert [item.code for item in result.diagnostics] == [ + "sdl.noncanonical_field", + "sdl.noncanonical_field", + "sdl.noncanonical_field", + ] + + +def test_format_cli_supports_stdout_write_and_check(tmp_path) -> None: + source = tmp_path / "scenario.yaml" + source.write_text("Name: cli-format\n", encoding="utf-8") + runner = CliRunner() + + stdout_result = runner.invoke(app, ["sdl", "format", str(source)]) + assert stdout_result.exit_code == 0 + assert "name: cli-format" in stdout_result.stdout + assert "sdl.noncanonical_field" in stdout_result.stderr + assert source.read_text(encoding="utf-8") == "Name: cli-format\n" + + check_result = runner.invoke(app, ["sdl", "format", str(source), "--check"]) + assert check_result.exit_code == 1 + + write_result = runner.invoke(app, ["sdl", "format", str(source), "--write"]) + assert write_result.exit_code == 0 + assert source.read_text(encoding="utf-8") == "name: cli-format\n" + + clean_check = runner.invoke(app, ["sdl", "format", str(source), "--check"]) + assert clean_check.exit_code == 0 diff --git a/implementations/python/tests/test_sdl_fuzz.py b/implementations/python/tests/test_sdl_fuzz.py index 3c329f5c7..0c4a3483d 100644 --- a/implementations/python/tests/test_sdl_fuzz.py +++ b/implementations/python/tests/test_sdl_fuzz.py @@ -397,3 +397,49 @@ def test_fuzz_feature_dependency_cycles(features): parse_sdl(yaml_str) except (SDLParseError, SDLValidationError): pass + + +@given( + pair=st.sampled_from( + [ + ("Password-Strength", "password_strength"), + ("PASSWORD_STRENGTH", "password-strength"), + ("password-strength", "Password_Strength"), + ] + ) +) +@settings(max_examples=50, deadline=2000) +def test_fuzz_structural_key_alias_mutations_fail_closed(pair): + """Generated field aliases must never overwrite an earlier spelling.""" + first, second = pair + source = f"""\ +name: fuzz-aliases +nodes: + host: {{type: switch}} +accounts: + alice: + username: alice + node: host + {first}: strong + {second}: weak +""" + + with pytest.raises(SDLParseError) as excinfo: + parse_sdl(source) + assert excinfo.value.diagnostics[0].code == "sdl.mapping_key_conflict" + + +@given(identifier=slugs) +@settings(max_examples=50, deadline=2000) +def test_fuzz_exact_identifier_duplicate_mutations_fail_closed(identifier): + """Generated duplicate symbol definitions must fail before construction.""" + source = f"""\ +name: fuzz-duplicates +nodes: + {identifier}: {{type: switch}} + {identifier}: {{type: switch}} +""" + + with pytest.raises(SDLParseError) as excinfo: + parse_sdl(source) + assert excinfo.value.diagnostics[0].code == "sdl.mapping_key_conflict" diff --git a/implementations/python/tests/test_sdl_identifiers.py b/implementations/python/tests/test_sdl_identifiers.py new file mode 100644 index 000000000..7dd6612d0 --- /dev/null +++ b/implementations/python/tests/test_sdl_identifiers.py @@ -0,0 +1,867 @@ +"""Cross-boundary tests for the portable SDL identifier contract.""" + +from __future__ import annotations + +from pathlib import Path + +import jsonschema +import pytest +from aces_backend_protocols.naming import provider_resource_name +from aces_contracts.addressing import ( + COMPILED_ADDRESS_JSON_SCHEMA, + COMPILED_ADDRESS_MAX_LENGTH, + render_compiled_address, + require_compiled_address, +) +from aces_contracts.contracts import ( + EvaluationPlanModel, + OrchestrationPlanModel, + ProvisioningPlanModel, + RuntimeSnapshotEnvelopeModel, + schema_bundle, +) +from aces_contracts.planning import ( + ChangeAction, + OrchestrationPlan, + PlannedResource, + ProvisioningPlan, + ProvisionOp, + RuntimeDomain, +) +from aces_contracts.runtime_state import ApplyResult, OperationStatus, RuntimeSnapshot, SnapshotEntry +from aces_processor.compiler import compile_runtime_model +from aces_processor.models import NetworkRuntime, NodeRuntime, RuntimeModel +from aces_sdl._declarations import build_declaration_index +from aces_sdl._errors import SDLParseError, SDLValidationError +from aces_sdl._model_diagnostics import _bounded_model_message +from aces_sdl._source_profile import SDLParserLimits +from aces_sdl.identifiers import ( + PORTABLE_IDENTIFIER_JSON_SCHEMA, + QUALIFIED_IDENTIFIER_MAX_LENGTH, + QualifiedName, + is_portable_identifier, + require_portable_identifier, +) +from aces_sdl.infrastructure import ACLRule +from aces_sdl.instantiate import instantiate_scenario +from aces_sdl.nodes import ServicePort +from aces_sdl.parser import parse_sdl +from aces_sdl.runtime_values import require_symbol +from aces_sdl.scenario import ExpandedScenario, ImportDecl, ModuleDescriptor, Scenario +from aces_sdl.validator import SemanticValidator +from hypothesis import given +from hypothesis import strategies as st +from pydantic import ValidationError + +_PORTABLE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789-_" +_PORTABLE_START = "abcdefghijklmnopqrstuvwxyz0123456789" +_portable_identifier_strategy = st.builds( + lambda first, tail: first + tail, + st.sampled_from(tuple(_PORTABLE_START)), + st.text(alphabet=_PORTABLE_CHARACTERS, min_size=0, max_size=63), +) + + +def _minimal_instantiation_provenance() -> dict[str, object]: + return { + "authored_digest": { + "profile": "aces-sdl-semantic/v1", + "algorithm": "sha256", + "value": f"sha256:{'0' * 64}", + } + } + + +@pytest.mark.parametrize( + "identifier", + ["a", "0", "001", "a-b", "a_b", "a" * 64], +) +def test_portable_identifier_accepts_exact_boundary_values(identifier: str) -> None: + assert is_portable_identifier(identifier) + assert require_portable_identifier(identifier, field_name="test_id") == identifier + jsonschema.validate(identifier, PORTABLE_IDENTIFIER_JSON_SCHEMA) + + +@pytest.mark.parametrize( + "identifier", + [ + "", + " ", + "a.b", + "a/b", + "a:b", + "A", + "_private", + "caf\u00e9", + "a\n", + "a\r", + "a\x00", + "a" * 65, + "${name}", + ], +) +def test_portable_identifier_rejects_noncanonical_values(identifier: str) -> None: + assert not is_portable_identifier(identifier) + with pytest.raises(ValueError, match="portable SDL identifier"): + require_portable_identifier(identifier, field_name="test_id") + with pytest.raises(jsonschema.ValidationError): + jsonschema.validate(identifier, PORTABLE_IDENTIFIER_JSON_SCHEMA) + + +def test_model_diagnostic_messages_are_control_escaped_and_bounded() -> None: + message = _bounded_model_message("invalid\n" + "x" * 600) + + assert "\n" not in message + assert "\\u000a" in message + assert len(message) == 512 + assert message.endswith("...") + + +@given(st.text(max_size=80)) +def test_python_and_json_schema_identifier_grammars_are_differentially_equivalent(value: str) -> None: + schema_accepts = not list(jsonschema.Draft202012Validator(PORTABLE_IDENTIFIER_JSON_SCHEMA).iter_errors(value)) + assert schema_accepts is is_portable_identifier(value) + + +@given( + st.lists(_portable_identifier_strategy, min_size=1, max_size=6), + st.lists(_portable_identifier_strategy, min_size=1, max_size=6), +) +def test_qualified_name_rendering_is_injective_over_portable_segments( + left_parts: list[str], + right_parts: list[str], +) -> None: + left = QualifiedName(tuple(left_parts)) + right = QualifiedName(tuple(right_parts)) + assert (left.render() == right.render()) is (left.parts == right.parts) + + +@pytest.mark.parametrize( + "address", + [ + "nodes.vm", + "nodes.shared.vm", + "nodes.__private.vm", + "a." + "b" * 64, + ], +) +def test_compiled_address_accepts_exact_canonical_values(address: str) -> None: + assert require_compiled_address(address) == address + assert render_compiled_address(*address.split(".")) == address + jsonschema.validate(address, COMPILED_ADDRESS_JSON_SCHEMA) + + +@pytest.mark.parametrize( + "address", + [ + "vm", + ".nodes.vm", + "nodes.vm.", + "nodes..vm", + "nodes.VM", + "nodes.vm\n", + "nodes.caf\u00e9", + "nodes._private.vm", + "nodes." + "b" * 65, + "a." + ".".join("b" for _ in range(COMPILED_ADDRESS_MAX_LENGTH)), + ], +) +def test_compiled_address_rejects_noncanonical_values(address: str) -> None: + with pytest.raises(ValueError, match="canonical compiled address"): + require_compiled_address(address) + with pytest.raises(jsonschema.ValidationError): + jsonschema.validate(address, COMPILED_ADDRESS_JSON_SCHEMA) + + +def test_qualified_identifier_has_a_bounded_rendering() -> None: + overlong = ".".join("a" * 64 for _ in range(33)) + assert len(overlong) > QUALIFIED_IDENTIFIER_MAX_LENGTH + with pytest.raises(ValueError, match="maximum length"): + QualifiedName.parse(overlong) + + +def test_authored_declaration_key_reports_source_range() -> None: + source = """\ +name: root +nodes: + bad.name: + type: switch +""" + + with pytest.raises(SDLParseError) as caught: + parse_sdl(source) + + diagnostic = caught.value.diagnostics[0] + assert diagnostic.code == "sdl.identifier.invalid" + assert diagnostic.pointer == "/nodes/bad.name" + assert diagnostic.primary_range.start.line == 3 + assert diagnostic.primary_range.start.column == 3 + assert "bad.name" not in diagnostic.message + + +@pytest.mark.parametrize( + ("source", "pointer", "line"), + [ + ( + """\ +name: root +forwarding_agents: + - forwarding_agent_id: bad.name +""", + "/forwarding_agents/0/forwarding_agent_id", + 3, + ), + ( + """\ +name: root +nodes: + vm: + type: vm + runtime: + database_services: + - database_service_id: bad.name +""", + "/nodes/vm/runtime/database_services/0/database_service_id", + 7, + ), + ], +) +def test_scalar_identifier_reports_its_own_source_range( + source: str, + pointer: str, + line: int, +) -> None: + with pytest.raises(SDLParseError) as caught: + parse_sdl(source, skip_semantic_validation=True) + + diagnostic = caught.value.diagnostics[0] + assert diagnostic.code == "sdl.identifier.invalid" + assert diagnostic.pointer == pointer + assert diagnostic.primary_range.start.line == line + assert "bad.name" not in diagnostic.message + + +def test_identifier_errors_remain_fatal_under_accept_migration_policy() -> None: + source = """\ +name: Root +nodes: {} +""" + + with pytest.raises(SDLParseError) as caught: + parse_sdl(source, migration_policy="accept") + + assert caught.value.diagnostics[0].code == "sdl.identifier.invalid" + assert caught.value.diagnostics[0].severity == "error" + + +@pytest.mark.parametrize("name", ["", "Root", "root.name", "root\n"]) +def test_direct_scenario_construction_enforces_name(name: str) -> None: + with pytest.raises(ValidationError, match="portable SDL identifier"): + Scenario(name=name) + + +def test_direct_scenario_construction_enforces_declaration_keys() -> None: + with pytest.raises(ValidationError, match="portable SDL identifier"): + Scenario(name="root", nodes={"bad.name": {"type": "switch"}}) + + +def test_non_identifier_data_retains_its_own_contract() -> None: + scenario = Scenario( + name="root", + entities={"team": {"name": "\u00c9quipe de recherche"}}, + content={ + "report": { + "type": "file", + "target": "node.with.external.syntax", + "path": "/var/tmp/Report 01.txt", + } + }, + ) + + assert scenario.entities["team"].name == "\u00c9quipe de recherche" + assert scenario.content["report"].path == "/var/tmp/Report 01.txt" + + +@pytest.mark.parametrize("module_id", ["acme", "Acme/shared", "acme/shared/extra", "acme/bad.name"]) +def test_module_id_is_exactly_two_portable_segments(module_id: str) -> None: + with pytest.raises(ValidationError, match="module.id"): + ModuleDescriptor(id=module_id, version="1.0.0") + + +def test_import_requires_explicit_portable_namespace() -> None: + with pytest.raises(ValidationError, match="namespace"): + ImportDecl(source="local:shared.yaml") + with pytest.raises(ValidationError, match="namespace"): + ImportDecl(source="local:shared.yaml", namespace="shared.module") + + +@pytest.mark.parametrize( + ("factory", "field_name"), + [ + (lambda: ServicePort(port=443, name="https.service"), "name"), + (lambda: ACLRule(name="allow.web"), "name"), + ], +) +def test_addressable_nested_names_use_portable_identifiers(factory, field_name: str) -> None: + with pytest.raises(ValidationError, match=field_name): + factory() + + +def test_runtime_stable_ids_use_the_same_identifier_contract() -> None: + with pytest.raises(ValueError, match="portable SDL identifier"): + require_symbol("database.primary", field_name="database_id") + + +def test_raw_forwarding_agent_id_is_local_but_expanded_id_may_be_qualified() -> None: + with pytest.raises(ValidationError, match="portable SDL identifier"): + Scenario( + name="root", + forwarding_agents=[{"forwarding_agent_id": "shared.shipper"}], + ) + + expanded = ExpandedScenario( + name="root", + forwarding_agents=[{"forwarding_agent_id": "shared.shipper"}], + ) + assert expanded.forwarding_agents[0].forwarding_agent_id == "shared.shipper" + + with pytest.raises(ValidationError, match="portable SDL identifier"): + ExpandedScenario( + name="root", + nodes={ + "shared.vm": { + "type": "vm", + "runtime": { + "forwarding_agents": [ + {"forwarding_agent_id": "shared.shipper"}, + ] + }, + } + }, + ) + + +def test_parameter_values_do_not_change_declared_identity() -> None: + scenario = parse_sdl( + """\ +name: parameterized +variables: + label: + type: string + required: true +nodes: + vm: + type: vm + description: ${label} + resources: {ram: 1 gib, cpu: 1} +""", + skip_semantic_validation=True, + ) + + instantiated = instantiate_scenario( + scenario, + parameters={"label": "renamed.node"}, + ) + + assert set(instantiated.nodes) == {"vm"} + assert instantiated.nodes["vm"].description == "renamed.node" + + +def test_declaration_index_covers_typed_nested_addresses_and_aliases() -> None: + scenario = Scenario( + name="root", + nodes={ + "vm": { + "type": "vm", + "resources": {"ram": "1 gib", "cpu": 1}, + "roles": {"admin": {"username": "root"}}, + "services": [{"port": 443, "name": "https"}], + } + }, + infrastructure={"vm": {"acls": [{"name": "allow-https"}]}}, + entities={"team": {"entities": {"operator": {}}}}, + content={ + "mail": { + "type": "dataset", + "target": "vm", + "items": [{"name": "message", "display_name": "message.eml"}], + } + }, + workflows={"flow": {"start": "done", "steps": {"done": {"type": "end"}}}}, + ) + + index = build_declaration_index(scenario) + + assert { + "scenario.root", + "nodes.vm", + "nodes.vm.roles.admin", + "nodes.vm.services.https", + "infrastructure.vm.acls.allow-https", + "entities.team", + "entities.team.operator", + "content.mail.items.message", + "workflows.flow.steps.done", + }.issubset(index.addresses) + assert index.resolve("https") == set() + assert index.resolve("nodes.vm.services.https") == {"nodes.vm.services.https"} + assert index.resolve("flow.done") == {"workflows.flow.steps.done"} + + +def test_declaration_index_rejects_cross_kind_render_collision() -> None: + scenario = ExpandedScenario( + name="root", + nodes={ + "a": { + "type": "vm", + "resources": {"ram": "1 gib", "cpu": 1}, + "services": [{"port": 443, "name": "b"}], + }, + "a.services.b": {"type": "switch"}, + }, + ) + + with pytest.raises(SDLValidationError) as caught: + build_declaration_index(scenario) + + message = "\n".join(caught.value.errors) + assert "nodes.a.services.b" in message + assert "collides between" in message + assert "service" in message + assert "node" in message + + +def test_runtime_references_are_resolved_by_exact_declaration_not_marker_split() -> None: + application_node = "team.runtime.applications.web" + database_node = "team.runtime.database_services.db" + scenario = ExpandedScenario( + name="root", + nodes={ + application_node: { + "type": "vm", + "services": [{"port": 8080, "name": "http"}], + "runtime": { + "applications": [{"application_id": "frontend", "service": "http"}], + }, + }, + database_node: { + "type": "vm", + "services": [{"port": 5432, "name": "postgres"}], + "runtime": { + "database_services": [ + { + "database_service_id": "primary", + "service": "postgres", + "engine": "postgresql", + "protocol": "postgresql", + "databases": [{"database_id": "experiment", "name": "experiment"}], + "roles": [{"role_id": "writer", "name": "writer"}], + } + ], + }, + }, + }, + relationships={ + "writes": { + "type": "connects_to", + "source": f"nodes.{application_node}.runtime.applications.frontend", + "target": (f"nodes.{database_node}.runtime.database_services.primary.databases.experiment"), + "database_access": { + "role_ref": "writer", + "auth_method": "password", + }, + } + }, + ) + + source = f"nodes.{application_node}.runtime.applications.frontend" + target = f"nodes.{database_node}.runtime.database_services.primary.databases.experiment" + index = build_declaration_index(scenario) + + SemanticValidator(scenario).validate() + assert index.resolve(source) == {source} + assert index.resolve(target) == {target} + + +def test_published_authoring_schema_rejects_nonportable_declaration_key() -> None: + schema = schema_bundle()["sdl-authoring-input-v1"] + payload = {"name": "root", "nodes": {"bad.name": {"type": "switch"}}} + + with pytest.raises(jsonschema.ValidationError): + jsonschema.Draft202012Validator(schema).validate(payload) + + +def test_published_instantiated_schema_accepts_generated_qualified_key() -> None: + schema = schema_bundle()["instantiated-scenario-v1"] + payload = { + "name": "root", + "nodes": {"shared.vm": {"type": "switch"}}, + "instantiation_provenance": _minimal_instantiation_provenance(), + } + + assert list(jsonschema.Draft202012Validator(schema).iter_errors(payload)) == [] + + +def test_published_instantiation_request_schema_rejects_nonportable_parameter_key() -> None: + schema = schema_bundle()["scenario-instantiation-request-v1"] + payload = {"parameters": {"bad.name": "value"}} + + with pytest.raises(jsonschema.ValidationError): + jsonschema.Draft202012Validator(schema).validate(payload) + + +@pytest.mark.parametrize( + ("contract_id", "model", "address", "resource_type"), + [ + ("provisioning-plan-v1", ProvisioningPlanModel, "evaluation.objective.fake", "node"), + ("provisioning-plan-v1", ProvisioningPlanModel, "provision.node.fake", "objective"), + ("orchestration-plan-v1", OrchestrationPlanModel, "provision.node.fake", "workflow"), + ("orchestration-plan-v1", OrchestrationPlanModel, "orchestration.workflow.fake", "node"), + ("evaluation-plan-v1", EvaluationPlanModel, "orchestration.workflow.fake", "objective"), + ("evaluation-plan-v1", EvaluationPlanModel, "evaluation.objective.fake", "workflow"), + ], +) +def test_published_plan_contracts_reject_endpoint_identity_incoherence( + contract_id: str, + model, + address: str, + resource_type: str, +) -> None: + payload = { + "operations": [ + { + "action": "create", + "address": address, + "resource_type": resource_type, + } + ] + } + with pytest.raises(ValidationError, match="must belong to its runtime domain"): + model.model_validate(payload) + with pytest.raises(jsonschema.ValidationError): + jsonschema.Draft202012Validator(schema_bundle()[contract_id]).validate(payload) + + +def test_typed_plan_rejects_new_operation_outside_endpoint_identity() -> None: + with pytest.raises(ValueError, match="address must belong to its runtime domain"): + ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address="evaluation.objective.fake", + resource_type="node", + payload={}, + ) + ] + ) + + +@pytest.mark.parametrize("map_key", ["bad", "a..b"]) +def test_published_snapshot_schema_rejects_noncanonical_entry_keys(map_key: str) -> None: + schema = schema_bundle()["runtime-snapshot-v1"] + + with pytest.raises(jsonschema.ValidationError): + jsonschema.Draft202012Validator(schema).validate({"entries": {map_key: {"arbitrary": "untyped"}}}) + + +def test_published_schema_constrains_runtime_family_identifiers() -> None: + schema = schema_bundle()["sdl-authoring-input-v1"] + payload = { + "name": "root", + "nodes": { + "vm": { + "type": "vm", + "runtime": { + "database_services": [ + {"database_service_id": "bad.name"}, + ] + }, + } + }, + } + + with pytest.raises(jsonschema.ValidationError): + jsonschema.Draft202012Validator(schema).validate(payload) + + +def test_published_schema_separates_top_level_forwarder_identity_phases() -> None: + authoring = schema_bundle()["sdl-authoring-input-v1"] + instantiated = schema_bundle()["instantiated-scenario-v1"] + top_level = { + "name": "root", + "forwarding_agents": [{"forwarding_agent_id": "shared.shipper"}], + } + instantiated_top_level = { + **top_level, + "instantiation_provenance": _minimal_instantiation_provenance(), + } + nested_runtime = { + "name": "root", + "nodes": { + "shared.vm": { + "type": "vm", + "runtime": { + "forwarding_agents": [{"forwarding_agent_id": "shared.shipper"}], + }, + } + }, + "instantiation_provenance": _minimal_instantiation_provenance(), + } + + with pytest.raises(jsonschema.ValidationError): + jsonschema.Draft202012Validator(authoring).validate(top_level) + jsonschema.Draft202012Validator(instantiated).validate(instantiated_top_level) + with pytest.raises(jsonschema.ValidationError): + jsonschema.Draft202012Validator(instantiated).validate(nested_runtime) + + +def test_module_composition_uses_one_aggregate_import_budget(tmp_path: Path) -> None: + module = tmp_path / "shared.yaml" + module.write_text( + """\ +name: shared +version: 1.0.0 +module: + id: acme/shared + version: 1.0.0 + exports: + nodes: [vm] +nodes: + vm: {type: switch} +""", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + """\ +name: root +imports: + - source: local:shared.yaml + namespace: first + - source: local:shared.yaml + namespace: second +""", + encoding="utf-8", + ) + + limits = SDLParserLimits(max_imports=1) + with pytest.raises(SDLParseError, match="composition import budget"): + from aces_sdl.parser import parse_sdl_file + + parse_sdl_file(root, limits=limits) + + +def test_module_composition_bounds_decoded_bytes_across_the_request(tmp_path: Path) -> None: + module = tmp_path / "shared.yaml" + module.write_text( + """\ +name: shared +version: 1.0.0 +module: + id: acme/shared + version: 1.0.0 + exports: {nodes: [vm]} +nodes: {vm: {type: switch}} +""", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + """\ +name: root +imports: + - source: local:shared.yaml + namespace: shared +""", + encoding="utf-8", + ) + + from aces_sdl.parser import parse_sdl_file + + with pytest.raises(SDLParseError, match="decoded-byte budget"): + parse_sdl_file(root, limits=SDLParserLimits(max_composed_bytes=16)) + + +@pytest.mark.parametrize( + ("limits", "message"), + [ + (SDLParserLimits(max_composition_depth=1), "composition depth budget"), + (SDLParserLimits(max_namespace_depth=1), "namespace-depth budget"), + ], +) +def test_nested_composition_bounds_depth_and_namespace_growth( + tmp_path: Path, + limits: SDLParserLimits, + message: str, +) -> None: + (tmp_path / "leaf.yaml").write_text( + """\ +name: leaf +version: 1.0.0 +module: + id: acme/leaf + version: 1.0.0 + exports: {nodes: [vm]} +nodes: {vm: {type: switch}} +""", + encoding="utf-8", + ) + (tmp_path / "middle.yaml").write_text( + """\ +name: middle +version: 1.0.0 +module: + id: acme/middle + version: 1.0.0 + exports: {nodes: [inner.vm]} +imports: + - source: local:leaf.yaml + namespace: inner +""", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + """\ +name: root +imports: + - source: local:middle.yaml + namespace: outer +""", + encoding="utf-8", + ) + + from aces_sdl.parser import parse_sdl_file + + with pytest.raises(SDLParseError, match=message): + parse_sdl_file(root, limits=limits) + + +def test_compiled_runtime_model_rejects_map_key_address_mismatch() -> None: + resource = NodeRuntime(address="provision.node.vm", name="vm", spec={}) + + with pytest.raises(ValueError, match="map key"): + RuntimeModel( + scenario_name="root", + node_deployments={"provision.node.other": resource}, + ) + + +def test_compiled_runtime_model_rejects_cross_family_address_collision() -> None: + address = "provision.shared" + + with pytest.raises(ValueError, match="duplicate compiled address"): + RuntimeModel( + scenario_name="root", + networks={address: NetworkRuntime(address=address, name="network", spec={})}, + node_deployments={address: NodeRuntime(address=address, name="node", spec={})}, + ) + + +def test_composed_realization_concern_retains_canonical_resource_address() -> None: + model = compile_runtime_model( + ExpandedScenario( + name="root", + nodes={ + "shared.vm": { + "type": "vm", + "os": "linux", + "resources": {"ram": "1 gib", "cpu": 1}, + } + }, + ) + ) + + requirements = {requirement.field_path: requirement for requirement in model.realization_requirements} + assert requirements["nodes.shared.vm.os"].address == "provision.node.shared.vm" + assert requirements["nodes.shared.vm.type"].address == "provision.node.shared.vm" + + +def test_in_process_plan_rejects_resource_map_key_address_mismatch() -> None: + resource = PlannedResource( + address="provision.node.vm", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={}, + ) + + with pytest.raises(ValueError, match="resource map key"): + ProvisioningPlan(resources={"provision.node.other": resource}) + + +def test_in_process_plan_rejects_duplicate_operations_and_unknown_startup_address() -> None: + operation = ProvisionOp( + action=ChangeAction.CREATE, + address="provision.node.vm", + resource_type="node", + payload={}, + ) + + with pytest.raises(ValueError, match="operation addresses"): + ProvisioningPlan(operations=[operation, operation]) + with pytest.raises(ValueError, match="admitted operation"): + OrchestrationPlan(startup_order=["orchestration.workflow.flow"]) + + +def test_runtime_snapshot_rejects_map_key_address_mismatch() -> None: + entry = SnapshotEntry( + address="provision.node.vm", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={}, + ) + + with pytest.raises(ValueError, match="map key"): + RuntimeSnapshot(entries={"provision.node.other": entry}) + with pytest.raises(ValidationError, match="map key"): + RuntimeSnapshotEnvelopeModel( + entries={ + "provision.node.other": { + "address": "provision.node.vm", + "domain": "provisioning", + "resource_type": "node", + } + } + ) + + +@pytest.mark.parametrize("factory", [ApplyResult, OperationStatus]) +def test_runtime_changed_addresses_are_canonical_and_unique(factory) -> None: + kwargs = {"success": True, "snapshot": RuntimeSnapshot()} if factory is ApplyResult else {} + with pytest.raises(ValueError, match="canonical compiled address"): + factory(changed_addresses=["bad address"], **kwargs) + with pytest.raises(ValueError, match="unique"): + factory(changed_addresses=["provision.node.vm", "provision.node.vm"], **kwargs) + + +def test_published_plan_rejects_duplicate_operation_addresses() -> None: + operation = { + "action": "create", + "address": "provision.node.vm", + "resource_type": "node", + } + + with pytest.raises(ValidationError, match="operation addresses"): + ProvisioningPlanModel(operations=[operation, operation]) + + +def test_provider_name_is_bounded_and_collision_resistant_for_full_address() -> None: + first = provider_resource_name( + "provision.node.first.shared", + prefix="aces", + maximum_length=63, + ) + second = provider_resource_name( + "provision.node.second.shared", + prefix="aces", + maximum_length=63, + ) + + assert first != second + assert first == provider_resource_name( + "provision.node.first.shared", + prefix="aces", + maximum_length=63, + ) + assert len(first) <= 63 + assert first.startswith("aces-") diff --git a/implementations/python/tests/test_sdl_lineage.py b/implementations/python/tests/test_sdl_lineage.py new file mode 100644 index 000000000..1e8792b4f --- /dev/null +++ b/implementations/python/tests/test_sdl_lineage.py @@ -0,0 +1,335 @@ +"""Revision-pinned SDL lineage and provenance contract tests.""" + +from __future__ import annotations + +import json +import sys +from copy import deepcopy +from pathlib import Path + +import pytest +from pydantic import ValidationError + +REPO_ROOT = Path(__file__).resolve().parents[3] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +import tools.check_sdl_lineage as lineage_checker # noqa: E402 +from aces_contracts.provenance import SDLLineageLedgerModel # noqa: E402 +from tools.check_sdl_lineage import ( # noqa: E402 + _canonical_subjects, + _validate_authorities, + _validate_bibliography, + _validate_current_prose, + _validate_internal_paths, + evaluate, +) + +LEDGER_PATH = REPO_ROOT / "contracts/provenance/sdl-lineage-ledger-v1.json" + + +def _payload() -> dict[str, object]: + return json.loads(LEDGER_PATH.read_text(encoding="utf-8")) + + +def test_real_lineage_ledger_is_valid_and_covers_exact_current_subject_set() -> None: + ledger = SDLLineageLedgerModel.model_validate(_payload()) + current = {subject.subject_id for subject in ledger.subjects if subject.disposition.value == "current"} + assert current == _canonical_subjects(REPO_ROOT) + assert len(current) == 70 + assert {subject.subject_id for subject in ledger.subjects if subject.disposition.value == "removed"} == { + "sdl-field:evaluations", + "sdl-field:goals", + "sdl-field:metrics", + "sdl-field:tlos", + } + + +def test_real_lineage_policy_gate_is_clean() -> None: + assert evaluate(REPO_ROOT) == [] + + +def test_git_source_requires_full_revision_pin() -> None: + payload = _payload() + payload["sources"][0]["commit"] = "fe83e828" + with pytest.raises(ValidationError, match="string_pattern_mismatch"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_artifact_code_claim_requires_notice_disposition() -> None: + payload = _payload() + payload["third_party_dispositions"] = [] + with pytest.raises(ValidationError, match="lacks notice disposition"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_artifact_code_claim_rejects_unresolved_notice_disposition() -> None: + payload = _payload() + payload["third_party_dispositions"][0]["notice_decision"] = "unknown" + del payload["third_party_dispositions"][0]["notice_artifact"] + with pytest.raises(ValidationError, match="unresolved notice disposition"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_artifact_code_claim_must_be_covered_by_audited_derivation_scope() -> None: + payload = _payload() + claim = next( + claim for subject in payload["subjects"] for claim in subject["claims"] if claim["plane"] == "artifact_code" + ) + claim["aces_boundaries"] = [ + { + "artifact": "implementations/python/packages/aces_sdl/accounts.py", + "symbol_or_pointer": "Account", + } + ] + with pytest.raises(ValidationError, match="outside the audited derivation scope"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_third_party_disposition_is_unique_per_source() -> None: + payload = _payload() + payload["third_party_dispositions"].append(deepcopy(payload["third_party_dispositions"][0])) + with pytest.raises(ValidationError, match="third-party disposition source ids must be unique"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_git_source_urls_are_pinned_to_the_declared_commit() -> None: + payload = _payload() + payload["sources"][0]["canonical_url"] = "https://github.com/Open-Cyber-Range/SDL-parser" + with pytest.raises(ValidationError, match="declared commit"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_git_license_url_is_pinned_to_the_declared_commit() -> None: + payload = _payload() + payload["sources"][0]["license_url"] = "https://github.com/Open-Cyber-Range/SDL-parser/LICENSE" + with pytest.raises(ValidationError, match="license_url must contain the declared commit"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_required_notice_disposition_requires_notice_artifact() -> None: + payload = _payload() + del payload["third_party_dispositions"][0]["notice_artifact"] + with pytest.raises(ValidationError, match="required notice disposition must name notice_artifact"): + SDLLineageLedgerModel.model_validate(payload) + + +@pytest.mark.parametrize("decision", ["not_required", "unknown"]) +def test_non_required_notice_disposition_rejects_notice_artifact(decision: str) -> None: + payload = _payload() + payload["third_party_dispositions"][0]["notice_decision"] = decision + with pytest.raises(ValidationError, match="notice_artifact is valid only when a required notice is included"): + SDLLineageLedgerModel.model_validate(payload) + + +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" + ) + native_subject["claims"][0]["source_refs"] = [payload["sources"][0]["source_id"]] + native_subject["claims"][0]["compatibility"] = "compatible" + with pytest.raises(ValidationError, match="ACES-native claims"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_native_claim_requires_internal_authority_refs() -> None: + payload = _payload() + native_claim = next( + claim + for subject in payload["subjects"] + for claim in subject["claims"] + if claim["classification"] == "aces_native" + ) + native_claim["internal_authority_refs"] = [] + with pytest.raises(ValidationError, match="ACES-native claims require internal authority refs"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_native_claim_rejects_source_compatibility_relation() -> None: + payload = _payload() + native_claim = next( + claim + for subject in payload["subjects"] + for claim in subject["claims"] + if claim["classification"] == "aces_native" + ) + native_claim["compatibility"] = "partial" + with pytest.raises(ValidationError, match="no source compatibility relation"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_native_claim_rejects_source_compatibility_direction() -> None: + payload = _payload() + native_claim = next( + claim + for subject in payload["subjects"] + for claim in subject["claims"] + if claim["classification"] == "aces_native" + ) + native_claim["compatibility_direction"] = "aces_relative_to_source" + with pytest.raises(ValidationError, match="no source compatibility direction"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_non_native_claim_requires_source_boundary_and_citation_refs() -> None: + payload = _payload() + claim = next(claim for subject in payload["subjects"] for claim in subject["claims"] if claim["source_refs"]) + claim["source_boundaries"] = [] + with pytest.raises(ValidationError, match="non-native claims require source, source boundary, and citation refs"): + SDLLineageLedgerModel.model_validate(payload) + + +@pytest.mark.parametrize( + ("classification", "plane", "message"), + [ + ("adopted_syntax", "semantics", "adopted_syntax is valid only on the syntax plane"), + ("adopted_semantics", "syntax", "adopted_semantics is valid only on the semantics plane"), + ], +) +def test_adopted_classification_requires_matching_claim_plane( + classification: str, + plane: str, + message: str, +) -> None: + payload = _payload() + claim = next(claim for subject in payload["subjects"] for claim in subject["claims"] if claim["source_refs"]) + claim["classification"] = classification + claim["plane"] = plane + with pytest.raises(ValidationError, match=message): + SDLLineageLedgerModel.model_validate(payload) + + +def test_non_native_claim_requires_explicit_compatibility_direction() -> None: + payload = _payload() + claim = next( + claim + for subject in payload["subjects"] + for claim in subject["claims"] + if claim["classification"] != "aces_native" + ) + claim["compatibility_direction"] = "not_applicable" + with pytest.raises(ValidationError, match="assess ACES relative"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_subject_namespace_must_match_declared_kind() -> None: + payload = _payload() + payload["subjects"][0]["subject_kind"] = "runtime_family" + with pytest.raises(ValidationError, match="namespace .* does not match subject_kind"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_claim_citations_must_identify_the_claimed_sources() -> None: + payload = _payload() + claim = next(claim for subject in payload["subjects"] for claim in subject["claims"] if claim["source_refs"]) + claim["citation_refs"] = ["crack-cose-2020"] + with pytest.raises(ValidationError, match="citation refs do not identify its sources"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_native_internal_authority_refs_must_resolve() -> None: + payload = _payload() + native_claim = next( + claim + for subject in payload["subjects"] + for claim in subject["claims"] + if claim["classification"] == "aces_native" + ) + native_claim["internal_authority_refs"] = ["specs/sdl/does-not-exist.md"] + ledger = SDLLineageLedgerModel.model_validate(payload) + failures = _validate_internal_paths(REPO_ROOT, ledger) + assert any( + failure.rule_id == "lineage-internal-artifact-missing" and "specs/sdl/does-not-exist.md" in failure.message + for failure in failures + ) + + +def test_authority_contract_id_must_match_published_schema_path() -> None: + payload = _payload() + subject = next(item for item in payload["subjects"] if item["authority"].get("contract_id")) + subject["authority"]["contract_id"] = "semantic-profile-v1" + ledger = SDLLineageLedgerModel.model_validate(payload) + failures = _validate_authorities(REPO_ROOT, ledger) + assert any(failure.rule_id == "lineage-authority-contract-mismatch" for failure in failures) + + +def test_planned_subject_cannot_claim_current_compatibility() -> None: + payload = _payload() + subject = deepcopy(payload["subjects"][0]) + subject["subject_id"] = "sdl-field:future" + subject["disposition"] = "planned" + subject["claims"][0]["compatibility"] = "compatible" + payload["subjects"].append(subject) + with pytest.raises(ValidationError, match="planned subject"): + SDLLineageLedgerModel.model_validate(payload) + + +def test_crack_publications_have_distinct_verified_identities() -> None: + citations = {item["citation_id"]: item for item in _payload()["citations"]} + assert citations["crack-nca-2018"]["doi"] == "10.1109/NCA.2018.8548324" + assert citations["crack-nca-2018"]["title"] == "Scenario Design and Validation for Next Generation Cyber Ranges" + assert citations["crack-cose-2020"]["doi"] == "10.1016/j.cose.2020.101837" + assert citations["crack-cose-2020"]["title"] == "Building next generation Cyber Ranges with CRACK" + + +def test_bibliography_rejects_conflicting_identity_for_one_doi() -> None: + payload = _payload() + citations = {item["citation_id"]: item for item in payload["citations"]} + citations["crack-cose-2020"]["doi"] = citations["crack-nca-2018"]["doi"] + citations["crack-cose-2020"]["canonical_url"] = citations["crack-nca-2018"]["canonical_url"] + ledger = SDLLineageLedgerModel.model_validate(payload) + failures = _validate_bibliography(ledger) + assert [failure.rule_id for failure in failures] == ["lineage-doi-identity-conflict"] + + +def test_bibliography_rejects_doi_url_mismatch() -> None: + payload = _payload() + citation = next(item for item in payload["citations"] if item.get("doi")) + citation["canonical_url"] = "https://example.org/wrong-publication" + ledger = SDLLineageLedgerModel.model_validate(payload) + failures = _validate_bibliography(ledger) + assert [failure.rule_id for failure in failures] == ["lineage-doi-url-mismatch"] + + +@pytest.mark.parametrize( + ("mutation", "expected_rule"), + [ + ("missing", "lineage-current-subjects-missing"), + ("unexpected", "lineage-current-subjects-unexpected"), + ], +) +def test_evaluate_rejects_bidirectional_current_subject_drift( + monkeypatch: pytest.MonkeyPatch, + mutation: str, + expected_rule: str, +) -> None: + payload = _payload() + if mutation == "missing": + payload["subjects"][0]["disposition"] = "removed" + else: + subject = deepcopy(payload["subjects"][0]) + subject["subject_id"] = "sdl-field:future" + payload["subjects"].append(subject) + + original_load_json = lineage_checker._load_json + + def load_json(repo_root: Path, rel_path: str) -> object: + if rel_path == lineage_checker.LEDGER_PATH: + return payload + return original_load_json(repo_root, rel_path) + + monkeypatch.setattr(lineage_checker, "_load_json", load_json) + failures = lineage_checker.evaluate(REPO_ROOT) + assert expected_rule in {failure.rule_id for failure in failures} + + +def test_prose_doi_label_year_must_match_verified_identity(tmp_path: Path) -> None: + prose_path = tmp_path / "lineage.md" + prose_path.write_text( + "[Russo et al. 2018](https://doi.org/10.1016/j.cose.2020.101837)\n", + encoding="utf-8", + ) + ledger = SDLLineageLedgerModel.model_validate(_payload()) + failures = _validate_current_prose(tmp_path, ledger, ("lineage.md",)) + assert [failure.rule_id for failure in failures] == ["lineage-doi-label-year-mismatch"] diff --git a/implementations/python/tests/test_sdl_models.py b/implementations/python/tests/test_sdl_models.py index 63a09bf36..35d91b8cc 100644 --- a/implementations/python/tests/test_sdl_models.py +++ b/implementations/python/tests/test_sdl_models.py @@ -1268,11 +1268,11 @@ def test_runtime_container_seccomp_consistency_accepts_agreeing_values(self): def test_runtime_container_seccomp_consistency_allows_variable_placeholder(self): container = RuntimeContainerConfiguration( - seccomp_profile="${SECCOMP}", + seccomp_profile="${seccomp}", security_opt=["seccomp:unconfined"], ) - assert container.seccomp_profile == "${SECCOMP}" + assert container.seccomp_profile == "${seccomp}" def test_runtime_container_seccomp_rejects_disagreeing_security_opt_entries(self): with pytest.raises(ValidationError, match="seccomp_profile"): @@ -1851,14 +1851,14 @@ def test_runtime_network_is_optional(self): def test_endpoint_accepts_variable_placeholders(self): ep = RuntimeNetworkEndpoint( network="aptl-dmz", - ip_address="${WEBAPP_IP}", - gateway="${DMZ_GATEWAY}", - mac_address="${WEBAPP_MAC}", - ip_prefix_length="${PREFIX}", + ip_address="${webapp_ip}", + gateway="${dmz_gateway}", + mac_address="${webapp_mac}", + ip_prefix_length="${prefix}", ) - assert ep.ip_address == "${WEBAPP_IP}" - assert ep.mac_address == "${WEBAPP_MAC}" - assert ep.ip_prefix_length == "${PREFIX}" + assert ep.ip_address == "${webapp_ip}" + assert ep.mac_address == "${webapp_mac}" + assert ep.ip_prefix_length == "${prefix}" def test_published_port_protocol_normalized_and_required(self): binding = RuntimePublishedPort(container_port="443", protocol="TCP") @@ -2237,17 +2237,17 @@ def test_speed_below_1_rejected(self): class TestObjectiveSuccess: def test_requires_at_least_one_reference(self): - with pytest.raises(ValidationError, match="at least one condition"): + with pytest.raises(ValidationError, match="at least one assertion"): ObjectiveSuccess() - def test_accepts_condition_reference(self): - success = ObjectiveSuccess(conditions=["exercise-passed"]) - assert success.conditions == ["exercise-passed"] + def test_accepts_assertion_reference(self): + success = ObjectiveSuccess(assertions=["exercise-passed"]) + assert success.assertions == ["exercise-passed"] def test_mode_placeholder(self): success = ObjectiveSuccess( mode="${objective_mode}", - conditions=["exercise-passed"], + assertions=["exercise-passed"], ) assert success.mode == "${objective_mode}" @@ -2256,14 +2256,14 @@ class TestObjective: def test_requires_exactly_one_actor_binding(self): with pytest.raises(ValidationError, match="exactly one"): Objective( - success={"conditions": ["c1"]}, + success={"assertions": ["c1"]}, ) with pytest.raises(ValidationError, match="exactly one"): Objective( agent="red-agent", entity="red-team", - success={"conditions": ["c1"]}, + success={"assertions": ["c1"]}, ) def test_valid_agent_objective(self): @@ -2271,7 +2271,7 @@ def test_valid_agent_objective(self): agent="red-agent", actions=["Scan"], targets=["web-server"], - success={"conditions": ["initial-access"]}, + success={"assertions": ["initial-access"]}, window={ "scripts": ["main-timeline"], "events": ["attack-wave"], @@ -2281,14 +2281,14 @@ def test_valid_agent_objective(self): depends_on=["recon"], ) assert objective.agent == "red-agent" - assert objective.success.conditions == ["initial-access"] + assert objective.success.assertions == ["initial-access"] assert isinstance(objective.window, ObjectiveWindow) assert objective.window.steps == ["response-flow.validate"] def test_valid_entity_objective(self): objective = Objective( entity="blue-team", - success={"conditions": ["report-quality"]}, + success={"assertions": ["report-quality"]}, ) assert objective.entity == "blue-team" @@ -2313,7 +2313,7 @@ def test_dataset_content(self): type="dataset", target="exchange", format="eml", - items=[ContentItem(name="email.eml", tags=["phishing"])], + items=[ContentItem(name="email", display_name="email.eml", tags=["phishing"])], ) assert len(c.items) == 1 assert c.items[0].tags == ["phishing"] @@ -2541,7 +2541,7 @@ def test_objective_step(self): step = WorkflowStep( type="objective", objective="verify-release", - **{"on-success": "done"}, + **{"on_success": "done"}, ) assert step.type == WorkflowStepType.OBJECTIVE assert step.objective == "verify-release" @@ -2551,7 +2551,7 @@ def test_decision_step_requires_branches(self): ValidationError, match="requires 'when', 'then', and 'else'", ): - WorkflowStep(type="decision", when={"conditions": ["c1"]}) + WorkflowStep(type="decision", when={"assertions": ["c1"]}) def test_parallel_step_requires_unique_branches(self): with pytest.raises(ValidationError, match="branches must be unique"): @@ -2564,7 +2564,7 @@ def test_valid_workflow(self): "validate": { "type": "objective", "objective": "verify-release", - "on-success": "done", + "on_success": "done", }, "done": {"type": "end"}, }, @@ -2576,7 +2576,7 @@ def test_retry_step(self): step = WorkflowStep( type="retry", objective="verify-release", - **{"on-success": "done", "max-attempts": 5}, + **{"on_success": "done", "max_attempts": 5}, ) assert step.type == WorkflowStepType.RETRY assert step.objective == "verify-release" @@ -2595,7 +2595,7 @@ def test_switch_step(self): type="switch", cases=[ { - "when": {"conditions": ["c1"]}, + "when": {"assertions": ["c1"]}, "next": "done", } ], @@ -2609,7 +2609,7 @@ def test_call_step(self): step = WorkflowStep( type="call", workflow="child", - **{"on-success": "done"}, + **{"on_success": "done"}, ) assert step.type == WorkflowStepType.CALL assert step.workflow == "child" @@ -2622,7 +2622,7 @@ def test_workflow_timeout_scalar_parses_to_policy(self): "validate": { "type": "objective", "objective": "verify-release", - "on-success": "done", + "on_success": "done", }, "done": {"type": "end"}, }, @@ -2639,8 +2639,8 @@ def test_retry_step_forbids_decision_fields(self): type="retry", objective="verify-release", **{ - "on-success": "done", - "max-attempts": 3, + "on_success": "done", + "max_attempts": 3, "then": "a", "else": "b", }, @@ -2651,14 +2651,14 @@ def test_retry_max_attempts_must_be_positive(self): WorkflowStep( type="retry", objective="verify-release", - **{"on-success": "done", "max-attempts": 0}, + **{"on_success": "done", "max_attempts": 0}, ) def test_retry_max_attempts_accepts_variable(self): step = WorkflowStep( type="retry", objective="verify-release", - **{"on-success": "done", "max-attempts": "${max_retries}"}, + **{"on_success": "done", "max_attempts": "${max_retries}"}, ) assert step.max_attempts == "${max_retries}" @@ -2666,7 +2666,7 @@ def test_on_failure_on_objective_step(self): step = WorkflowStep( type="objective", objective="verify-release", - **{"on-success": "done", "on-failure": "recover"}, + **{"on_success": "done", "on_failure": "recover"}, ) assert step.on_failure == "recover" @@ -2675,7 +2675,7 @@ def test_on_failure_on_parallel_step(self): type="parallel", branches=["a", "b"], join="done", - **{"on-failure": "recover"}, + **{"on_failure": "recover"}, ) assert step.on_failure == "recover" @@ -2684,9 +2684,9 @@ def test_on_exhausted_accepts_variable(self): type="retry", objective="verify-release", **{ - "on-success": "done", - "max-attempts": 3, - "on-exhausted": "${recovery_step}", + "on_success": "done", + "max_attempts": 3, + "on_exhausted": "${recovery_step}", }, ) assert step.on_exhausted == "${recovery_step}" @@ -2698,8 +2698,8 @@ def test_on_failure_forbidden_on_decision_step(self): ): WorkflowStep( type="decision", - when={"conditions": ["c1"]}, - **{"then": "a", "else": "b", "on-failure": "recover"}, + when={"assertions": ["c1"]}, + **{"then": "a", "else": "b", "on_failure": "recover"}, ) def test_join_step_requires_next(self): @@ -2707,7 +2707,7 @@ def test_join_step_requires_next(self): WorkflowStep(type="join") def test_step_state_predicate(self): - pred = WorkflowPredicate(steps=[{"step": "step-a", "outcomes": ["failed"], "min-attempts": 2}]) + pred = WorkflowPredicate(steps=[{"step": "step-a", "outcomes": ["failed"], "min_attempts": 2}]) assert pred.steps[0].step == "step-a" assert pred.steps[0].outcomes == [WorkflowStepOutcome.FAILED] assert pred.steps[0].min_attempts == 2 @@ -2723,7 +2723,7 @@ def test_predicate_with_only_step_state_is_valid(self): def test_legacy_workflow_step_type_rejected(self): with pytest.raises(ValidationError, match="no longer supported"): - WorkflowStep(type="if", when={"conditions": ["c1"]}, **{"then": "a", "else": "b"}) + WorkflowStep(type="if", when={"assertions": ["c1"]}, **{"then": "a", "else": "b"}) def test_predicate_empty_rejected(self): with pytest.raises(ValidationError, match="must reference at least one"): @@ -2802,13 +2802,13 @@ def test_requires_entity(self): def test_default_framing_lists_are_empty(self): a = Agent(entity="red-team") - assert a.starting_conditions == [] + assert a.starting_assertions == [] assert a.authority_anchors == [] assert a.operating_scope == [] - def test_starting_conditions_field(self): - a = Agent(entity="red-team", starting_conditions=["beacon-online", "vpn-up"]) - assert a.starting_conditions == ["beacon-online", "vpn-up"] + def test_starting_assertions_field(self): + a = Agent(entity="red-team", starting_assertions=["beacon-online", "vpn-up"]) + assert a.starting_assertions == ["beacon-online", "vpn-up"] def test_authority_anchors_field(self): a = Agent( @@ -2827,11 +2827,11 @@ def test_operating_scope_field(self): def test_framing_fields_accept_variable_placeholders(self): a = Agent( entity="red-team", - starting_conditions=["${beacon_condition}"], + starting_assertions=["${beacon_condition}"], authority_anchors=["${authority_ref}"], operating_scope=["${scope_ref}"], ) - assert a.starting_conditions == ["${beacon_condition}"] + assert a.starting_assertions == ["${beacon_condition}"] assert a.authority_anchors == ["${authority_ref}"] assert a.operating_scope == ["${scope_ref}"] @@ -3011,11 +3011,11 @@ def test_route_method_must_be_known(self): RuntimeApplicationRoute(route_id="r1", path="/login", methods=["FETCH"]) def test_route_id_rejects_variable_placeholder(self): - with pytest.raises(ValidationError, match="must be a stable identifier"): + with pytest.raises(ValidationError, match="portable SDL identifier"): RuntimeApplicationRoute(route_id="${rid}", path="/login", methods=["GET"]) def test_application_id_rejects_variable_placeholder(self): - with pytest.raises(ValidationError, match="must be a stable identifier"): + with pytest.raises(ValidationError, match="portable SDL identifier"): RuntimeApplicationSurface(application_id="${aid}") def test_response_status_code_range(self): @@ -3623,11 +3623,11 @@ def test_listener_port_range_enforced(self): DatabaseListener(address="*", port=70000) def test_database_service_id_rejects_variable_placeholder(self): - with pytest.raises(ValidationError, match="database_service_id must be a stable identifier"): + with pytest.raises(ValidationError, match="database_service_id must be a portable SDL identifier"): RuntimeDatabaseService(database_service_id="${svc}") def test_table_id_rejects_variable_placeholder(self): - with pytest.raises(ValidationError, match="table_id must be a stable identifier"): + with pytest.raises(ValidationError, match="table_id must be a portable SDL identifier"): DatabaseTable(table_id="${t}", name="users") def test_object_name_allows_variable_placeholder(self): @@ -3711,8 +3711,8 @@ def test_secret_bearing_name_with_empty_value_allows_plain_classification(self): assert setting.value_classification == RuntimeSensitivityClassification.PLAIN def test_secret_bearing_name_with_variable_classification_is_skipped(self): - setting = DatabaseSetting(name="password", value_classification="${CLS}") - assert setting.value_classification == "${CLS}" + setting = DatabaseSetting(name="password", value_classification="${cls}") + assert setting.value_classification == "${cls}" def test_non_secret_setting_keeps_default_unknown_classification(self): setting = DatabaseSetting(name="shared_buffers", value="128MB") @@ -3740,9 +3740,9 @@ def test_unknown_engine_rejected(self): RuntimeDatabaseService(database_service_id="svc", engine="cobol-db") def test_engine_protocol_accept_variable_placeholder(self): - svc = RuntimeDatabaseService(database_service_id="svc", engine="${ENGINE}", protocol="${PROTO}") - assert svc.engine == "${ENGINE}" - assert svc.protocol == "${PROTO}" + svc = RuntimeDatabaseService(database_service_id="svc", engine="${engine}", protocol="${proto}") + assert svc.engine == "${engine}" + assert svc.protocol == "${proto}" @pytest.mark.parametrize( "engine,bad_protocol,expected_protocol", @@ -3766,8 +3766,8 @@ def test_postgresql_engine_default_protocol_other_is_rejected(self): RuntimeDatabaseService(database_service_id="svc", engine="postgresql") def test_engine_with_variable_protocol_is_skipped(self): - svc = RuntimeDatabaseService(database_service_id="svc", engine="postgresql", protocol="${PROTO}") - assert svc.protocol == "${PROTO}" + svc = RuntimeDatabaseService(database_service_id="svc", engine="postgresql", protocol="${proto}") + assert svc.protocol == "${proto}" def test_mariadb_engine_accepts_mysql_protocol(self): svc = RuntimeDatabaseService(database_service_id="svc", engine="mariadb", protocol="mysql") diff --git a/implementations/python/tests/test_sdl_module_registry.py b/implementations/python/tests/test_sdl_module_registry.py index e19a6b638..9ee11c333 100644 --- a/implementations/python/tests/test_sdl_module_registry.py +++ b/implementations/python/tests/test_sdl_module_registry.py @@ -61,22 +61,6 @@ def _local_module(path: Path, *, version: str = "1.2.3", exports: str = "nodes: ) -def _flat_equivalent(path: Path) -> Path: - return _write( - path, - """ - name: flat - nodes: - shared.vm: - type: vm - os: linux - resources: {ram: 1 gib, cpu: 1} - infrastructure: - shared.vm: 1 - """, - ) - - def _root_import(path: Path, import_body: str) -> Path: lines = textwrap.dedent(import_body).strip().splitlines() import_lines = [f" - {lines[0].strip()}"] @@ -181,7 +165,6 @@ def test_local_path_source_and_locked_imports_compile_equivalently(tmp_path: Pat tmp_path / "root-source.yaml", "source: local:shared.yaml\n namespace: shared\n version: 1.2.3", ) - flat = _flat_equivalent(tmp_path / "flat.yaml") runner = CliRunner() resolve_result = runner.invoke(app, ["sdl", "resolve", str(root_source)]) @@ -198,20 +181,14 @@ def test_local_path_source_and_locked_imports_compile_equivalently(tmp_path: Pat path_model = compile_runtime_model(parse_sdl_file(root_path)) source_model = compile_runtime_model(parse_sdl_file(root_source)) locked_model = compile_runtime_model(parse_sdl_file(root_locked)) - flat_model = compile_runtime_model(parse_sdl_file(flat)) assert ( path_model.node_deployments.keys() == source_model.node_deployments.keys() == locked_model.node_deployments.keys() - == flat_model.node_deployments.keys() - ) - assert ( - path_model.networks.keys() - == source_model.networks.keys() - == locked_model.networks.keys() - == flat_model.networks.keys() + == {"provision.node.shared.vm"} ) + assert path_model.networks.keys() == source_model.networks.keys() == locked_model.networks.keys() == set() assert module_path.exists() @@ -264,22 +241,68 @@ def test_module_exports_are_enforced_for_importers(tmp_path: Path): check: entity: blue success: - conditions: [shared.health] + assertions: [shared.health] """, ) with pytest.raises( SDLValidationError, - match=r"Objective 'check' references undefined condition 'shared\.health' in success criteria", + match=r"Objective 'check' references undefined assertion 'shared\.health' in success criteria", ): parse_sdl_file(root) +@pytest.mark.parametrize("exported", [True, False]) +def test_scenario_forwarding_agents_compose_by_stable_list_identity(tmp_path: Path, exported: bool): + exports = " forwarding_agents: [shipper]\n" if exported else "" + _write( + tmp_path / "shared.yaml", + f""" + name: shared + version: 1.0.0 + module: + id: aces/shared-forwarder + version: 1.0.0 + exports: + nodes: [source, sink] + relationships: [shipping] + {exports.rstrip()} + nodes: + source: {{type: switch}} + sink: {{type: switch}} + forwarding_agents: + - forwarding_agent_id: shipper + relationships: + shipping: + type: connects_to + source: source + target: sink + forwarding_edge: + forwarder_ref: shipper + """, + ) + root = _root_import( + tmp_path / "root.yaml", + "source: local:shared.yaml\n namespace: shared", + ) + + scenario = parse_sdl_file(root) + + expected = "shared.shipper" if exported else "shared.__private.shipper" + assert isinstance(scenario.forwarding_agents, list) + assert [agent.forwarding_agent_id for agent in scenario.forwarding_agents] == [expected] + assert scenario.relationships["shared.shipping"].forwarding_edge.forwarder_ref == expected + + def test_import_cycles_and_namespace_collisions_are_rejected(tmp_path: Path): a = _write( tmp_path / "a.yaml", """ name: a + version: 1.0.0 + module: + id: aces/a + version: 1.0.0 imports: - source: local:b.yaml namespace: other @@ -289,6 +312,10 @@ def test_import_cycles_and_namespace_collisions_are_rejected(tmp_path: Path): tmp_path / "b.yaml", """ name: b + version: 1.0.0 + module: + id: aces/b + version: 1.0.0 imports: - source: local:a.yaml namespace: other @@ -727,15 +754,11 @@ def test_signed_oci_import_resolution_and_publish_cli(tmp_path: Path): tmp_path / "root-locked.yaml", f"source: locked:{lockfile.imports[0].resolved_source}\n namespace: shared", ) - flat = _flat_equivalent(tmp_path / "flat.yaml") remote_model = compile_runtime_model(parse_sdl_file(root)) locked_model = compile_runtime_model(parse_sdl_file(locked)) - flat_model = compile_runtime_model(parse_sdl_file(flat)) assert ( - remote_model.node_deployments.keys() - == locked_model.node_deployments.keys() - == flat_model.node_deployments.keys() + remote_model.node_deployments.keys() == locked_model.node_deployments.keys() == {"provision.node.shared.vm"} ) diff --git a/implementations/python/tests/test_sdl_parser.py b/implementations/python/tests/test_sdl_parser.py index 36bf61e75..f8a6dde7f 100644 --- a/implementations/python/tests/test_sdl_parser.py +++ b/implementations/python/tests/test_sdl_parser.py @@ -1,9 +1,9 @@ -"""Tests for SDL parser — YAML loading, key normalization, shorthands.""" +"""Tests for SDL parsing, canonical fields, migration, and shorthands.""" -import re from pathlib import Path import pytest +from aces_sdl import SDLMigrationPolicy from aces.core.sdl import instantiate_scenario from aces.core.sdl._errors import SDLParseError, SDLValidationError @@ -17,12 +17,20 @@ def test_lowercase_keys(self): assert "sw" in s.nodes def test_uppercase_keys(self): - """Pydantic field keys are normalized but user-defined names are preserved.""" - s = parse_sdl("Name: test\nNodes:\n SW:\n Type: Switch") - assert "SW" in s.nodes # user-defined name preserved as-is - assert s.nodes["SW"].type == NodeType.SWITCH # enum value normalized + """Explicit migration normalizes fields without rewriting identifiers.""" + s = parse_sdl( + "Name: test\nNodes:\n sw:\n Type: Switch", + migration_policy=SDLMigrationPolicy.ACCEPT, + ) + assert "sw" in s.nodes + assert s.nodes["sw"].type == NodeType.SWITCH + assert [diagnostic.code for diagnostic in s.source_diagnostics] == [ + "sdl.noncanonical_field", + "sdl.noncanonical_field", + "sdl.noncanonical_field", + ] - def test_hyphenated_keys(self): + def test_hyphenated_identifier_keys(self): sdl = """ name: test nodes: @@ -41,7 +49,7 @@ def test_hyphenated_keys(self): def test_integer_keys_in_user_defined_mapping_are_rejected(self): # YAML lets authors write a bare ``1:`` as a key, which yaml.safe_load # parses as an integer. User-defined hashmap keys (node names, role - # names, etc.) bypass the field-key normalization pass so the + # names, etc.) bypass the structural-field pass so the # integer survives until Pydantic. Closed-world ``SDLModel`` rejects # non-string keys; this test pins that contract so a future loosening # of the dict-key types (or a silent coerce-to-string) surfaces as a @@ -132,18 +140,19 @@ def test_non_string_top_level_keys_are_rejected_cleanly(self): ${objective_name}: agent: red-agent success: - conditions: [initial-access] + assertions: [initial-access] """, "objectives.${objective_name}", ), ], ) def test_variable_placeholders_rejected_in_mapping_keys(self, sdl, key_path): - with pytest.raises( - SDLParseError, - match=re.escape(f"user-defined mapping keys: '{key_path}'"), - ): + with pytest.raises(SDLParseError) as caught: parse_sdl(sdl) + if ".properties" in key_path: + assert f"user-defined mapping keys: '{key_path}'" in str(caught.value) + else: + assert caught.value.diagnostics[0].code == "sdl.identifier.invalid" def test_variable_declaration_names_must_match_contract_grammar(self): sdl = """ @@ -153,8 +162,10 @@ def test_variable_declaration_names_must_match_contract_grammar(self): type: string default: value """ - with pytest.raises(SDLParseError, match="String should match pattern"): + with pytest.raises(SDLParseError) as caught: parse_sdl(sdl) + assert caught.value.diagnostics[0].code == "sdl.identifier.invalid" + assert caught.value.diagnostics[0].pointer == "/variables/bad.name" class TestShorthandExpansion: @@ -178,11 +189,11 @@ def test_objectives_section_parses(self): actions: [Scan] targets: [red-agent] success: - conditions: [initial-access] + assertions: [initial-access] """ s = parse_sdl(sdl, skip_semantic_validation=True) assert s.objectives["initial-access"].agent == "red-agent" - assert s.objectives["initial-access"].success.conditions == ["initial-access"] + assert s.objectives["initial-access"].success.assertions == ["initial-access"] assert s.advisories == [] def test_workflows_section_parses(self): @@ -195,11 +206,19 @@ def test_workflows_section_parses(self): release-ready: command: /bin/check interval: 30 +propositions: + release-ready: + 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} +assertions: + release-ready: {proposition: release-ready, role: postcondition, polarity: positive} objectives: validate-release: entity: blue-team success: - conditions: [release-ready] + assertions: [release-ready] workflows: release-response: start: validate @@ -207,7 +226,7 @@ def test_workflows_section_parses(self): validate: type: objective objective: validate-release - on-success: finish + on_success: finish finish: type: end """ @@ -236,84 +255,84 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): mounts: - target: /shuffle-database source: aptl_shuffle_data - source-sensitivity: plain - source-kind: volume - filesystem-type: ext4 - read-only: false + source_sensitivity: plain + source_kind: volume + filesystem_type: ext4 + read_only: false options: [rw, nosuid] - options-sensitivity: plain + options_sensitivity: plain propagation: rprivate stability: volume-backed - backend-generated: true - filesystem-inventory: + backend_generated: true + filesystem_inventory: - path: /app/app.py - entry-type: file - owner-user: root - owner-group: root + entry_type: file + owner_user: root + owner_group: root uid: "0" gid: "0" mode: "0644" size: "4096" - content-digest: 4f8c2d - digest-algorithm: sha256 - source-path: src/webapp/app.py + content_digest: 4f8c2d + digest_algorithm: sha256 + source_path: src/webapp/app.py provenance: python-package stability: stable sensitivity: plain - path: /var/log/gunicorn/access.log - entry-type: file + entry_type: file mode: "0600" stability: log sensitivity: operator-secret - local-control-interfaces: - - control-interface-id: docker-sock + local_control_interfaces: + - control_interface_id: docker-sock path: /run/docker.sock kind: unix-socket protocol: docker - bind-source-sensitivity: operator-secret + bind_source_sensitivity: operator-secret access: read-write processes: - name: shufflebackend command: ./shufflebackend user: root - working-directory: /app + working_directory: /app - name: supervisord pid: 1 command: supervisord -n role: supervisor - name: gunicorn - parent-pid: 1 + parent_pid: 1 command: [gunicorn, app:app] role: worker environment: - name: TECHVAULT_ADMIN_PASSWORD - value-classification: redacted + value_classification: redacted provenance: operator - name: SCENARIO_FIXTURE_TOKEN value: fixture-token - value-classification: secret-fixture + value_classification: secret-fixture provenance: compose - linux-capabilities: + linux_capabilities: required: [CAP_NET_ADMIN] effective: CAP_NET_ADMIN - process-overrides: + process_overrides: - subject: name: gunicorn - parent-pid: 1 + parent_pid: 1 scope: subtree drop: [cap-audit-control] description: interactive participant shell - operational-policy: + operational_policy: restart: unless-stopped - resource-limits: + resource_limits: memory: 512 MiB cpu: 0.5 pids: 128 container: entrypoint: [/entrypoint.sh] command: [gunicorn, app:app] - log-driver: json-file - log-options: + log_driver: json-file + log_options: max-size: 10m max-file: "3" namespaces: @@ -323,74 +342,74 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): userns: host uts: private privileged: false - read-only-rootfs: false - publish-all-ports: false + read_only_rootfs: false + publish_all_ports: false autoremove: false - shm-size: 64 MiB - masked-paths: [/proc/acpi, /proc/kcore] - read-only-paths: /proc/sys - cgroup-parent: /docker - runtime-name: runc - init-process: + shm_size: 64 MiB + masked_paths: [/proc/acpi, /proc/kcore] + read_only_paths: /proc/sys + cgroup_parent: /docker + runtime_name: runc + init_process: enabled: true implementation: docker-init - executable-path: /sbin/docker-init - reaps-children: true + executable_path: /sbin/docker-init + reaps_children: true argv: [/sbin/docker-init, "--", /entrypoint.sh] devices: - - host-path: /dev/null - container-path: /dev/null + - host_path: /dev/null + container_path: /dev/null permissions: rwm - device-cgroup-rules: c 1:3 rwm - extra-hosts: + device_cgroup_rules: c 1:3 rwm + extra_hosts: - hostname: wazuh-manager address: 172.20.0.10 dns: [8.8.8.8] - dns-options: ndots:0 - dns-search: [techvault.local] - group-add: [adm, "101"] + dns_options: ndots:0 + dns_search: [techvault.local] + group_add: [adm, "101"] health: status: healthy - failing-streak: "0" + failing_streak: "0" log: - start: "2026-05-20T12:00:00Z" end: "2026-05-20T12:00:01Z" - exit-code: "0" + exit_code: "0" output: ok packages: - manager: apk name: musl version: 1.2.4-r2 - software-components: - - component-id: shuffle-backend-app + software_components: + - component_id: shuffle-backend-app name: shuffle-backend version: 1.2.3 - component-type: application + component_type: application provenance: scanner ecosystem: go purl: "pkg:golang/github.com/frikky/shuffle@1.2.3" cpe: "cpe:2.3:a:shuffle:shuffle:1.2.3:*:*:*:*:*:*:*" - package-manager: apk - package-name: shuffle-backend - package-version: 1.2.3-r0 - manifest-path: /app/go.mod - installed-paths: [/app/shufflebackend, /app/go.mod] + package_manager: apk + package_name: shuffle-backend + package_version: 1.2.3-r0 + manifest_path: /app/go.mod + installed_paths: [/app/shufflebackend, /app/go.mod] hashes: - algorithm: sha256 value: abc123 - dependency-manifests: + dependency_manifests: - ecosystem: go path: /app/go.mod format: go-module - package-vulnerabilities: + package_vulnerabilities: - id: CVE-2026-12345 - package-name: musl - installed-version: 1.2.4-r2 - fixed-version: 1.2.5-r0 + package_name: musl + installed_version: 1.2.4-r2 + fixed_version: 1.2.5-r0 severity: high scanner: trivy - image-digest: sha256:abc123 - scan-time: "2026-05-20T12:00:00Z" + image_digest: sha256:abc123 + scan_time: "2026-05-20T12:00:00Z" """ scenario = parse_sdl(sdl) node = scenario.nodes["shuffle-backend"] @@ -489,7 +508,7 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): assert node.runtime.package_vulnerabilities[0].image_digest == "sha256:abc123" assert node.runtime.package_vulnerabilities[0].scan_time == "2026-05-20T12:00:00Z" - def test_runtime_local_identity_inventory_parses_with_kebab_keys(self): + def test_runtime_local_identity_inventory_parses_with_canonical_keys(self): sdl = """ name: techvault-identity-inventory nodes: @@ -497,13 +516,13 @@ def test_runtime_local_identity_inventory_parses_with_kebab_keys(self): type: vm os: linux runtime: - local-identity: + local_identity: description: getent passwd/group capture users: - username: root uid: 0 - primary-gid: 0 - primary-group: root + primary_gid: 0 + primary_group: root gecos: root home: /root shell: /bin/bash @@ -511,12 +530,12 @@ def test_runtime_local_identity_inventory_parses_with_kebab_keys(self): stability: stable - username: www-data uid: 33 - primary-gid: 33 - primary-group: www-data + primary_gid: 33 + primary_group: www-data home: /var/www shell: /usr/sbin/nologin - supplemental-groups: [wazuh] - no-login: true + supplemental_groups: [wazuh] + no_login: true provenance: package groups: - name: root @@ -525,10 +544,10 @@ def test_runtime_local_identity_inventory_parses_with_kebab_keys(self): - name: wazuh gid: 101 members: [www-data] - sudo-rules: + sudo_rules: - principal: operator - principal-kind: user - run-as-users: [root] + principal_kind: user + run_as_users: [root] commands: ["/usr/bin/systemctl restart gunicorn"] nopasswd: true """ @@ -562,13 +581,13 @@ def test_runtime_local_identity_uid_variable_substitutes_on_instantiation(self): type: vm os: linux runtime: - local-identity: + local_identity: users: - username: wazuh uid: ${svc_uid} home: /var/ossec shell: /usr/sbin/nologin - no-login: true + no_login: true """ raw = parse_sdl(sdl) assert raw.nodes["techvault-webapp"].runtime.local_identity.users[0].uid == "${svc_uid}" @@ -577,7 +596,7 @@ def test_runtime_local_identity_uid_variable_substitutes_on_instantiation(self): assert user.uid == 999 assert user.no_login is True - def test_runtime_network_realization_parses_with_kebab_keys(self): + def test_runtime_network_realization_parses_with_canonical_keys(self): sdl = """ name: techvault-network-realization nodes: @@ -593,30 +612,30 @@ def test_runtime_network_realization_parses_with_kebab_keys(self): domainname: techvault.local endpoints: - network: aptl-dmz - network-id: net-a1b2c3d4e5f6 - network-id-stability: stable - endpoint-id: ep-1a2b3c4d5e6f - endpoint-id-stability: ephemeral - backend-generated: true - ip-address: 172.20.0.20 - ip-prefix-length: "24" + network_id: net-a1b2c3d4e5f6 + network_id_stability: stable + endpoint_id: ep-1a2b3c4d5e6f + endpoint_id_stability: ephemeral + backend_generated: true + ip_address: 172.20.0.20 + ip_prefix_length: "24" gateway: 172.20.0.1 - mac-address: 02:42:ac:14:00:14 + mac_address: 02:42:ac:14:00:14 aliases: [aptl-webapp, webapp] - dns-names: [aptl-webapp, webapp] - generated-dns-names: [a1b2c3d4e5f6] + dns_names: [aptl-webapp, webapp] + generated_dns_names: [a1b2c3d4e5f6] backend: driver: bridge - ipam-driver: default - driver-options: + ipam_driver: default + driver_options: com.docker.network.bridge.name: br-dmz - ipam-options: + ipam_options: com.docker.network.driver.mtu: "1500" - published-ports: - - container-port: "8080" + published_ports: + - container_port: "8080" protocol: tcp - host-ip: 127.0.0.1 - host-port: "8080" + host_ip: 127.0.0.1 + host_port: "8080" infrastructure: aptl-dmz: properties: @@ -641,7 +660,7 @@ def test_runtime_network_realization_parses_with_kebab_keys(self): assert endpoint.aliases == ["aptl-webapp", "webapp"] assert endpoint.dns_names == ["aptl-webapp", "webapp"] assert endpoint.generated_dns_names == ["a1b2c3d4e5f6"] - # Backend-native option keys are preserved verbatim (not key-normalized). + # Backend-native option keys are preserved verbatim as literal-map data. assert endpoint.backend.driver == "bridge" assert endpoint.backend.driver_options == {"com.docker.network.bridge.name": "br-dmz"} assert endpoint.backend.ipam_options == {"com.docker.network.driver.mtu": "1500"} @@ -668,7 +687,7 @@ def test_runtime_network_ip_variable_substitutes_on_instantiation(self): network: endpoints: - network: aptl-dmz - ip-address: ${webapp_ip} + ip_address: ${webapp_ip} infrastructure: aptl-dmz: properties: @@ -681,7 +700,7 @@ def test_runtime_network_ip_variable_substitutes_on_instantiation(self): endpoint = instantiated.nodes["techvault-webapp"].runtime.network.endpoints[0] assert endpoint.ip_address == "172.20.0.20" - def test_source_build_provenance_parses_with_kebab_keys(self): + def test_source_build_provenance_parses_with_canonical_keys(self): sdl = """ name: techvault-build-provenance nodes: @@ -692,9 +711,9 @@ def test_source_build_provenance_parses_with_kebab_keys(self): name: techvault-webapp version: local build: - base-image: python:3.12-slim - base-image-digest: sha256:deadbeef - dockerfile-path: containers/webapp/Dockerfile + base_image: python:3.12-slim + base_image_digest: sha256:deadbeef + dockerfile_path: containers/webapp/Dockerfile instructions: - instruction: from arguments: [python:3.12-slim] @@ -702,40 +721,40 @@ def test_source_build_provenance_parses_with_kebab_keys(self): arguments: [webapp/app.py, /app/app.py] layers: - digest: sha256:layer1 - created-by: FROM python:3.12-slim + created_by: FROM python:3.12-slim size: "31000000" - - created-by: ENV APP_HOME=/app + - created_by: ENV APP_HOME=/app empty: true - build-args: + build_args: - name: APP_VERSION value: 1.4.2 - value-classification: plain + value_classification: plain - name: PIP_INDEX_TOKEN - value-classification: redacted - copied-sources: - - source-path: webapp/app.py - destination-path: /app/app.py + value_classification: redacted + copied_sources: + - source_path: webapp/app.py + destination_path: /app/app.py config: entrypoint: [/entrypoint.sh] command: [gunicorn, app:app] - working-directory: /app - exposed-ports: [8080/tcp] + working_directory: /app + exposed_ports: [8080/tcp] labels: org.opencontainers.image.source: https://example.test/techvault com.Example.Tier: webapp - default-environment: + default_environment: - name: APP_HOME value: /app - source-inputs: + source_inputs: - identifier: webapp-app - source-path: webapp/app.py - destination-path: /app/app.py + source_path: webapp/app.py + destination_path: /app/app.py checksum: 4f8c2d - checksum-algorithm: sha256 + checksum_algorithm: sha256 attestation: status: absent verification: not-applicable - attestation-type: in-toto + attestation_type: in-toto """ s = parse_sdl(sdl, skip_semantic_validation=True) build = s.nodes["techvault-webapp"].source.build @@ -884,8 +903,8 @@ def test_ocr_duration_units_parse(self): phase-1: {} scripts: main: - start-time: 1 us - end-time: 1 mon + start_time: 1 us + end_time: 1 mon speed: 1 events: phase-1: 1 ms @@ -945,12 +964,20 @@ def test_leaf_enum_placeholders_parse(self): release-ready: command: /bin/check interval: 30 +propositions: + release-ready: + 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} +assertions: + release-ready: {proposition: release-ready, role: postcondition, polarity: positive} objectives: review: entity: blue-team success: mode: ${success_mode} - conditions: [release-ready] + assertions: [release-ready] """ s = parse_sdl(sdl) assert s.nodes["vm"].os == "${host_os}" @@ -966,8 +993,8 @@ def test_negative_numeric_duration_rejected(self): phase-1: {} scripts: main: - start-time: -5 - end-time: 10 + start_time: -5 + end_time: 10 speed: 1 events: phase-1: 1 @@ -975,8 +1002,10 @@ def test_negative_numeric_duration_rejected(self): exercise: scripts: [main] """ - with pytest.raises(SDLParseError, match="Invalid duration"): + with pytest.raises(SDLParseError) as caught: parse_sdl(sdl) + assert caught.value.diagnostics[0].code == "sdl.model.invalid" + assert caught.value.diagnostics[0].pointer == "/scripts/main/start_time" class TestFormat: @@ -995,8 +1024,10 @@ def test_switch_rejects_vm_only_fields(self): - port: 80 name: http """ - with pytest.raises(SDLParseError, match="Switch nodes cannot have VM-only fields"): + with pytest.raises(SDLParseError) as caught: parse_sdl(sdl) + assert caught.value.diagnostics[0].code == "sdl.model.invalid" + assert caught.value.diagnostics[0].pointer == "/nodes/sw" @pytest.mark.parametrize( "field_name", @@ -1053,8 +1084,10 @@ def test_discriminant_enums_reject_placeholders(self, field_name): default: hello """, } - with pytest.raises(SDLParseError, match=rf"{field_name}[\s\S]*Input should be"): + with pytest.raises(SDLParseError) as caught: parse_sdl(sdl_by_field[field_name], skip_semantic_validation=True) + assert caught.value.diagnostics[0].code == "sdl.model.invalid" + assert caught.value.diagnostics[0].pointer == "/" + field_name.replace(".", "/") @pytest.mark.parametrize( ("sdl", "message"), @@ -1089,8 +1122,15 @@ def test_discriminant_enums_reject_placeholders(self, field_name): ], ) def test_extension_sections_reject_missing_anchor_fields(self, sdl, message): - with pytest.raises(SDLParseError, match=message): + with pytest.raises(SDLParseError) as caught: parse_sdl(sdl) + assert caught.value.diagnostics[0].code == "sdl.model.invalid" + assert caught.value.diagnostics[0].pointer.startswith( + {"Content requires 'target'": "/content/c1", "Account requires 'node'": "/accounts/a1"}.get( + message, + "/agents/red-agent", + ) + ) class TestErrorHandling: @@ -1117,7 +1157,7 @@ def test_structural_only(self): s = parse_sdl( "name: test\nentities:\n blue:\n role: blue\n" "objectives:\n obj:\n entity: blue\n success:\n" - " conditions:\n - missing-condition", + " assertions:\n - missing-assertion", skip_semantic_validation=True, ) assert "obj" in s.objectives @@ -1131,6 +1171,7 @@ def test_parse_sdl_rejects_imports_without_file_context(self): name: root imports: - path: common.yaml + namespace: common """ ) @@ -1140,6 +1181,17 @@ def test_parse_sdl_file_expands_namespaced_imports(self, tmp_path: Path): """ name: common version: 1.2.0 +module: + id: aces/common + version: 1.2.0 + exports: + nodes: [vm] + conditions: [health] + propositions: [health] + assertions: [health] + entities: [blue] + objectives: [validate] + workflows: [response] nodes: vm: type: vm @@ -1153,6 +1205,14 @@ def test_parse_sdl_file_expands_namespaced_imports(self, tmp_path: Path): health: command: /bin/true interval: 15 +propositions: + health: + 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} +assertions: + health: {proposition: health, role: postcondition, polarity: positive} entities: blue: role: blue @@ -1160,7 +1220,7 @@ def test_parse_sdl_file_expands_namespaced_imports(self, tmp_path: Path): validate: entity: blue success: - conditions: [health] + assertions: [health] workflows: response: start: run @@ -1168,7 +1228,7 @@ def test_parse_sdl_file_expands_namespaced_imports(self, tmp_path: Path): run: type: objective objective: validate - on-success: finish + on_success: finish finish: type: end """, @@ -1207,6 +1267,19 @@ def test_parse_sdl_file_namespaces_named_qualified_refs(self, tmp_path: Path): """ name: common version: 1.2.0 +module: + id: aces/common + version: 1.2.0 + exports: + nodes: [vm, net] + infrastructure: [vm, net] + entities: [blue] + conditions: [health] + propositions: [health] + assertions: [health] + content: [docs] + relationships: [blue-controls-vm] + agents: [blue-agent] nodes: vm: type: vm @@ -1236,6 +1309,14 @@ def test_parse_sdl_file_namespaces_named_qualified_refs(self, tmp_path: Path): health: command: /bin/true interval: 15 +propositions: + health: + 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} +assertions: + health: {proposition: health, role: precondition, polarity: positive} content: docs: type: dataset @@ -1250,7 +1331,7 @@ def test_parse_sdl_file_namespaces_named_qualified_refs(self, tmp_path: Path): agents: blue-agent: entity: blue - starting_conditions: [conditions.health, health] + starting_assertions: [health] authority_anchors: [entities.blue, content.docs.items.playbook] allowed_subnets: [net] operating_scope: [nodes.vm, infrastructure.net, nodes.vm.services.ssh] @@ -1272,10 +1353,7 @@ def test_parse_sdl_file_namespaces_named_qualified_refs(self, tmp_path: Path): scenario = parse_sdl_file(root) agent = scenario.agents["shared.blue-agent"] - # ADR-020 §6 accepts both bare (`health`) and qualified - # (`conditions.health`) references in starting_conditions; both - # must survive composition with the namespace baked in. - assert agent.starting_conditions == ["conditions.shared.health", "shared.health"] + assert agent.starting_assertions == ["shared.health"] assert agent.authority_anchors == ["entities.shared.blue", "content.shared.docs.items.playbook"] assert agent.operating_scope == [ "nodes.shared.vm", @@ -1287,10 +1365,10 @@ def test_parse_sdl_file_namespaces_named_qualified_refs(self, tmp_path: Path): assert rel.target == "nodes.shared.vm" def test_parse_sdl_file_namespaces_agent_participant_framing_fields(self, tmp_path: Path): - # ACT-601 / ADR-020: Agent.starting_conditions, .authority_anchors, and + # ACT-601 / ADR-020: Agent.starting_assertions, .authority_anchors, and # .operating_scope are semantic references and must be rewritten by the # module composition pass when their imported targets are namespaced; - # otherwise an `agent.starting_conditions: [health]` from a root + # otherwise an `agent.starting_assertions: [health]` from a root # scenario silently breaks (or, worse, accidentally binds to a same- # named root element) once the imported `conditions: health` becomes # `shared.health`. @@ -1299,6 +1377,18 @@ def test_parse_sdl_file_namespaces_agent_participant_framing_fields(self, tmp_pa """ name: common version: 1.2.0 +module: + id: aces/common + version: 1.2.0 + exports: + nodes: [vm, net] + infrastructure: [vm, net] + entities: [blue] + conditions: [health] + propositions: [health] + assertions: [health] + relationships: [blue-controls-vm] + agents: [blue-agent] nodes: vm: type: vm @@ -1325,6 +1415,14 @@ def test_parse_sdl_file_namespaces_agent_participant_framing_fields(self, tmp_pa health: command: /bin/true interval: 15 +propositions: + health: + 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} +assertions: + health: {proposition: health, role: precondition, polarity: positive} relationships: blue-controls-vm: type: manages @@ -1333,7 +1431,7 @@ def test_parse_sdl_file_namespaces_agent_participant_framing_fields(self, tmp_pa agents: blue-agent: entity: blue - starting_conditions: [health] + starting_assertions: [health] authority_anchors: [blue, blue-controls-vm] allowed_subnets: [net] operating_scope: [vm, net] @@ -1355,7 +1453,7 @@ def test_parse_sdl_file_namespaces_agent_participant_framing_fields(self, tmp_pa scenario = parse_sdl_file(root) agent = scenario.agents["shared.blue-agent"] - assert agent.starting_conditions == ["shared.health"] + assert agent.starting_assertions == ["shared.health"] assert agent.authority_anchors == ["shared.blue", "shared.blue-controls-vm"] assert agent.operating_scope == ["shared.vm", "shared.net"] # Semantic validation has already run as part of parse_sdl_file; if the @@ -1368,6 +1466,11 @@ def test_parse_sdl_file_rejects_version_mismatch(self, tmp_path: Path): """ name: common version: 2.0.0 +module: + id: aces/common + version: 2.0.0 + exports: + nodes: [sw] nodes: sw: type: switch @@ -1380,6 +1483,7 @@ def test_parse_sdl_file_rejects_version_mismatch(self, tmp_path: Path): name: root imports: - path: common.yaml + namespace: common version: 1.0.0 """, encoding="utf-8", @@ -1393,6 +1497,12 @@ def test_parse_sdl_file_rejects_namespace_collisions(self, tmp_path: Path): first.write_text( """ name: shared +version: 1.0.0 +module: + id: aces/first + version: 1.0.0 + exports: + nodes: [vm] nodes: vm: type: vm @@ -1405,6 +1515,12 @@ def test_parse_sdl_file_rejects_namespace_collisions(self, tmp_path: Path): second.write_text( """ name: shared +version: 1.0.0 +module: + id: aces/second + version: 1.0.0 + exports: + nodes: [vm] nodes: vm: type: vm @@ -1442,6 +1558,12 @@ def test_parse_sdl_file_rewrites_database_and_application_relationship_refs(self """ name: shared-db version: 1.0.0 +module: + id: aces/shared-db + version: 1.0.0 + exports: + nodes: [db, web] + relationships: [webapp-to-db] nodes: db: type: vm @@ -1522,7 +1644,7 @@ def test_all_scenarios_parse(self, scenarios_dir): class TestRuntimeApplicationParsing: - def test_runtime_application_surface_parses_with_kebab_keys(self): + def test_runtime_application_surface_parses_with_canonical_keys(self): sdl = """ name: techvault-application-surface nodes: @@ -1534,27 +1656,27 @@ def test_runtime_application_surface_parses_with_kebab_keys(self): name: techvault-http runtime: applications: - - application-id: techvault-webapp + - application_id: techvault-webapp service: techvault-http protocol: http - base-path: / + base_path: / framework: flask routes: - - route-id: login + - route_id: login path: /login methods: [get, post] - auth-required: false - session-required: false + auth_required: false + session_required: false parameters: - name: username location: form required: true responses: - - status-code: "200" - content-type: text/html + - status_code: "200" + content_type: text/html redirects: - target: /dashboard - status-code: "302" + status_code: "302" """ scenario = parse_sdl(sdl) applications = scenario.nodes["techvault-webapp"].runtime.applications @@ -1584,12 +1706,12 @@ def test_runtime_application_auth_variable_substitutes_on_instantiation(self): os: linux runtime: applications: - - application-id: techvault-webapp + - application_id: techvault-webapp routes: - - route-id: login + - route_id: login path: /login methods: [GET] - auth-required: ${login_auth} + auth_required: ${login_auth} """ raw = parse_sdl(sdl) route = raw.nodes["techvault-webapp"].runtime.applications[0].routes[0] @@ -1600,7 +1722,7 @@ def test_runtime_application_auth_variable_substitutes_on_instantiation(self): class TestRuntimeSshServerParsing: - def test_ssh_server_configuration_parses_with_kebab_keys(self): + def test_ssh_server_configuration_parses_with_canonical_keys(self): sdl = """ name: techvault-ssh-surface nodes: @@ -1611,26 +1733,26 @@ def test_ssh_server_configuration_parses_with_kebab_keys(self): - port: 22 name: ssh runtime: - ssh-servers: - - ssh-server-id: sshd-default + ssh_servers: + - ssh_server_id: sshd-default service: ssh - accept-env: [APTL_SESSION_ID, APTL_RUN_ID, APTL_TRACE_ID] - password-authentication: false - pubkey-authentication: true - permit-tty: true - allow-users: [kali] - authentication-methods: [publickey] - chroot-directory: /var/empty - authorized-keys-file: /etc/ssh/authorized_keys.d/%u - match-rules: - - match-id: m-kali + accept_env: [APTL_SESSION_ID, APTL_RUN_ID, APTL_TRACE_ID] + password_authentication: false + pubkey_authentication: true + permit_tty: true + allow_users: [kali] + authentication_methods: [publickey] + chroot_directory: /var/empty + authorized_keys_file: /etc/ssh/authorized_keys.d/%u + match_rules: + - match_id: m-kali criteria: - kind: user pattern: kali - forced-command: - command-kind: absolute_path + forced_command: + command_kind: absolute_path command: /usr/local/bin/aptl-wrap-shell.sh - permit-tty: true + permit_tty: true """ scenario = parse_sdl(sdl) ssh_servers = scenario.nodes["techvault-kali"].runtime.ssh_servers @@ -1665,10 +1787,10 @@ def test_ssh_server_accept_env_scalar_coerces_to_list(self): - port: 22 name: ssh runtime: - ssh-servers: - - ssh-server-id: sshd-default + ssh_servers: + - ssh_server_id: sshd-default service: ssh - accept-env: APTL_SESSION_ID + accept_env: APTL_SESSION_ID """ scenario = parse_sdl(sdl) server = scenario.nodes["techvault-kali"].runtime.ssh_servers[0] @@ -1689,10 +1811,10 @@ def test_ssh_server_chroot_directory_variable_substitutes_on_instantiation(self) - port: 22 name: ssh runtime: - ssh-servers: - - ssh-server-id: sshd-default + ssh_servers: + - ssh_server_id: sshd-default service: ssh - chroot-directory: ${chroot_path} + chroot_directory: ${chroot_path} """ raw = parse_sdl(sdl) server = raw.nodes["techvault-kali"].runtime.ssh_servers[0] @@ -1718,8 +1840,8 @@ def test_ssh_server_variable_ref_server_id_rejected_on_instantiation(self): - port: 22 name: ssh runtime: - ssh-servers: - - ssh-server-id: ${server_id} + ssh_servers: + - ssh_server_id: ${server_id} service: ssh """ # The parse step itself should reject a variable-ref symbol-defining identifier @@ -1729,7 +1851,7 @@ def test_ssh_server_variable_ref_server_id_rejected_on_instantiation(self): class TestRuntimeIdentityAuthorityParsing: - def test_identity_authority_surface_parses_with_kebab_keys(self): + def test_identity_authority_surface_parses_with_canonical_keys(self): sdl = """ name: techvault-directory-identity nodes: @@ -1740,47 +1862,47 @@ def test_identity_authority_surface_parses_with_kebab_keys(self): - {port: 389, name: ldap} - {port: 88, name: kerberos} runtime: - identity-authorities: - - identity-authority-id: techvault-domain + identity_authorities: + - identity_authority_id: techvault-domain kind: domain name: TechVault Domain namespace: techvault.local - domain-name: TECHVAULT + domain_name: TECHVAULT realm: TECHVAULT.LOCAL - base-dn: DC=techvault,DC=local + base_dn: DC=techvault,DC=local services: - - service-id: ldap-endpoint + - service_id: ldap-endpoint service: ldap protocol: LDAP address: dc.techvault.local port: "389" subjects: - - subject-id: alice + - subject_id: alice kind: user name: alice - principal-name: alice@TECHVAULT.LOCAL - distinguished-name: CN=Alice,CN=Users,DC=techvault,DC=local + principal_name: alice@TECHVAULT.LOCAL + distinguished_name: CN=Alice,CN=Users,DC=techvault,DC=local enabled: true attributes: - name: department values: security - - subject-id: domain-admins + - subject_id: domain-admins kind: group name: Domain Admins - - subject-id: ldap-svc + - subject_id: ldap-svc kind: service-principal name: ldap - service-principal-names: [LDAP/dc.techvault.local] + service_principal_names: [LDAP/dc.techvault.local] relationships: - - relationship-id: alice-admin - relationship-type: member-of - source-ref: alice - target-ref: domain-admins + - relationship_id: alice-admin + relationship_type: member-of + source_ref: alice + target_ref: domain-admins policies: - - policy-id: default-password-policy - policy-kind: password + - policy_id: default-password-policy + policy_kind: password name: Default Domain Policy - applies-to-refs: [techvault-domain] + applies_to_refs: [techvault-domain] settings: - name: min_length values: "14" @@ -1809,11 +1931,11 @@ def test_identity_authority_value_fields_substitute_on_instantiation(self): type: vm os: linux runtime: - identity-authorities: - - identity-authority-id: techvault-idp + identity_authorities: + - identity_authority_id: techvault-idp kind: identity-provider namespace: https://idp.techvault.local - tenant-id: ${tenant_id} + tenant_id: ${tenant_id} """ raw = parse_sdl(sdl) authority = raw.nodes["idp"].runtime.identity_authorities[0] @@ -1832,10 +1954,10 @@ def test_identity_authority_service_ref_must_resolve_to_same_node_service(self): services: - {port: 389, name: ldap} runtime: - identity-authorities: - - identity-authority-id: techvault-domain + identity_authorities: + - identity_authority_id: techvault-domain services: - - service-id: ldap-endpoint + - service_id: ldap-endpoint service: missing-ldap protocol: ldap """ @@ -1850,15 +1972,15 @@ def test_identity_authority_relationship_refs_must_resolve_inside_authority(self type: vm os: windows runtime: - identity-authorities: - - identity-authority-id: techvault-domain + identity_authorities: + - identity_authority_id: techvault-domain subjects: - - {subject-id: domain-admins, kind: group, name: Domain Admins} + - {subject_id: domain-admins, kind: group, name: Domain Admins} relationships: - - relationship-id: alice-admin - relationship-type: member-of - source-ref: alice - target-ref: domain-admins + - relationship_id: alice-admin + relationship_type: member-of + source_ref: alice + target_ref: domain-admins """ with pytest.raises(SDLValidationError, match="source_ref 'alice' does not resolve"): parse_sdl(sdl) @@ -1871,11 +1993,11 @@ def test_identity_authority_policy_applies_to_ref_must_resolve_inside_authority( type: vm os: windows runtime: - identity-authorities: - - identity-authority-id: techvault-domain + identity_authorities: + - identity_authority_id: techvault-domain policies: - - policy-id: default-policy - applies-to-refs: [missing-subject] + - policy_id: default-policy + applies_to_refs: [missing-subject] """ with pytest.raises(SDLValidationError, match="applies_to_ref 'missing-subject' does not resolve"): parse_sdl(sdl) @@ -1890,28 +2012,28 @@ def test_identity_authority_local_refs_include_stable_service_and_relationship_i services: - {port: 389, name: ldap} runtime: - identity-authorities: - - identity-authority-id: techvault-domain + identity_authorities: + - identity_authority_id: techvault-domain services: - - service-id: ldap-endpoint + - service_id: ldap-endpoint service: ldap protocol: ldap subjects: - - {subject-id: alice, kind: user, name: alice} - - {subject-id: domain-admins, kind: group, name: Domain Admins} + - {subject_id: alice, kind: user, name: alice} + - {subject_id: domain-admins, kind: group, name: Domain Admins} relationships: - - relationship-id: alice-admin - relationship-type: member-of - source-ref: alice - target-ref: domain-admins - - relationship-id: ldap-documents-membership - relationship-type: associated - source-ref: ldap-endpoint - target-ref: alice-admin + - relationship_id: alice-admin + relationship_type: member-of + source_ref: alice + target_ref: domain-admins + - relationship_id: ldap-documents-membership + relationship_type: associated + source_ref: ldap-endpoint + target_ref: alice-admin policies: - - policy-id: ldap-audit-policy - policy-kind: other - applies-to-refs: [ldap-endpoint, alice-admin] + - policy_id: ldap-audit-policy + policy_kind: other + applies_to_refs: [ldap-endpoint, alice-admin] """ authority = parse_sdl(sdl).nodes["ad"].runtime.identity_authorities[0] @@ -1947,26 +2069,28 @@ def test_identity_authority_local_ref_ids_must_be_unique_across_id_families(self type: vm os: windows runtime: - identity-authorities: - - identity-authority-id: {authority_id} + identity_authorities: + - identity_authority_id: {authority_id} services: - - service-id: {service_id} + - service_id: {service_id} protocol: ldap subjects: - - subject-id: {subject_id} + - subject_id: {subject_id} kind: user name: alice relationships: - - relationship-id: {relationship_id} - relationship-type: associated - source-ref: {subject_id} - external-target: external.example + - relationship_id: {relationship_id} + relationship_type: associated + source_ref: {subject_id} + external_target: external.example policies: - - policy-id: {policy_id} - applies-to-refs: [{subject_id}] + - policy_id: {policy_id} + applies_to_refs: [{subject_id}] """ - with pytest.raises(SDLParseError, match="Duplicate runtime identity stable id 'shared'"): + with pytest.raises(SDLParseError) as caught: parse_sdl(sdl) + assert caught.value.diagnostics[0].code == "sdl.model.invalid" + assert caught.value.diagnostics[0].pointer == "/nodes/ad/runtime/identity_authorities/0" def test_imported_identity_authority_refs_survive_module_namespacing(self, tmp_path): imported = tmp_path / "shared-directory.yaml" @@ -1974,26 +2098,32 @@ def test_imported_identity_authority_refs_survive_module_namespacing(self, tmp_p """ name: shared-directory version: 1.0.0 +module: + id: aces/shared-directory + version: 1.0.0 + exports: + nodes: [ad] + relationships: [alice-admin, ldap-policy, membership-policy] nodes: ad: type: vm os: windows runtime: - identity-authorities: - - identity-authority-id: techvault-domain + identity_authorities: + - identity_authority_id: techvault-domain services: - - {service-id: ldap-endpoint, protocol: ldap} + - {service_id: ldap-endpoint, protocol: ldap} subjects: - - {subject-id: alice, kind: user, name: alice} - - {subject-id: domain-admins, kind: group, name: Domain Admins} + - {subject_id: alice, kind: user, name: alice} + - {subject_id: domain-admins, kind: group, name: Domain Admins} policies: - - policy-id: default-policy - applies-to-refs: [techvault-domain] + - policy_id: default-policy + applies_to_refs: [techvault-domain] relationships: - - relationship-id: alice-admin - relationship-type: member-of - source-ref: alice - target-ref: domain-admins + - relationship_id: alice-admin + relationship_type: member-of + source_ref: alice + target_ref: domain-admins relationships: alice-admin: type: trusts @@ -2036,7 +2166,7 @@ def test_imported_identity_authority_refs_survive_module_namespacing(self, tmp_p class TestRuntimeDnsParsing: - def test_dns_service_field_keys_normalized_names_preserved(self): + def test_dns_service_canonical_field_keys_preserve_names(self): sdl = """ name: techvault-dns nodes: @@ -2046,25 +2176,25 @@ def test_dns_service_field_keys_normalized_names_preserved(self): services: - {port: 53, protocol: udp, name: dns} runtime: - dns-services: - - dns-service-id: tv-dns + dns_services: + - dns_service_id: tv-dns service: dns implementation: BIND roles: [authoritative, recursive-resolver] - resolver-policy: - recursion-enabled: true - dnssec-validation: auto + resolver_policy: + recursion_enabled: true + dnssec_validation: auto forwarders: - {address: 8.8.8.8, port: 53} zones: - - zone-id: techvault-local + - zone_id: techvault-local name: TechVault.Local. - zone-class: IN + zone_class: IN purpose: forward rrsets: - - rrset-id: web-a + - rrset_id: web-a owner: Web.TechVault.Local. - record-type: A + record_type: A ttl: 300 records: - {address: 172.20.10.20} @@ -2087,6 +2217,12 @@ def test_dns_runtime_refs_rewrite_on_module_import(self, tmp_path): """ name: shared-dns version: 1.0.0 +module: + id: aces/shared-dns + version: 1.0.0 + exports: + nodes: [dns] + relationships: [dns-record] nodes: dns: type: vm @@ -2094,16 +2230,16 @@ def test_dns_runtime_refs_rewrite_on_module_import(self, tmp_path): services: - {port: 53, protocol: udp, name: dns} runtime: - dns-services: - - dns-service-id: tv-dns + dns_services: + - dns_service_id: tv-dns service: dns zones: - - zone-id: techvault-local + - zone_id: techvault-local name: techvault.local. rrsets: - - rrset-id: web-a + - rrset_id: web-a owner: web.techvault.local. - record-type: a + record_type: a ttl: 300 records: - {address: 172.20.10.20} @@ -2135,9 +2271,9 @@ def test_dns_runtime_refs_rewrite_on_module_import(self, tmp_path): class TestRuntimeDatabaseParsing: - def test_database_service_field_keys_normalized_names_preserved(self): - # Field keys (hyphenated/cased) normalize; observed object names are - # data and survive verbatim — including mixed case and underscores. + def test_database_service_canonical_field_keys_preserve_names(self): + # Structural fields are canonical; observed object names are data and + # survive verbatim, including mixed case and underscores. sdl = """ name: techvault-db nodes: @@ -2147,19 +2283,19 @@ def test_database_service_field_keys_normalized_names_preserved(self): services: - {port: 5432, name: pg} runtime: - database-services: - - database-service-id: tv-pg + database_services: + - database_service_id: tv-pg service: pg engine: PostgreSQL protocol: postgresql databases: - - database-id: tv-db + - database_id: tv-db name: TechVault_Prod schemas: - - schema-id: pub + - schema_id: pub name: public tables: - - {table-id: audit, name: Audit_Log} + - {table_id: audit, name: Audit_Log} settings: - {name: log_statement, value: all, provenance: configuration-file} """ @@ -2187,8 +2323,8 @@ def test_database_service_variable_substitutes_on_instantiation(self): services: - {port: 5432, name: pg} runtime: - database-services: - - database-service-id: tv-pg + database_services: + - database_service_id: tv-pg service: pg engine: postgresql protocol: postgresql @@ -2212,8 +2348,8 @@ def test_removed_top_level_scoring_sections_rejected(self, section): with pytest.raises(SDLParseError, match="were removed from the language by ADR-073"): parse_sdl(sdl) - def test_migration_message_points_to_success_conditions(self): - with pytest.raises(SDLParseError, match="objectives.\\*.success.conditions"): + def test_migration_message_points_to_success_assertions(self): + with pytest.raises(SDLParseError, match="objectives.\\*.success.assertions"): parse_sdl("name: test\ngoals:\n g1:\n tlos: [t1]\n") def test_agent_reward_calculator_rejected(self): @@ -2257,7 +2393,7 @@ def test_objective_success_removed_fields_rejected(self, field): with pytest.raises(SDLParseError): parse_sdl(sdl) - def test_objective_success_accepts_conditions_only(self): + def test_objective_success_accepts_assertions_only(self): sdl = """ name: test entities: @@ -2267,11 +2403,19 @@ def test_objective_success_accepts_conditions_only(self): release-ready: command: /bin/check interval: 30 +propositions: + release-ready: + 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} +assertions: + release-ready: {proposition: release-ready, role: postcondition, polarity: positive} objectives: obj: entity: blue-team success: - conditions: [release-ready] + assertions: [release-ready] """ s = parse_sdl(sdl) - assert s.objectives["obj"].success.conditions == ["release-ready"] + assert s.objectives["obj"].success.assertions == ["release-ready"] diff --git a/implementations/python/tests/test_sdl_phase_contracts.py b/implementations/python/tests/test_sdl_phase_contracts.py new file mode 100644 index 000000000..98c6c932a --- /dev/null +++ b/implementations/python/tests/test_sdl_phase_contracts.py @@ -0,0 +1,458 @@ +"""Issue #724: closed SDL phase types and portable derivation provenance.""" + +from __future__ import annotations + +import inspect +import json + +import aces_sdl +import pytest +from aces_contracts.contracts import schema_bundle +from aces_processor.compiler import compile_runtime_model +from aces_sdl import SDLInstantiationError, SDLValidationError +from aces_sdl.canonical import ( + INSTANTIATED_SNAPSHOT_PROFILE, + canonical_instantiated_sdl_bytes, + canonical_instantiated_sdl_digest, +) +from aces_sdl.instantiate import ( + _bind_scenario_content, + admit_instantiated_scenario, + instantiate_scenario, +) +from aces_sdl.parser import parse_sdl_file +from aces_sdl.phase_contracts import ( + BindingOrigin, + CapabilityConstraint, + InstantiationProvenance, + ParameterBinding, + ResolvedImportProvenance, + SemanticDigest, +) +from aces_sdl.scenario import ExpandedScenario, InstantiatedScenario, Scenario, ScenarioContent +from jsonschema import Draft202012Validator +from pydantic import ValidationError + + +def _digest() -> SemanticDigest: + return SemanticDigest( + profile="aces-sdl-semantic/v1", + algorithm="sha256", + value="sha256:" + "a" * 64, + ) + + +def _provenance( + *, + bindings: tuple[ParameterBinding, ...] = (), + constraints: tuple[CapabilityConstraint, ...] = (), +) -> InstantiationProvenance: + return InstantiationProvenance( + authored_digest=_digest(), + bindings=bindings, + capability_constraints=constraints, + ) + + +def test_phase_models_have_disjoint_authoring_and_instantiated_fields() -> None: + shared = set(ScenarioContent.model_fields) + assert not issubclass(InstantiatedScenario, Scenario) + assert set(Scenario.model_fields) == shared | {"module", "imports", "variables"} + assert set(ExpandedScenario.model_fields) == shared | {"variables", "expansion_provenance"} + assert set(InstantiatedScenario.model_fields) == shared | {"instantiation_provenance"} + assert "ExpandedScenario" not in aces_sdl.__all__ + + +@pytest.mark.parametrize( + ("field", "value"), + (("variables", {}), ("imports", []), ("module", None)), +) +def test_instantiated_model_forbids_authoring_machinery_even_when_empty(field: str, value: object) -> None: + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + InstantiatedScenario.model_validate( + { + "name": "concrete", + "instantiation_provenance": _provenance().model_dump(mode="json"), + field: value, + } + ) + + +def test_instantiated_model_requires_provenance() -> None: + with pytest.raises(ValidationError, match="instantiation_provenance"): + InstantiatedScenario.model_validate({"name": "concrete"}) + + +def test_instantiation_serializes_binding_origin_without_live_variables() -> None: + scenario = Scenario.model_validate( + { + "name": "parameterized", + "variables": { + "image": { + "type": "string", + "default": "linux", + "allowed_values": ["linux"], + } + }, + "nodes": { + "host": { + "type": "vm", + "os": "${image}", + "resources": {"ram": "1 gib", "cpu": 1}, + } + }, + "infrastructure": {"host": {"count": 1}}, + } + ) + scenario._set_semantic_validated(True) + + concrete = instantiate_scenario(scenario) + payload = concrete.model_dump(mode="json") + + assert {"module", "imports", "variables"}.isdisjoint(payload) + assert payload["nodes"]["host"]["os"] == "linux" + assert payload["instantiation_provenance"]["bindings"] == [ + {"parameter": ["image"], "origin": "default", "value": "linux"} + ] + assert concrete.instantiation_provenance.root_binding_values == {"image": "linux"} + assert concrete.instantiation_provenance.bindings[0].origin is BindingOrigin.DEFAULT + assert not hasattr(concrete, "variables") + assert not hasattr(concrete, "imports") + assert not hasattr(concrete, "module") + + +def test_instantiated_artifact_round_trips_without_private_state() -> None: + binding = ParameterBinding(parameter=("region",), origin=BindingOrigin.PROVIDED, value="eu-central") + first = InstantiatedScenario( + name="round-trip", + description="eu-central", + instantiation_provenance=_provenance(bindings=(binding,)), + ) + + restored = InstantiatedScenario.model_validate_json(first.model_dump_json()) + + assert restored == first + assert restored.instantiation_provenance.root_binding_values == {"region": "eu-central"} + assert restored.model_dump(mode="json") == first.model_dump(mode="json") + + +def test_instantiated_snapshot_has_distinct_deterministic_profile() -> None: + artifact = InstantiatedScenario( + name="snapshot", + instantiation_provenance=_provenance(), + ) + + first = canonical_instantiated_sdl_bytes(artifact) + second = canonical_instantiated_sdl_bytes(InstantiatedScenario.model_validate_json(artifact.model_dump_json())) + decoded = json.loads(first) + + assert first == second + assert decoded["profile"] == INSTANTIATED_SNAPSHOT_PROFILE + assert decoded["scenario"]["name"] == "snapshot" + assert decoded["scenario"]["instantiation_provenance"]["authored_digest"]["value"] == "sha256:" + "a" * 64 + digest = canonical_instantiated_sdl_digest(artifact) + assert digest.profile == INSTANTIATED_SNAPSHOT_PROFILE + assert digest.value.startswith("sha256:") + + +def test_snapshot_schema_is_published_as_a_distinct_contract() -> None: + artifact = InstantiatedScenario.model_validate( + { + "name": "snapshot", + "nodes": { + "shared.host": { + "type": "vm", + "os": "linux", + "resources": {"ram": "1 gib", "cpu": 1}, + } + }, + "instantiation_provenance": _provenance().model_dump(mode="json"), + } + ) + snapshot = json.loads(canonical_instantiated_sdl_bytes(artifact)) + + bundle = schema_bundle() + assert "instantiated-scenario-snapshot-v1" in bundle + Draft202012Validator(bundle["instantiated-scenario-snapshot-v1"]).validate(snapshot) + + +def test_snapshot_digest_changes_when_replay_provenance_changes() -> None: + first = InstantiatedScenario( + name="snapshot", + instantiation_provenance=_provenance( + bindings=(ParameterBinding(parameter=("region",), origin="provided", value="eu-central"),) + ), + ) + second = InstantiatedScenario( + name="snapshot", + instantiation_provenance=_provenance( + bindings=(ParameterBinding(parameter=("region",), origin="provided", value="us-east"),) + ), + ) + + assert canonical_instantiated_sdl_digest(first) != canonical_instantiated_sdl_digest(second) + + +def test_public_instantiation_has_no_semantic_validation_bypass() -> None: + assert "validate_semantics" not in inspect.signature(instantiate_scenario).parameters + bound = _bind_scenario_content(Scenario(name="private-boundary")) + assert not isinstance(bound.content, InstantiatedScenario) + + +def test_compiler_and_snapshot_admit_direct_artifacts_semantically() -> None: + artifact = InstantiatedScenario.model_validate( + { + "name": "invalid-direct-artifact", + "events": {"kickoff": {"injects": ["missing"]}}, + "instantiation_provenance": _provenance().model_dump(mode="json"), + } + ) + + with pytest.raises(SDLValidationError, match="undefined inject"): + compile_runtime_model(artifact) + with pytest.raises(SDLValidationError, match="undefined inject"): + canonical_instantiated_sdl_bytes(artifact) + + +def test_json_round_trip_preserves_compiler_capability_and_explicitness_semantics() -> None: + authored = Scenario.model_validate( + { + "name": "portable-compile", + "variables": { + "image": { + "type": "string", + "default": "linux", + "allowed_values": ["linux", "windows"], + } + }, + "nodes": { + "host": { + "type": "vm", + "os": "${image}", + "resources": {"ram": "1 gib", "cpu": 1}, + } + }, + "infrastructure": {"host": {"count": 1}}, + } + ) + concrete = instantiate_scenario(authored) + restored = InstantiatedScenario.model_validate_json(concrete.model_dump_json()) + + assert concrete.instantiation_provenance.capability_constraints + assert concrete.instantiation_provenance.explicitness + assert compile_runtime_model(restored) == compile_runtime_model(concrete) + compiled = compile_runtime_model(restored) + assert not hasattr(compiled, "variable_specs") + assert not hasattr(compiled, "node_variable_refs") + assert len(compiled.capability_constraints) == 1 + constraint = compiled.capability_constraints[0] + assert constraint.address == "provision.node.host" + assert constraint.concern == "nodes.os" + assert constraint.parameter == ("image",) + assert constraint.allowed_values == ("linux", "windows") + + +def test_capability_constraint_is_limited_to_the_normative_field_surface() -> None: + with pytest.raises(ValidationError, match="nodes.*os.*infrastructure.*count"): + CapabilityConstraint( + field_pointer="/description", + parameter=("value",), + allowed_values=("concrete",), + ) + + +def test_provenance_uses_json_value_equality_not_python_boolean_integer_equality() -> None: + binding = ParameterBinding(parameter=("count",), origin="provided", value=True) + constraint = CapabilityConstraint( + field_pointer="/infrastructure/host/count", + parameter=("count",), + allowed_values=(1,), + ) + + with pytest.raises(ValidationError, match="does not satisfy"): + InstantiationProvenance( + authored_digest=_digest(), + bindings=(binding,), + capability_constraints=(constraint,), + ) + + distinct = CapabilityConstraint( + field_pointer="/infrastructure/host/count", + parameter=("count",), + allowed_values=(True, 1), + ) + assert distinct.allowed_values == (True, 1) + + +def test_provenance_rejects_non_finite_json_numbers() -> None: + with pytest.raises(ValidationError, match="finite"): + ParameterBinding(parameter=("scale",), origin="provided", value=float("nan")) + + +def test_capability_provenance_must_match_the_concrete_field() -> None: + binding = ParameterBinding(parameter=("image",), origin="provided", value="linux") + provenance = _provenance( + bindings=(binding,), + constraints=( + CapabilityConstraint( + field_pointer="/nodes/host/os", + parameter=("image",), + allowed_values=("linux", "windows"), + ), + ), + ) + + with pytest.raises(ValidationError, match="does not match the concrete field"): + InstantiatedScenario.model_validate( + { + "name": "mismatched-evidence", + "nodes": { + "host": { + "type": "vm", + "os": "windows", + "resources": {"ram": "1 gib", "cpu": 1}, + } + }, + "instantiation_provenance": provenance.model_dump(mode="json"), + } + ) + + +def test_binding_failures_do_not_echo_supplied_values_or_allowed_domains() -> None: + marker = "s3cr3t-marker-68f5" + authored = Scenario.model_validate( + { + "name": "bounded-diagnostic", + "description": "${token}", + "variables": { + "token": { + "type": "string", + "allowed_values": ["public-value"], + "required": True, + } + }, + } + ) + + with pytest.raises(SDLInstantiationError) as caught: + instantiate_scenario(authored, {"token": marker}) + diagnostic = str(caught.value) + assert marker not in diagnostic + assert "public-value" not in diagnostic + + +def test_direct_artifact_admission_wraps_structural_errors_without_value_echo() -> None: + marker = "s3cr3t-marker-a41c" + + with pytest.raises(SDLInstantiationError, match="Instantiated artifact is invalid") as caught: + admit_instantiated_scenario( + { + "name": "direct-admission", + "variables": {"token": marker}, + "instantiation_provenance": _provenance().model_dump(mode="json"), + } + ) + assert marker not in str(caught.value) + + +@pytest.mark.parametrize( + ("field", "source"), + ( + ("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"), + ( + "resolved_source", + "user:token@registry.example/aces/module@sha256:" + "b" * 64, + ), + ), +) +def test_import_provenance_rejects_host_paths_and_registry_credentials( + field: str, + source: str, +) -> None: + payload = { + "namespace": ["shared"], + "requested_source": "local:module.yaml", + "module_id": "aces/module", + "module_version": "1.0.0", + "resolved_source": "module.yaml", + field: source, + } + + with pytest.raises(ValidationError, match="absolute host path|registry credentials"): + ResolvedImportProvenance.model_validate(payload) + + +def test_nested_import_provenance_is_ordered_portable_and_replay_complete(tmp_path) -> None: + inner = tmp_path / "inner.yaml" + inner.write_text( + """ +name: inner +version: 1.0.0 +module: + id: aces/inner + version: 1.0.0 + parameters: [image] + exports: {nodes: [host]} +variables: + image: + type: string + required: true + allowed_values: [linux, windows] +nodes: + host: + type: vm + os: '${image}' + resources: {ram: 1 gib, cpu: 1} +""", + encoding="utf-8", + ) + outer = tmp_path / "outer.yaml" + outer.write_text( + """ +name: outer +version: 2.0.0 +module: + id: aces/outer + version: 2.0.0 + parameters: [flavor] + exports: {nodes: [inner.host]} +variables: + flavor: {type: string, default: standard} +imports: + - path: inner.yaml + namespace: inner + version: 1.0.0 + parameters: {image: linux} +""", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + """ +name: root +imports: + - path: outer.yaml + namespace: outer + version: 2.0.0 +""", + encoding="utf-8", + ) + + concrete = instantiate_scenario(parse_sdl_file(root)) + imports = concrete.instantiation_provenance.imports + + assert [record.namespace for record in imports] == [("outer",), ("outer", "inner")] + assert imports[0].bindings[0].parameter == ("flavor",) + assert imports[0].bindings[0].origin is BindingOrigin.DEFAULT + assert imports[1].bindings[0].parameter == ("image",) + assert imports[1].bindings[0].origin is BindingOrigin.PROVIDED + assert imports[0].resolved_source == "outer.yaml" + assert imports[1].resolved_source == "inner.yaml" + assert all(not record.resolved_source.startswith(str(tmp_path)) for record in imports) + assert all(record.content_digest.startswith("sha256:") for record in imports) + assert all(record.export_hash.startswith("sha256:") for record in imports) + constraint = concrete.instantiation_provenance.capability_constraints[0] + assert constraint.parameter == ("outer", "inner", "image") + assert constraint.field_pointer == "/nodes/outer.inner.host/os" diff --git a/implementations/python/tests/test_sdl_realworld.py b/implementations/python/tests/test_sdl_realworld.py index 2ec75e4d2..231f4e7a7 100644 --- a/implementations/python/tests/test_sdl_realworld.py +++ b/implementations/python/tests/test_sdl_realworld.py @@ -786,20 +786,54 @@ def _parse(yaml_str: str, label: str): conditions: scada-hmi-responsive: + proposition: scada-hmi-responsive command: /usr/bin/check-hmi-availability interval: 60 scada-hmi-disrupted: + proposition: scada-hmi-disrupted command: /usr/bin/check-hmi-disruption interval: 60 +evidence_requirements: + scada-state-evidence: + description: Capture governed HMI state observations. + source_refs: [nodes.hmi-server] + scope_refs: [nodes.hmi-server] + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required + +propositions: + scada-hmi-responsive: + 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} + 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} + evidence_requirements: [scada-state-evidence] + +assertions: + scada-hmi-responsive: {proposition: scada-hmi-responsive, role: postcondition, polarity: positive} + scada-hmi-disrupted: {proposition: scada-hmi-disrupted, role: postcondition, polarity: positive} + events: disruption-wave: {} recovery-phase: {} scripts: locked-shields-day-1: - start-time: 0 - end-time: 8 hour + start_time: 0 + end_time: 8 hour speed: 1 events: disruption-wave: 2 hour @@ -814,7 +848,7 @@ def _parse(yaml_str: str, label: str): entity: crimsonia-red targets: [hmi-controls-power, hmi-controls-water, hmi-server] success: - conditions: [scada-hmi-disrupted] + assertions: [scada-hmi-disrupted] window: stories: [main-exercise] scripts: [locked-shields-day-1] @@ -824,7 +858,7 @@ def _parse(yaml_str: str, label: str): entity: berylia-blue.ot-team targets: [hmi-server, plc-power, plc-water] success: - conditions: [scada-hmi-responsive] + assertions: [scada-hmi-responsive] window: stories: [main-exercise] scripts: [locked-shields-day-1] diff --git a/implementations/python/tests/test_sdl_source_format.py b/implementations/python/tests/test_sdl_source_format.py new file mode 100644 index 000000000..d7435880f --- /dev/null +++ b/implementations/python/tests/test_sdl_source_format.py @@ -0,0 +1,316 @@ +"""Conformance tests for the canonical ``sdl-yaml/v1`` source profile.""" + +from __future__ import annotations + +import textwrap +from pathlib import Path + +import pytest +from aces_sdl import ( + SDL_SOURCE_FORMAT, + SDLMigrationPolicy, + SDLParseError, + SDLParserLimits, + load_sdl_fragment, + parse_sdl, + parse_sdl_file, +) +from paths import REPO_ROOT + + +def _diagnostic_codes(error: SDLParseError) -> set[str]: + return {item.code for item in error.diagnostics} + + +def test_yaml_12_core_scalar_resolution_is_portable() -> None: + payload = load_sdl_fragment( + textwrap.dedent( + """ + yes_value: yes + no_value: NO + on_value: on + off_value: Off + true_value: true + false_value: FALSE + decimal: 012 + octal: 0o12 + hexadecimal: 0x0a + date_like: 2026-07-11 + null_value: null + underscore_integer: 1_000 + signed_hexadecimal: -0x0a + """ + ) + ) + + assert payload == { + "yes_value": "yes", + "no_value": "NO", + "on_value": "on", + "off_value": "Off", + "true_value": True, + "false_value": False, + "decimal": 12, + "octal": 10, + "hexadecimal": 10, + "date_like": "2026-07-11", + "null_value": None, + "underscore_integer": "1_000", + "signed_hexadecimal": "-0x0a", + } + + +@pytest.mark.parametrize( + ("content", "code"), + [ + ("value: !!str yes\n", "sdl.explicit_tag"), + ("%YAML 1.2\n---\nvalue: yes\n", "sdl.directive"), + ("value: .inf\n", "sdl.non_json_value"), + ("value: .nan\n", "sdl.non_json_value"), + ("true: value\n", "sdl.mapping_key_type"), + ], +) +def test_non_profile_yaml_constructs_fail_before_model_construction(content: str, code: str) -> None: + with pytest.raises(SDLParseError) as exc_info: + load_sdl_fragment(content) + + assert code in _diagnostic_codes(exc_info.value) + + +def test_canonical_structural_fields_are_exact_snake_case() -> None: + with pytest.raises(SDLParseError) as exc_info: + parse_sdl("Name: canonical-name\n") + + diagnostic = exc_info.value.diagnostics[0] + assert diagnostic.code == "sdl.noncanonical_field" + assert diagnostic.pointer == "/name" + assert diagnostic.authored_keys == ("Name", "name") + assert diagnostic.severity == "error" + + +def test_migration_policy_accepts_aliases_with_source_ranged_advisories(tmp_path: Path) -> None: + path = tmp_path / "legacy.sdl.yaml" + path.write_text( + textwrap.dedent( + """ + Name: migrated + workflows: + response-flow: + start: finish + steps: + finish: + type: objective + objective: objective-ref + on-success: done + done: + type: end + """ + ), + encoding="utf-8", + ) + + scenario = parse_sdl_file( + path, + migration_policy=SDLMigrationPolicy.ACCEPT, + skip_semantic_validation=True, + ) + + assert scenario.name == "migrated" + assert [item.code for item in scenario.source_diagnostics] == [ + "sdl.noncanonical_field", + "sdl.noncanonical_field", + ] + assert [item.pointer for item in scenario.source_diagnostics] == [ + "/name", + "/workflows/response-flow/steps/finish/on_success", + ] + assert all(item.severity == "warning" for item in scenario.source_diagnostics) + assert all(item.source == str(path) for item in scenario.source_diagnostics) + + +def test_literal_identifiers_are_not_migration_aliases() -> None: + with pytest.raises(SDLParseError) as exc_info: + parse_sdl( + textwrap.dedent( + """ + name: literal-ids + nodes: + Web-App: {type: switch} + web_app: {type: switch} + """ + ), + migration_policy=SDLMigrationPolicy.ACCEPT, + ) + + assert _diagnostic_codes(exc_info.value) == {"sdl.identifier.invalid"} + + +def test_merge_keys_are_migration_only_and_conflicts_remain_fatal() -> None: + content = textwrap.dedent( + """ + name: merge-migration + nodes: + template: &template + type: switch + inherited: + <<: *template + """ + ) + + with pytest.raises(SDLParseError) as strict_exc: + parse_sdl(content) + assert "sdl.noncanonical_merge" in _diagnostic_codes(strict_exc.value) + + scenario = parse_sdl(content, migration_policy=SDLMigrationPolicy.ACCEPT) + assert scenario.nodes["inherited"].type.value == "switch" + assert [item.code for item in scenario.source_diagnostics] == ["sdl.noncanonical_merge"] + + conflicting = content.replace("<<: *template", "<<: *template\n type: vm") + with pytest.raises(SDLParseError) as conflict_exc: + parse_sdl(conflicting, migration_policy=SDLMigrationPolicy.ACCEPT) + assert "sdl.mapping_key_conflict" in _diagnostic_codes(conflict_exc.value) + + +@pytest.mark.parametrize( + ("limits", "content"), + [ + (SDLParserLimits(max_input_bytes=8), "value: too-long\n"), + (SDLParserLimits(max_scalar_bytes=3), "value: four\n"), + (SDLParserLimits(max_depth=2), "value:\n nested:\n leaf: true\n"), + (SDLParserLimits(max_nodes=2), "first: 1\nsecond: 2\n"), + (SDLParserLimits(max_aliases=1), "base: &base [1]\nvalues: [*base, *base]\n"), + ( + SDLParserLimits(max_expanded_nodes=8), + "base: &base [1, 2, 3]\nvalues: [*base, *base, *base]\n", + ), + ], +) +def test_parser_limits_fail_with_one_stable_operational_diagnostic( + limits: SDLParserLimits, + content: str, +) -> None: + with pytest.raises(SDLParseError) as exc_info: + load_sdl_fragment(content, limits=limits) + + assert _diagnostic_codes(exc_info.value) == {"sdl.source_limit"} + + +def test_alias_reuse_is_checked_at_its_effective_expanded_depth() -> None: + content = """\ +base: &base + child: + leaf: true +nested: + inner: *base +""" + + with pytest.raises(SDLParseError) as exc_info: + load_sdl_fragment( + content, + mapping_keys="literal", + limits=SDLParserLimits(max_depth=4), + ) + + assert _diagnostic_codes(exc_info.value) == {"sdl.source_limit"} + + +def test_unknown_source_format_fails_closed() -> None: + assert SDL_SOURCE_FORMAT == "sdl-yaml/v1" + with pytest.raises(SDLParseError) as exc_info: + parse_sdl("name: example\n", source_format="sdl-yaml/v2") + + assert _diagnostic_codes(exc_info.value) == {"sdl.source_format"} + + +def test_unknown_migration_policy_fails_with_a_structured_diagnostic() -> None: + with pytest.raises(SDLParseError) as exc_info: + parse_sdl("name: example\n", migration_policy="guess") + + assert _diagnostic_codes(exc_info.value) == {"sdl.migration_policy"} + + +def test_source_must_be_one_mapping_document() -> None: + with pytest.raises(SDLParseError, match="single document"): + parse_sdl("---\nname: first\n---\nname: second\n") + + with pytest.raises(SDLParseError, match="YAML mapping"): + parse_sdl("- name\n- second\n") + + +def test_unpaired_unicode_surrogate_fails_as_invalid_utf8() -> None: + with pytest.raises(SDLParseError) as exc_info: + parse_sdl("name: \ud800\n") + + assert _diagnostic_codes(exc_info.value) == {"sdl.utf8"} + + +def test_invalid_utf8_file_has_a_structured_source_diagnostic(tmp_path: Path) -> None: + path = tmp_path / "invalid.yaml" + path.write_bytes(b"name: \xff\n") + + with pytest.raises(SDLParseError) as exc_info: + parse_sdl_file(path) + + diagnostic = exc_info.value.diagnostics[0] + assert diagnostic.code == "sdl.utf8" + assert diagnostic.source == str(path) + assert diagnostic.primary_range.start.line == 1 + + +def test_migration_policy_and_source_identity_propagate_through_imports(tmp_path: Path) -> None: + module = tmp_path / "module.yaml" + module.write_text( + """\ +Name: imported +module: + id: acme/imported + version: 1.0.0 + exports: {} +""", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + """\ +name: root +imports: + - path: module.yaml + namespace: imported +""", + encoding="utf-8", + ) + + with pytest.raises(SDLParseError): + parse_sdl_file(root) + + scenario = parse_sdl_file(root, migration_policy=SDLMigrationPolicy.ACCEPT) + assert [item.code for item in scenario.source_diagnostics] == ["sdl.noncanonical_field"] + assert scenario.source_diagnostics[0].source == str(module) + + +def test_indentation_is_not_silently_rewritten() -> None: + with pytest.raises(SDLParseError): + parse_sdl(" name: indented-root\nother: value\n") + + +def test_normative_source_profile_fixture_corpus() -> None: + fixture_root = REPO_ROOT / "contracts" / "fixtures" / "sdl" / "sdl-yaml-v1" + valid = sorted((fixture_root / "valid").glob("*.yaml")) + invalid = sorted((fixture_root / "invalid").glob("*.yaml")) + migration = sorted((fixture_root / "migration").glob("*.yaml")) + assert valid and invalid and migration + + for path in valid: + parse_sdl_file(path, skip_semantic_validation=True) + for path in invalid: + with pytest.raises(SDLParseError): + parse_sdl_file(path, skip_semantic_validation=True) + for path in migration: + with pytest.raises(SDLParseError): + parse_sdl_file(path, skip_semantic_validation=True) + scenario = parse_sdl_file( + path, + migration_policy=SDLMigrationPolicy.ACCEPT, + skip_semantic_validation=True, + ) + assert scenario.source_diagnostics diff --git a/implementations/python/tests/test_sdl_stress.py b/implementations/python/tests/test_sdl_stress.py index ed60249ea..df8cb7ce0 100644 --- a/implementations/python/tests/test_sdl_stress.py +++ b/implementations/python/tests/test_sdl_stress.py @@ -95,11 +95,37 @@ def _parse(yaml_str: str, label: str): conditions: service-check: + proposition: service-ready command: /usr/local/bin/check.sh interval: 30 lib-condition: source: checker-pkg +evidence_requirements: + service-check-evidence: + description: Capture the governed service readiness observation. + source_refs: [nodes.win-10] + scope_refs: [nodes.win-10] + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required + +propositions: + service-ready: + 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} + evidence_requirements: [service-check-evidence] + +assertions: + service-ready: {proposition: service-ready, role: precondition, polarity: positive} + vulnerabilities: sqli-vuln: name: SQL Injection @@ -121,21 +147,21 @@ def _parse(yaml_str: str, label: str): injects: attack-inject: source: attack-pkg - from-entity: red-team - to-entities: + from_entity: red-team + to_entities: - blue-team events: attack-event: - conditions: - - service-check + assertions: + - service-ready injects: - attack-inject scripts: main-script: - start-time: 5 min - end-time: 2 hour + start_time: 5 min + end_time: 2 hour speed: 1.0 events: attack-event: 30 min @@ -1103,11 +1129,14 @@ def _parse(yaml_str: str, label: str): description: "Spearphishing emails targeting finance analyst" sensitive: true items: - - name: "Q3 Budget Review - Action Required.eml" + - name: q3-budget-review + display_name: "Q3 Budget Review - Action Required.eml" tags: [phishing, attachment, macro] - - name: "Urgent Wire Transfer Approval.eml" + - name: urgent-wire-transfer + display_name: "Urgent Wire Transfer Approval.eml" tags: [phishing, link, credential-harvesting] - - name: "Updated Benefits Enrollment.eml" + - name: updated-benefits-enrollment + display_name: "Updated Benefits Enrollment.eml" tags: [phishing, attachment, exe-in-zip] sensitive-financials: type: dataset @@ -1117,9 +1146,11 @@ def _parse(yaml_str: str, label: str): description: "Legitimate confidential financial emails" sensitive: true items: - - name: "Board Minutes - Q3 Confidential.eml" + - name: board-minutes-q3 + display_name: "Board Minutes - Q3 Confidential.eml" tags: [pii, financial, exfil-target] - - name: "M&A Target List - Internal Only.eml" + - name: merger-target-list + display_name: "M&A Target List - Internal Only.eml" tags: [financial, exfil-target] planted-webshell: type: file @@ -1184,12 +1215,46 @@ def _parse(yaml_str: str, label: str): conditions: enterprise0-compromised: + proposition: enterprise0-compromised command: /usr/bin/check-enterprise0-compromise interval: 60 enterprise0-detected: + proposition: enterprise0-detected command: /usr/bin/check-enterprise0-detection interval: 60 +evidence_requirements: + enterprise0-state-evidence: + description: Capture governed enterprise0 state observations. + source_refs: [nodes.enterprise0] + scope_refs: [nodes.enterprise0] + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required + +propositions: + enterprise0-compromised: + 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} + 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} + evidence_requirements: [enterprise0-state-evidence] + +assertions: + enterprise0-compromised: {proposition: enterprise0-compromised, role: postcondition, polarity: positive} + enterprise0-detected: {proposition: enterprise0-detected, role: postcondition, polarity: positive} + accounts: phished-user: username: jdoe @@ -1253,8 +1318,8 @@ def _parse(yaml_str: str, label: str): scripts: day-1: - start-time: 0 - end-time: 2 hour + start_time: 0 + end_time: 2 hour speed: 1 events: phishing-wave: 5 min @@ -1270,7 +1335,7 @@ def _parse(yaml_str: str, label: str): actions: [DiscoverRemoteSystems, ExploitRemoteService, EternalBlue] targets: [enterprise0] success: - conditions: [enterprise0-compromised] + assertions: [enterprise0-compromised] window: stories: [exercise] scripts: [day-1] @@ -1281,7 +1346,7 @@ def _parse(yaml_str: str, label: str): actions: [Monitor, Analyse] targets: [enterprise0, velociraptor] success: - conditions: [enterprise0-detected] + assertions: [enterprise0-detected] window: stories: [exercise] scripts: [day-1] @@ -1434,16 +1499,42 @@ def _parse(yaml_str: str, label: str): conditions: federation-service-up: + proposition: federation-service-up command: /usr/bin/check-adfs-federation interval: 60 +evidence_requirements: + federation-state-evidence: + description: Capture governed federation service observations. + source_refs: [nodes.adfs] + scope_refs: [nodes.adfs] + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required + +propositions: + federation-service-up: + 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} + evidence_requirements: [federation-state-evidence] + +assertions: + federation-service-up: {proposition: federation-service-up, role: postcondition, polarity: positive} + events: federation-cutover: {} scripts: identity-day: - start-time: 0 - end-time: 4 hour + start_time: 0 + end_time: 4 hour speed: 1 events: federation-cutover: 30 min @@ -1457,7 +1548,7 @@ def _parse(yaml_str: str, label: str): entity: blue-team targets: [adfs-service, child-trusts-parent] success: - conditions: [federation-service-up] + assertions: [federation-service-up] window: stories: [federation-exercise] scripts: [identity-day] diff --git a/implementations/python/tests/test_sdl_validator.py b/implementations/python/tests/test_sdl_validator.py index 939e21d81..56a89a583 100644 --- a/implementations/python/tests/test_sdl_validator.py +++ b/implementations/python/tests/test_sdl_validator.py @@ -1,6 +1,7 @@ """Tests for SDL semantic validation.""" import pytest +from pydantic import ValidationError from aces.core.sdl._errors import SDLValidationError from aces.core.sdl.scenario import Scenario @@ -59,13 +60,8 @@ def test_undefined_vulnerability_on_node(self): def test_node_name_too_long(self): long_name = "a" * 36 - s = _make_scenario( - nodes={ - long_name: {"type": "switch"}, - }, - ) - errors = _validate(s) - assert any("35 characters" in e for e in errors) + with pytest.raises(ValidationError, match="35 characters"): + _make_scenario(nodes={long_name: {"type": "switch"}}) @pytest.mark.parametrize( ("field_name", "section_name", "section_value", "error_fragment"), @@ -251,12 +247,12 @@ def test_endpoint_ip_outside_referenced_cidr_is_rejected(self): def test_endpoint_network_variable_reference_is_skipped(self): s = _make_scenario( - variables={"TARGET_NET": {"type": "string", "required": True}}, + variables={"target_net": {"type": "string", "required": True}}, nodes={ "vm": { "type": "vm", "resources": {"ram": "1 gib", "cpu": 1}, - "runtime": {"network": {"endpoints": [{"network": "${TARGET_NET}"}]}}, + "runtime": {"network": {"endpoints": [{"network": "${target_net}"}]}}, }, }, ) @@ -319,7 +315,7 @@ def test_override_subject_missing_from_processes_is_rejected(self): def test_override_with_variable_subject_name_is_skipped(self): s = _make_scenario( - variables={"SHELL_NAME": {"type": "string", "required": True}}, + variables={"shell_name": {"type": "string", "required": True}}, nodes={ "vm": { "type": "vm", @@ -331,7 +327,7 @@ def test_override_with_variable_subject_name_is_skipped(self): "linux_capabilities": { "process_overrides": [ { - "subject": {"name": "${SHELL_NAME}"}, + "subject": {"name": "${shell_name}"}, "scope": "subtree", "drop": ["CAP_AUDIT_CONTROL"], } @@ -410,12 +406,12 @@ def test_inject_references_undefined_entity(self): class TestVerifyEvents: - def test_event_references_undefined_condition(self): + def test_event_references_undefined_assertion(self): s = _make_scenario( - events={"e1": {"conditions": ["missing"]}}, + events={"e1": {"assertions": ["missing"]}}, ) errors = _validate(s) - assert any("undefined condition" in e for e in errors) + assert any("assertion 'missing' not in assertions section" in e for e in errors) class TestVerifyScripts: @@ -636,14 +632,14 @@ def test_valid_relationship(self): errors = _validate(s) assert not errors - def test_relationship_can_target_variable(self): + def test_relationship_rejects_non_targetable_variable(self): s = _make_scenario( nodes={"vm": {"type": "vm", "resources": {"ram": "1 gib", "cpu": 1}}}, variables={"env": {"type": "string", "default": "prod"}}, relationships={"r1": {"type": "connects_to", "source": "vm", "target": "env"}}, ) errors = _validate(s) - assert not errors + assert any("does not reference any defined targetable element" in error for error in errors) def test_relationship_can_target_other_relationship(self): s = _make_scenario( @@ -663,11 +659,11 @@ def test_relationship_can_target_content_item_name(self): "dataset": { "type": "dataset", "target": "vm", - "items": [{"name": "budget.eml"}], + "items": [{"name": "budget-email", "display_name": "budget.eml"}], } }, relationships={ - "r1": {"type": "connects_to", "source": "vm", "target": "budget.eml"}, + "r1": {"type": "connects_to", "source": "vm", "target": "budget-email"}, }, ) errors = _validate(s) @@ -890,7 +886,7 @@ class TestAgentParticipantFraming: """ACT-601 — declarative participant framing fields on Agent. Verifies semantic validation for the three framing fields that don't - already exist on Agent: ``starting_conditions``, ``authority_anchors``, + already exist on Agent: ``starting_assertions``, ``authority_anchors``, ``operating_scope``. Identity and role are already covered by the pre-existing ``Agent.entity`` and ``Entity.role`` bindings; the ``TestVerifyAgents`` cases above cover those. @@ -913,6 +909,27 @@ def _base_scenario_kwargs(self) -> dict: "conditions": { "beacon-online": {"command": "/usr/local/bin/check-beacon", "interval": 30}, }, + "propositions": { + "beacon-online": { + "description": "The governed VM has declared beacon state.", + "subjects": ["nodes.vm"], + "basis": "declared_state", + "predicate": { + "kind": "boolean", + "property": "beacon-online", + "semantic_ref": "urn:aces:declared-property:beacon-online", + "operator": "equals", + "expected": True, + }, + }, + }, + "assertions": { + "beacon-online": { + "proposition": "beacon-online", + "role": "precondition", + "polarity": "positive", + }, + }, "relationships": { "red-controls-vm": { "type": "manages", @@ -922,33 +939,33 @@ def _base_scenario_kwargs(self) -> dict: }, } - def test_undefined_starting_condition(self): + def test_undefined_starting_assertion(self): s = _make_scenario( **self._base_scenario_kwargs(), agents={ "a1": { "entity": "red", - "starting_conditions": ["ghost-condition"], + "starting_assertions": ["ghost-condition"], }, }, ) errors = _validate(s) - assert any("starting_condition 'ghost-condition' not in conditions section" in e for e in errors), errors + assert any("assertion 'ghost-condition' not in assertions section" in e for e in errors), errors - def test_defined_starting_condition(self): + def test_defined_starting_assertion(self): s = _make_scenario( **self._base_scenario_kwargs(), agents={ "a1": { "entity": "red", - "starting_conditions": ["beacon-online"], + "starting_assertions": ["beacon-online"], }, }, ) errors = _validate(s) assert not errors - def test_starting_condition_accepts_variable_placeholder(self): + def test_starting_assertion_accepts_variable_placeholder(self): kwargs = self._base_scenario_kwargs() kwargs["variables"] = {"beacon_ref": {"type": "string", "default": "beacon-online"}} s = _make_scenario( @@ -956,40 +973,38 @@ def test_starting_condition_accepts_variable_placeholder(self): agents={ "a1": { "entity": "red", - "starting_conditions": ["${beacon_ref}"], + "starting_assertions": ["${beacon_ref}"], }, }, ) errors = _validate(s) assert not errors - def test_starting_condition_accepts_qualified_ref(self): - # ADR-020 §6 publishes starting_conditions as accepting bare or - # `conditions.` qualified references. + def test_starting_assertion_rejects_section_qualified_ref(self): s = _make_scenario( **self._base_scenario_kwargs(), agents={ "a1": { "entity": "red", - "starting_conditions": ["conditions.beacon-online"], + "starting_assertions": ["assertions.beacon-online"], }, }, ) errors = _validate(s) - assert not errors + assert any("assertion 'assertions.beacon-online' not in assertions section" in e for e in errors), errors - def test_qualified_starting_condition_undefined_is_rejected(self): + def test_qualified_starting_assertion_undefined_is_rejected(self): s = _make_scenario( **self._base_scenario_kwargs(), agents={ "a1": { "entity": "red", - "starting_conditions": ["conditions.ghost"], + "starting_assertions": ["assertions.ghost"], }, }, ) errors = _validate(s) - assert any("starting_condition 'conditions.ghost' not in conditions section" in e for e in errors), errors + assert any("assertion 'assertions.ghost' not in assertions section" in e for e in errors), errors def test_undefined_authority_anchor(self): s = _make_scenario( @@ -1115,7 +1130,7 @@ def test_full_participant_framing_agent_validates(self): "red-agent": { "entity": "red", "starting_accounts": ["phished"], - "starting_conditions": ["beacon-online"], + "starting_assertions": ["beacon-online"], "authority_anchors": ["red", "red-controls-vm"], "allowed_subnets": ["net"], "operating_scope": ["net", "vm"], @@ -1285,6 +1300,27 @@ def _base_kwargs(self) -> dict: "conditions": { "exercise-passed": {"command": "/bin/check", "interval": 30}, }, + "propositions": { + "exercise-passed": { + "description": "The governed exercise completion state is declared.", + "subjects": ["nodes.web"], + "basis": "declared_state", + "predicate": { + "kind": "boolean", + "property": "exercise-passed", + "semantic_ref": "urn:aces:declared-property:exercise-passed", + "operator": "equals", + "expected": True, + }, + }, + }, + "assertions": { + "exercise-passed": { + "proposition": "exercise-passed", + "role": "postcondition", + "polarity": "positive", + }, + }, "events": {"attack-wave": {}}, "scripts": { "main-timeline": { @@ -1303,7 +1339,7 @@ def test_undefined_agent(self): objectives={ "obj-1": { "agent": "ghost-agent", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, ) @@ -1316,7 +1352,7 @@ def test_undefined_entity(self): objectives={ "obj-1": { "entity": "ghost-team", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, ) @@ -1330,7 +1366,7 @@ def test_actions_must_be_declared_by_agent(self): "obj-1": { "agent": "red-agent", "actions": ["Persist"], - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, ) @@ -1344,7 +1380,7 @@ def test_target_must_resolve(self): "obj-1": { "agent": "red-agent", "targets": ["ghost-target"], - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, ) @@ -1360,7 +1396,7 @@ def test_target_rejects_ambiguous_bare_ref(self): "obj-1": { "agent": "red-agent", "targets": ["web"], - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, ) @@ -1376,7 +1412,7 @@ def test_targets_accept_section_qualified_refs(self): "obj-1": { "agent": "red-agent", "targets": ["nodes.web", "infrastructure.net"], - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, ) @@ -1405,7 +1441,7 @@ def test_targets_can_reference_named_services_and_acls(self): "nodes.web.services.web-https", "infrastructure.net.acls.allow-admin", ], - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, relationships={ @@ -1425,12 +1461,12 @@ def test_success_references_must_exist(self): objectives={ "obj-1": { "agent": "red-agent", - "success": {"conditions": ["ghost-condition"]}, + "success": {"assertions": ["ghost-condition"]}, }, }, ) errors = _validate(s) - assert any("undefined condition" in e for e in errors) + assert any("success assertion 'ghost-condition' not in assertions section" in e for e in errors) def test_window_event_must_belong_to_script(self): kwargs = self._base_kwargs() @@ -1440,7 +1476,7 @@ def test_window_event_must_belong_to_script(self): objectives={ "obj-1": { "agent": "red-agent", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "window": { "scripts": ["main-timeline"], "events": ["cleanup-wave"], @@ -1457,12 +1493,12 @@ def test_dependency_cycle_rejected(self): objectives={ "obj-1": { "agent": "red-agent", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "depends_on": ["obj-2"], }, "obj-2": { "entity": "blue", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "depends_on": ["obj-1"], }, }, @@ -1476,7 +1512,7 @@ def test_depends_on_must_reference_defined_objective(self): objectives={ "obj-1": { "agent": "red-agent", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "depends_on": ["ghost-objective"], }, }, @@ -1490,7 +1526,7 @@ def test_window_steps_require_workflows(self): objectives={ "obj-1": { "agent": "red-agent", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "window": {"steps": ["response.validate"]}, }, }, @@ -1504,7 +1540,7 @@ def test_window_steps_must_belong_to_workflow(self): objectives={ "obj-1": { "agent": "red-agent", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "window": { "workflows": ["response"], "steps": ["other.validate"], @@ -1518,7 +1554,7 @@ def test_window_steps_must_belong_to_workflow(self): "validate": { "type": "objective", "objective": "obj-1", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1529,7 +1565,7 @@ def test_window_steps_must_belong_to_workflow(self): "validate": { "type": "objective", "objective": "obj-1", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1547,7 +1583,7 @@ def test_valid_objective(self): "agent": "red-agent", "actions": ["Scan"], "targets": ["web"], - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "window": { "stories": ["exercise"], "scripts": ["main-timeline"], @@ -1556,7 +1592,7 @@ def test_valid_objective(self): }, "report": { "entity": "blue", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, "depends_on": ["recon"], }, }, @@ -1572,14 +1608,35 @@ def _base_kwargs(self) -> dict: "conditions": { "exercise-passed": {"command": "/bin/check", "interval": 30}, }, + "propositions": { + "exercise-passed": { + "description": "The governed exercise completion state is declared.", + "subjects": ["entities.blue"], + "basis": "declared_state", + "predicate": { + "kind": "boolean", + "property": "exercise-passed", + "semantic_ref": "urn:aces:declared-property:exercise-passed", + "operator": "equals", + "expected": True, + }, + }, + }, + "assertions": { + "exercise-passed": { + "proposition": "exercise-passed", + "role": "postcondition", + "polarity": "positive", + }, + }, "objectives": { "validate-release": { "entity": "blue", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, "rollback-edge": { "entity": "blue", - "success": {"conditions": ["exercise-passed"]}, + "success": {"assertions": ["exercise-passed"]}, }, }, } @@ -1594,7 +1651,7 @@ def test_workflow_references_undefined_objective(self): "validate": { "type": "objective", "objective": "missing-objective", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1629,7 +1686,7 @@ def test_workflow_cycle_rejected(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "parallel", @@ -1639,7 +1696,7 @@ def test_workflow_cycle_rejected(self): "recover": { "type": "objective", "objective": "rollback-edge", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "join", "next": "validate"}, }, @@ -1659,7 +1716,7 @@ def test_workflow_unreachable_step_rejected(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, "orphan": {"type": "end"}, @@ -1685,7 +1742,7 @@ def test_parallel_branch_reference_must_exist(self): "rollback-edge": { "type": "objective", "objective": "rollback-edge", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "finish"}, "finish": {"type": "end"}, @@ -1706,7 +1763,7 @@ def test_valid_workflow(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "decision", @@ -1722,12 +1779,12 @@ def test_valid_workflow(self): "rollback": { "type": "objective", "objective": "rollback-edge", - "on-success": "joined", + "on_success": "joined", }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "finish"}, "finish": {"type": "end"}, @@ -1748,14 +1805,14 @@ def test_valid_retry_step(self): "loop": { "type": "retry", "objective": "validate-release", - "on-success": "finish", - "max-attempts": 5, - "on-exhausted": "recover", + "on_success": "finish", + "max_attempts": 5, + "on_exhausted": "recover", }, "recover": { "type": "objective", "objective": "rollback-edge", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1775,7 +1832,7 @@ def test_valid_switch_and_call_workflow(self): "run": { "type": "objective", "objective": "validate-release", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1796,7 +1853,7 @@ def test_valid_switch_and_call_workflow(self): "delegate": { "type": "call", "workflow": "child", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1816,7 +1873,7 @@ def test_workflow_call_cycle_rejected(self): "delegate": { "type": "call", "workflow": "b", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1827,7 +1884,7 @@ def test_workflow_call_cycle_rejected(self): "delegate": { "type": "call", "workflow": "a", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1847,9 +1904,9 @@ def test_retry_missing_exhausted_step_ref(self): "loop": { "type": "retry", "objective": "validate-release", - "on-success": "finish", - "max-attempts": 3, - "on-exhausted": "nonexistent", + "on_success": "finish", + "max_attempts": 3, + "on_exhausted": "nonexistent", }, "finish": {"type": "end"}, }, @@ -1869,7 +1926,7 @@ def test_step_state_must_reference_prior_executable_step(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "decision", @@ -1895,7 +1952,7 @@ def test_step_state_undefined_ref(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "decision", @@ -1921,7 +1978,7 @@ def test_step_state_non_causal_ref_rejected(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "decision", @@ -1932,7 +1989,7 @@ def test_step_state_non_causal_ref_rejected(self): "confirm": { "type": "objective", "objective": "rollback-edge", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -1973,7 +2030,7 @@ def test_step_state_non_executable_ref_rejected(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "decision", @@ -1999,7 +2056,7 @@ def test_step_state_decision_ref_rejected(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "decision", @@ -2009,7 +2066,7 @@ def test_step_state_decision_ref_rejected(self): }, "gate": { "type": "decision", - "when": {"conditions": ["service-restored"]}, + "when": {"assertions": ["service-restored"]}, "then": "finish", "else": "finish", }, @@ -2031,7 +2088,7 @@ def test_step_state_impossible_outcome_rejected(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "branch", + "on_success": "branch", }, "branch": { "type": "decision", @@ -2056,7 +2113,7 @@ def test_join_rejects_foreign_predecessors(self): "steps": { "gate": { "type": "decision", - "when": {"conditions": ["service-restored"]}, + "when": {"assertions": ["service-restored"]}, "then": "fanout", "else": "joined", }, @@ -2068,12 +2125,12 @@ def test_join_rejects_foreign_predecessors(self): "rollback": { "type": "objective", "objective": "rollback-edge", - "on-success": "joined", + "on_success": "joined", }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "finish"}, "finish": {"type": "end"}, @@ -2099,12 +2156,12 @@ def test_parallel_join_must_be_join_step(self): "rollback": { "type": "objective", "objective": "rollback-edge", - "on-success": "finish", + "on_success": "finish", }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "finish", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -2129,12 +2186,12 @@ def test_parallel_branch_paths_must_converge_on_join(self): "rollback": { "type": "objective", "objective": "rollback-edge", - "on-success": "joined", + "on_success": "joined", }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "finish", + "on_success": "finish", }, "joined": {"type": "join", "next": "finish"}, "finish": {"type": "end"}, @@ -2155,7 +2212,7 @@ def test_join_step_must_be_referenced(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "finish", + "on_success": "finish", }, "orphan-join": {"type": "join", "next": "finish"}, "finish": {"type": "end"}, @@ -2181,12 +2238,12 @@ def test_post_join_branch_state_ref_is_allowed(self): "rollback": { "type": "objective", "objective": "rollback-edge", - "on-success": "joined", + "on_success": "joined", }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "branch"}, "branch": { @@ -2218,13 +2275,13 @@ def test_post_join_attempt_count_ref_is_allowed_when_guaranteed(self): "rollback": { "type": "retry", "objective": "rollback-edge", - "on-success": "joined", - "max-attempts": 3, + "on_success": "joined", + "max_attempts": 3, }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "branch"}, "branch": { @@ -2234,7 +2291,7 @@ def test_post_join_attempt_count_ref_is_allowed_when_guaranteed(self): { "step": "rollback", "outcomes": ["succeeded"], - "min-attempts": 2, + "min_attempts": 2, } ] }, @@ -2264,7 +2321,7 @@ def test_branch_local_state_ref_before_join_is_rejected(self): "rollback": { "type": "objective", "objective": "rollback-edge", - "on-success": "branch-in-branch", + "on_success": "branch-in-branch", }, "branch-in-branch": { "type": "decision", @@ -2275,7 +2332,7 @@ def test_branch_local_state_ref_before_join_is_rejected(self): "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "finish"}, "finish": {"type": "end"}, @@ -2300,19 +2357,19 @@ def test_non_guaranteed_branch_internal_state_ref_after_join_is_rejected(self): }, "rollback": { "type": "decision", - "when": {"conditions": ["service-restored"]}, + "when": {"assertions": ["service-restored"]}, "then": "rollback-success", "else": "joined", }, "rollback-success": { "type": "objective", "objective": "rollback-edge", - "on-success": "joined", + "on_success": "joined", }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "branch"}, "branch": { @@ -2347,17 +2404,17 @@ def test_parallel_failure_bypass_does_not_expose_branch_state(self): "type": "parallel", "branches": ["rollback", "confirm"], "join": "joined", - "on-failure": "recover", + "on_failure": "recover", }, "rollback": { "type": "objective", "objective": "rollback-edge", - "on-success": "joined", + "on_success": "joined", }, "confirm": { "type": "objective", "objective": "validate-release", - "on-success": "joined", + "on_success": "joined", }, "joined": {"type": "join", "next": "finish"}, "recover": { @@ -2387,8 +2444,8 @@ def test_on_failure_variable_ref_tolerated(self): "validate": { "type": "objective", "objective": "validate-release", - "on-success": "finish", - "on-failure": "${recovery_step}", + "on_success": "finish", + "on_failure": "${recovery_step}", }, "finish": {"type": "end"}, }, @@ -2408,10 +2465,10 @@ def test_non_compensable_workflow_step_rejects_compensation_target(self): "steps": { "branch": { "type": "decision", - "when": {"conditions": ["check"]}, + "when": {"assertions": ["check"]}, "then": "finish", "else": "finish", - "compensate-with": "rollback", + "compensate_with": "rollback", }, "finish": {"type": "end"}, }, @@ -2434,8 +2491,8 @@ def test_workflow_compensation_cycle_is_rejected(self): "run": { "type": "objective", "objective": "validate-release", - "compensate-with": "rollback", - "on-success": "finish", + "compensate_with": "rollback", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -2446,8 +2503,8 @@ def test_workflow_compensation_cycle_is_rejected(self): "undo": { "type": "objective", "objective": "rollback-edge", - "compensate-with": "response", - "on-success": "finish", + "compensate_with": "response", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -2468,8 +2525,8 @@ def test_compensation_workflow_cannot_declare_compensate_with_steps(self): "run": { "type": "objective", "objective": "validate-release", - "compensate-with": "rollback", - "on-success": "finish", + "compensate_with": "rollback", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -2480,8 +2537,8 @@ def test_compensation_workflow_cannot_declare_compensate_with_steps(self): "undo": { "type": "objective", "objective": "rollback-edge", - "compensate-with": "cleanup", - "on-success": "finish", + "compensate_with": "cleanup", + "on_success": "finish", }, "finish": {"type": "end"}, }, @@ -2551,6 +2608,26 @@ def test_defined_variables_allow_placeholders_across_models(self): "interval": "${check_interval}", } }, + propositions={ + "check": { + "description": "The variable-selected objective target has declared runtime state.", + "subjects": ["${objective_target}"], + "basis": "declared_state", + "predicate": { + "kind": "presence", + "property": "runtime", + "semantic_ref": "urn:aces:declared-property:runtime", + "operator": "exists", + }, + }, + }, + assertions={ + "check": { + "proposition": "check", + "role": "postcondition", + "polarity": "positive", + }, + }, entities={"blue": {"role": "blue"}}, events={"evt": {}}, scripts={ @@ -2601,7 +2678,7 @@ def test_defined_variables_allow_placeholders_across_models(self): "obj": { "agent": "a1", "targets": ["${objective_target}"], - "success": {"conditions": ["check"]}, + "success": {"assertions": ["check"]}, } }, ) @@ -2712,7 +2789,11 @@ def test_application_qualified_service_ref_resolves(self): def test_application_qualified_service_ref_other_node_is_rejected(self): s = _make_scenario( nodes={ - "other": {"type": "vm", "resources": {"ram": "1 gib", "cpu": 1}}, + "other": { + "type": "vm", + "resources": {"ram": "1 gib", "cpu": 1}, + "services": [{"port": 8081, "name": "http"}], + }, "vm": self._node_with_application( {"application_id": "app", "service": "nodes.other.services.http"}, services=[{"port": 8080, "name": "http"}], @@ -2724,9 +2805,9 @@ def test_application_qualified_service_ref_other_node_is_rejected(self): def test_application_service_variable_reference_is_skipped(self): s = _make_scenario( - variables={"SVC": {"type": "string", "required": True}}, + variables={"svc": {"type": "string", "required": True}}, nodes={ - "vm": self._node_with_application({"application_id": "app", "service": "${SVC}"}), + "vm": self._node_with_application({"application_id": "app", "service": "${svc}"}), }, ) assert _validate(s) == [] @@ -3391,7 +3472,7 @@ def test_database_access_source_must_be_a_runtime_application(self): def test_database_access_variable_source_is_skipped(self): # An unresolved ${var} source is left for instantiation, not flagged. - s = self._scenario_with_app_and_db(source="${APP_REF}") + s = self._scenario_with_app_and_db(source="${app_ref}") assert not any("does not resolve to a runtime application" in e for e in _validate(s)) @@ -3616,19 +3697,12 @@ def test_proxy_upstream_agreement_accepts_bare_and_qualified_service_refs(self): ) assert _validate(s) == [] - def test_proxy_upstream_accepts_dotted_node_names_in_qualified_service_refs(self): - s = self._scenario_with_proxy( - route_upstream={ - "target_node_ref": "app.backend", - "target_service": "nodes.app.backend.services.app", - "tls_terminated_here": True, - }, - proxy_upstream={"client_tls_terminated": True}, - proxy_node_name="front.proxy", - backend_node_name="app.backend", - relationship_target="nodes.app.backend.services.app", - ) - assert _validate(s) == [] + def test_proxy_upstream_rejects_dotted_authored_node_names(self): + with pytest.raises(ValidationError, match="nodes declaration key must be a portable SDL identifier"): + self._scenario_with_proxy( + proxy_node_name="front.proxy", + backend_node_name="app.backend", + ) # --------------------------------------------------------------------------- @@ -3682,7 +3756,11 @@ def test_qualified_service_ref_same_node_resolves(self): def test_qualified_service_ref_other_node_rejected(self): s = _make_scenario( nodes={ - "other": {"type": "vm", "resources": {"ram": "1 gib", "cpu": 1}}, + "other": { + "type": "vm", + "resources": {"ram": "1 gib", "cpu": 1}, + "services": [{"port": 2222, "name": "ssh"}], + }, "vm": self._node_with_ssh_server( {"ssh_server_id": "sshd-default", "service": "nodes.other.services.ssh"}, services=[{"port": 22, "name": "ssh"}], @@ -3705,10 +3783,10 @@ def test_malformed_qualified_service_ref_rejected(self): def test_service_variable_reference_skipped(self): s = _make_scenario( - variables={"SVC": {"type": "string", "required": True}}, + variables={"svc": {"type": "string", "required": True}}, nodes={ "vm": self._node_with_ssh_server( - {"ssh_server_id": "sshd-default", "service": "${SVC}"}, + {"ssh_server_id": "sshd-default", "service": "${svc}"}, ), }, ) diff --git a/implementations/python/tests/test_sem_208_participant_behavior.py b/implementations/python/tests/test_sem_208_participant_behavior.py index aa8534fb2..ed8e247f0 100644 --- a/implementations/python/tests/test_sem_208_participant_behavior.py +++ b/implementations/python/tests/test_sem_208_participant_behavior.py @@ -118,113 +118,113 @@ def _scenario_yaml(*, actions: str = "[scan]", boundaries: str = "[red-view]") - entities: red-team: role: red - action-contracts: + action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: nmap service discovery - realization-profile: backend-declared - fidelity-claim: records participant discovery intent and terminal observation + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: nmap service discovery + realization_profile: backend-declared + fidelity_claim: records participant discovery intent and terminal observation preconditions: - - precondition-id: authority-in-scope - precondition-class: authority + - precondition_id: authority-in-scope + precondition_class: authority description: red participant is authorized to scan the web service - support-refs: [agents.red-agent, nodes.web.services.http] - - precondition-id: target-service-present - precondition-class: target + support_refs: [agents.red-agent, nodes.web.services.http] + - precondition_id: target-service-present + precondition_class: target description: target service exists in the participant action scope - support-refs: [nodes.web.services.http] - - precondition-id: backend-can-realize-scan - precondition-class: realization + support_refs: [nodes.web.services.http] + - precondition_id: backend-can-realize-scan + precondition_class: realization description: backend can realize the scan action contract - support-refs: [backend.participant-runtime] + support_refs: [backend.participant-runtime] effects: - - effect-id: discover-network-services - effect-class: intended_effect + - effect_id: discover-network-services + effect_class: intended_effect description: discover network services - target-refs: [nodes.web.services.http] - - effect-id: participant-service-knowledge-update - effect-class: side_effect + target_refs: [nodes.web.services.http] + - effect_id: participant-service-knowledge-update + effect_class: side_effect description: participant-local service knowledge changes - target-refs: [nodes.web.services.http] - - effect-id: terminal-scan-observation - effect-class: observation_effect + target_refs: [nodes.web.services.http] + - effect_id: terminal-scan-observation + effect_class: observation_effect description: terminal scan observation - evidence-refs: [evidence.scan-output] - - effect-id: participant-view-discovers-node - effect-class: visibility_effect + evidence_refs: [evidence.scan-output] + - effect_id: participant-view-discovers-node + effect_class: visibility_effect description: participant view marks the web node discovered - target-refs: [nodes.web] - - effect-id: scan-output-evidence - effect-class: evidence_effect + target_refs: [nodes.web] + - effect_id: scan-output-evidence + effect_class: evidence_effect description: scan output is retained as evidence - evidence-refs: [evidence.scan-output] - - effect-id: no-hidden-truth-effect - effect-class: no_effect + evidence_refs: [evidence.scan-output] + - effect_id: no-hidden-truth-effect + effect_class: no_effect description: scan does not disclose hidden adjudication material - state-transition-effects: [participant knowledge expands] - observation-expectations: [terminal scan result] - evidence-expectations: [tool output] - failure-classes: [target_unavailable, precondition_unsatisfied, backend_error, unknown] - backend-failure-mappings: - - backend-error-code: backend.target-unreachable - failure-class: target_unavailable + state_transition_effects: [participant knowledge expands] + observation_expectations: [terminal scan result] + evidence_expectations: [tool output] + failure_classes: [target_unavailable, precondition_unsatisfied, backend_error, unknown] + backend_failure_mappings: + - backend_error_code: backend.target-unreachable + failure_class: target_unavailable diagnostic: backend target unreachable interactions: - - interaction-class: shared_state_change + - interaction_class: shared_state_change target: nodes.web.services.http rationale: scan reads and updates participant-visible service knowledge - shared-state-refs: [nodes.web.services.http] - external-mappings: + shared_state_refs: [nodes.web.services.http] + external_mappings: - system: attack identifier: T1046 - loss-label: technique-to-contract + loss_label: technique-to-contract rationale: ATT&CK does not encode ACES observation or state-transition semantics - observation-boundaries: + observation_boundaries: red-view: - projection-basis: participant-local projection over observed services - observable-refs: [] - hidden-refs: [nodes.web, content.private-answer-key] - evidence-refs: [evidence.scan-output] - redaction-policy: hidden refs never project without explicit disclosure - latency-profile: terminal observation emitted after state transition commit - observer-effects: [tool execution may affect telemetry] - realized-view-disclosure: backend reports terminal scan output only - view-rules: - - information-ref: nodes.web - boundary-class: observable_resource + projection_basis: participant-local projection over observed services + observable_refs: [] + hidden_refs: [nodes.web, content.private-answer-key] + evidence_refs: [evidence.scan-output] + redaction_policy: hidden refs never project without explicit disclosure + latency_profile: terminal observation emitted after state transition commit + observer_effects: [tool execution may affect telemetry] + realized_view_disclosure: backend reports terminal scan output only + view_rules: + - information_ref: nodes.web + boundary_class: observable_resource disposition: hidden - visibility-basis: service is not known before terminal scan output - latency-profile: terminal observation latency - - information-ref: content.private-answer-key - boundary-class: private_answer_key + visibility_basis: service is not known before terminal scan output + latency_profile: terminal observation latency + - information_ref: content.private-answer-key + boundary_class: private_answer_key disposition: hidden - visibility-basis: adjudication-only hidden truth - - information-ref: evidence.scan-output - boundary-class: archival_evidence + visibility_basis: adjudication-only hidden truth + - information_ref: evidence.scan-output + boundary_class: archival_evidence disposition: evidence_only - visibility-basis: archival run evidence reference - evidence-refs: [evidence.scan-output] - view-transitions: - - transition-id: discover-web-service - transition-kind: discovery - information-ref: nodes.web + visibility_basis: archival run evidence reference + evidence_refs: [evidence.scan-output] + view_transitions: + - transition_id: discover-web-service + transition_kind: discovery + information_ref: nodes.web trigger: scan terminal observation - effective-from: episode-step:scan-0001:terminal-observation - effective-order: 30 - history-event-type: observation_emitted - action-instance-id: scan-0001 - from-disposition: hidden - to-disposition: discovered - evidence-refs: [evidence.scan-output] + effective_from: episode-step:scan-0001:terminal-observation + effective_order: 30 + history_event_type: observation_emitted + action_instance_id: scan-0001 + from_disposition: hidden + to_disposition: discovered + evidence_refs: [evidence.scan-output] certainty: high - latency-profile: terminal observation latency + latency_profile: terminal observation latency agents: red-agent: entity: red-team actions: {actions} - observation-boundaries: {boundaries} + observation_boundaries: {boundaries} """ ) @@ -258,6 +258,14 @@ def _act607_authority_scope_scenario_yaml() -> str: beacon-online: command: /usr/local/bin/check-beacon interval: 30 + propositions: + beacon-online: + 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} + assertions: + beacon-online: {proposition: beacon-online, role: precondition, polarity: positive} relationships: red-controls-web: type: manages @@ -269,41 +277,41 @@ def _act607_authority_scope_scenario_yaml() -> str: target: web items: - name: playbook - action-contracts: + action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: scan contract - realization-profile: backend-declared - fidelity-claim: records scan intent + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: scan contract + realization_profile: backend-declared + fidelity_claim: records scan intent preconditions: - - precondition-id: authority-in-scope - precondition-class: authority + - precondition_id: authority-in-scope + precondition_class: authority description: participant authority is declared in SDL effects: - - effect-id: no-effect - effect-class: no_effect + - effect_id: no-effect + effect_class: no_effect description: compilation-only contract - failure-classes: [authority_denied, unknown] - observation-boundaries: + failure_classes: [authority_denied, unknown] + observation_boundaries: red-view: - projection-basis: participant view - evidence-refs: [evidence.scan-output] - redaction-policy: hidden refs are not disclosed - latency-profile: immediate + projection_basis: participant view + evidence_refs: [evidence.scan-output] + redaction_policy: hidden refs are not disclosed + latency_profile: immediate agents: red-agent: entity: red-team actions: [scan] - starting-accounts: [operator] - initial-knowledge: + starting_accounts: [operator] + initial_knowledge: hosts: [web] subnets: [net] services: [http] accounts: [operator] - starting-conditions: [beacon-online] - authority-anchors: + starting_assertions: [beacon-online] + authority_anchors: - red-team - red-controls-web - operator @@ -311,21 +319,21 @@ def _act607_authority_scope_scenario_yaml() -> str: - red-view - docs - nodes.web.services.http - operating-scope: + operating_scope: - web - net - nodes.web.services.http - docs - playbook - observation-boundaries: [red-view] - behavior-specifications: + observation_boundaries: [red-view] + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - observation-boundary-refs: [red-view] - authority-scope-refs: + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + observation_boundary_refs: [red-view] + authority_scope_refs: - nodes.web.services.http - operator - scan @@ -333,7 +341,7 @@ def _act607_authority_scope_scenario_yaml() -> str: - docs - playbook - red-controls-web - extension-policy: governed-extension + extension_policy: governed-extension """ ) @@ -358,6 +366,14 @@ def _act607_typed_ref_collision_scenario_yaml() -> str: beacon-online: command: /usr/local/bin/check-beacon interval: 30 + propositions: + beacon-online: + 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} + assertions: + beacon-online: {proposition: beacon-online, role: precondition, polarity: positive} content: operator: type: dataset @@ -374,12 +390,12 @@ def _act607_typed_ref_collision_scenario_yaml() -> str: agents: red-agent: entity: red-team - starting-accounts: [operator] - initial-knowledge: + starting_accounts: [operator] + initial_knowledge: hosts: [web] services: [http] accounts: [operator] - starting-conditions: [beacon-online] + starting_assertions: [beacon-online] """ ) @@ -400,22 +416,22 @@ def test_behavior_specifications_parse_validate_and_compile(): _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - participant-role-refs: [red] - action-contract-refs: [scan] - observation-boundary-refs: [red-view] - authority-scope-refs: [nodes.web.services.http] - behavior-mode: policy-directed - ai-offensive-behavior-refs: [ai-model-access, defense-evasion] - offensive-behavior-refs: [reconnaissance, exfiltration] - realization-profile-ref: participant-implementation-manifest:reference-red-agent - backend-feature-support-refs: [action_contracts] - evidence-contract-refs: [participant-behavior-history-event-stream-v1] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + participant_role_refs: [red] + action_contract_refs: [scan] + observation_boundary_refs: [red-view] + authority_scope_refs: [nodes.web.services.http] + behavior_mode: policy-directed + ai_offensive_behavior_refs: [ai-model-access, defense-evasion] + offensive_behavior_refs: [reconnaissance, exfiltration] + realization_profile_ref: participant-implementation-manifest:reference-red-agent + backend_feature_support_refs: [action_contracts] + evidence_contract_refs: [participant-behavior-history-event-stream-v1] + extension_policy: governed-extension extensions: x-acme:review-note: owner: acme @@ -458,8 +474,8 @@ def test_participant_behavior_runtime_carries_act607_authority_scope_metadata(): "provision.node.web.service.http", "provision.account.operator", ) - assert compiled.starting_condition_refs == ("beacon-online",) - assert compiled.starting_condition_addresses == ("template.condition.beacon-online",) + assert compiled.starting_assertion_refs == ("beacon-online",) + assert compiled.starting_assertion_addresses == ("evaluation.assertion.beacon-online",) assert compiled.authority_anchor_refs == ( "red-team", "red-controls-web", @@ -497,7 +513,7 @@ def test_participant_behavior_runtime_carries_act607_authority_scope_metadata(): "provision.node.web", "provision.network.net", "provision.node.web.service.http", - "template.condition.beacon-online", + "evaluation.assertion.beacon-online", "provision.content.docs", "provision.content.docs.items.playbook", ) @@ -514,12 +530,12 @@ def test_participant_typed_authority_refs_ignore_global_alias_collisions(): "provision.node.web.service.http", "provision.account.operator", ) - assert compiled.starting_condition_addresses == ("template.condition.beacon-online",) + assert compiled.starting_assertion_addresses == ("evaluation.assertion.beacon-online",) assert compiled.refresh_dependencies == ( "provision.account.operator", "provision.node.web", "provision.node.web.service.http", - "template.condition.beacon-online", + "evaluation.assertion.beacon-online", ) @@ -568,46 +584,46 @@ def test_behavior_specification_refs_are_namespaced_during_module_composition(tm exports: entities: [red-team] agents: [red-agent] - action-contracts: [scan] - observation-boundaries: [red-view] - behavior-specifications: [red-scan-behavior] + action_contracts: [scan] + observation_boundaries: [red-view] + behavior_specifications: [red-scan-behavior] entities: red-team: role: red agents: red-agent: entity: red-team - action-contracts: + action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: scan contract - realization-profile: backend-declared - fidelity-claim: records scan intent + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: scan contract + realization_profile: backend-declared + fidelity_claim: records scan intent preconditions: - - precondition-id: authority-in-scope - precondition-class: authority + - precondition_id: authority-in-scope + precondition_class: authority description: participant has authority effects: - - effect-id: no-effect - effect-class: no_effect + - effect_id: no-effect + effect_class: no_effect description: composition-only contract - failure-classes: [unknown] - observation-boundaries: + failure_classes: [unknown] + observation_boundaries: red-view: - projection-basis: participant view - evidence-refs: [evidence.scan-output] - redaction-policy: no hidden refs are disclosed - latency-profile: immediate - behavior-specifications: + projection_basis: participant view + evidence_refs: [evidence.scan-output] + redaction_policy: no hidden refs are disclosed + latency_profile: immediate + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - observation-boundary-refs: [red-view] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + observation_boundary_refs: [red-view] + extension_policy: governed-extension """ ).lstrip(), encoding="utf-8", @@ -644,13 +660,13 @@ def test_behavior_specification_optional_fields_compile_empty_when_omitted(): _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + extension_policy: governed-extension """ ) ) @@ -678,14 +694,14 @@ def test_act_608_behavior_modes_parse_validate_and_compile(behavior_mode: str): _scenario_yaml() + textwrap.dedent( f""" - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - behavior-mode: {behavior_mode} - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + behavior_mode: {behavior_mode} + extension_policy: governed-extension """ ) ) @@ -704,14 +720,14 @@ def test_behavior_specification_behavior_mode_allows_governed_extensions(): _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - behavior-mode: x-acme:swarm-control - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + behavior_mode: x-acme:swarm-control + extension_policy: governed-extension """ ) ) @@ -727,14 +743,14 @@ def test_act_609_offensive_behavior_refs_allow_governed_extensions(): _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - offensive-behavior-refs: [reconnaissance, x-acme:phishing-campaign] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + offensive_behavior_refs: [reconnaissance, x-acme:phishing-campaign] + extension_policy: governed-extension """ ) ) @@ -750,14 +766,14 @@ def test_act_609_ai_offensive_behavior_refs_allow_governed_extensions(): _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - ai-offensive-behavior-refs: [ai-model-access, x-acme:model-poisoning] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + ai_offensive_behavior_refs: [ai-model-access, x-acme:model-poisoning] + extension_policy: governed-extension """ ) ) @@ -772,26 +788,26 @@ def test_act_609_ai_offensive_behavior_refs_allow_governed_extensions(): ("field", "replacement", "expected"), [ ( - "participant-refs: [red-agent]", - "participant-refs: [blue-agent]", + "participant_refs: [red-agent]", + "participant_refs: [blue-agent]", "Behavior specification 'red-scan-behavior' participant_ref 'blue-agent' " "does not reference a declared agent", ), ( - "action-contract-refs: [scan]", - "action-contract-refs: [exploit]", + "action_contract_refs: [scan]", + "action_contract_refs: [exploit]", "Behavior specification 'red-scan-behavior' action_contract_ref 'exploit' " "does not reference a declared action_contract", ), ( - "observation-boundary-refs: [red-view]", - "observation-boundary-refs: [leaked-view]", + "observation_boundary_refs: [red-view]", + "observation_boundary_refs: [leaked-view]", "Behavior specification 'red-scan-behavior' observation_boundary_ref 'leaked-view' " "does not reference a declared observation_boundary", ), ( - "authority-scope-refs: [nodes.web.services.http]", - "authority-scope-refs: [nodes.missing.services.http]", + "authority_scope_refs: [nodes.web.services.http]", + "authority_scope_refs: [nodes.missing.services.http]", "Behavior specification 'red-scan-behavior' authority_scope_ref 'nodes.missing.services.http' " "does not reference any defined targetable element", ), @@ -800,17 +816,17 @@ def test_act_609_ai_offensive_behavior_refs_allow_governed_extensions(): def test_behavior_specification_references_fail_closed(field: str, replacement: str, expected: str): behavior_spec = textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - participant-role-refs: [red] - action-contract-refs: [scan] - observation-boundary-refs: [red-view] - authority-scope-refs: [nodes.web.services.http] - behavior-mode: policy-directed - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + participant_role_refs: [red] + action_contract_refs: [scan] + observation_boundary_refs: [red-view] + authority_scope_refs: [nodes.web.services.http] + behavior_mode: policy-directed + extension_policy: governed-extension """ ) scenario = _scenario_yaml() + behavior_spec.replace(field, replacement) @@ -824,14 +840,14 @@ def test_behavior_specification_references_fail_closed(field: str, replacement: def test_behavior_specification_behavior_mode_uses_governed_vocabulary(): scenario = _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - behavior-mode: supervised - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + behavior_mode: supervised + extension_policy: governed-extension """ ) @@ -844,14 +860,14 @@ def test_behavior_specification_behavior_mode_uses_governed_vocabulary(): def test_behavior_specification_offensive_behavior_refs_use_governed_vocabulary(): scenario = _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - offensive-behavior-refs: [fabricated-attack] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + offensive_behavior_refs: [fabricated-attack] + extension_policy: governed-extension """ ) @@ -864,14 +880,14 @@ def test_behavior_specification_offensive_behavior_refs_use_governed_vocabulary( def test_behavior_specification_ai_offensive_behavior_refs_use_governed_vocabulary(): scenario = _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - ai-offensive-behavior-refs: [fabricated-ai-attack] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + ai_offensive_behavior_refs: [fabricated-ai-attack] + extension_policy: governed-extension """ ) @@ -884,14 +900,14 @@ def test_behavior_specification_ai_offensive_behavior_refs_use_governed_vocabula def test_behavior_specification_backend_feature_refs_use_governed_vocabulary(): scenario = _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - backend-feature-support-refs: [participant-behavior-history] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + backend_feature_support_refs: [participant-behavior-history] + extension_policy: governed-extension """ ) @@ -907,14 +923,14 @@ def test_behavior_specification_backend_feature_refs_use_governed_vocabulary(): def test_behavior_specification_evidence_contract_refs_use_published_contract_ids(): scenario = _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - evidence-contract-refs: [raw-terminal-log-v1] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + evidence_contract_refs: [raw-terminal-log-v1] + extension_policy: governed-extension """ ) @@ -930,13 +946,13 @@ def test_behavior_specification_evidence_contract_refs_use_published_contract_id def test_behavior_specification_extension_keys_are_governed(): scenario = _scenario_yaml() + textwrap.dedent( """ - behavior-specifications: + behavior_specifications: red-scan-behavior: - semantic-version: 1.0.0 - lifecycle-state: active - participant-refs: [red-agent] - action-contract-refs: [scan] - extension-policy: governed-extension + semantic_version: 1.0.0 + lifecycle_state: active + participant_refs: [red-agent] + action_contract_refs: [scan] + extension_policy: governed-extension extensions: custom-mode: note: ungoverned @@ -968,8 +984,8 @@ def test_agent_observation_boundaries_must_resolve_to_declared_boundaries(): def test_participant_interactions_must_resolve_related_action_contracts(): scenario = _scenario_yaml().replace( - " shared-state-refs: [nodes.web.services.http]", - (" related-actions: [coordinate]\n shared-state-refs: [nodes.web.services.http]"), + " shared_state_refs: [nodes.web.services.http]", + (" related_actions: [coordinate]\n shared_state_refs: [nodes.web.services.http]"), ) with pytest.raises(SDLValidationError) as excinfo: @@ -997,8 +1013,8 @@ def test_participant_interactions_must_resolve_targets(): def test_participant_interactions_must_resolve_shared_state_refs(): scenario = _scenario_yaml().replace( - "shared-state-refs: [nodes.web.services.http]", - "shared-state-refs: [nodes.missing.services.http]", + "shared_state_refs: [nodes.web.services.http]", + "shared_state_refs: [nodes.missing.services.http]", ) with pytest.raises(SDLValidationError) as excinfo: @@ -1046,58 +1062,58 @@ def test_view_relation_timeline_tracks_inference_and_concealment_transitions(): scenario = ( _scenario_yaml() .replace( - "hidden-refs: [nodes.web, content.private-answer-key]", - "hidden-refs: [nodes.web, content.private-answer-key, nodes.web.services.http]", + "hidden_refs: [nodes.web, content.private-answer-key]", + "hidden_refs: [nodes.web, content.private-answer-key, nodes.web.services.http]", ) .replace( - " - information-ref: evidence.scan-output\n" - " boundary-class: archival_evidence\n" + " - information_ref: evidence.scan-output\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.scan-output]", - " - information-ref: nodes.web.services.http\n" - " boundary-class: observable_resource\n" + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.scan-output]", + " - information_ref: nodes.web.services.http\n" + " boundary_class: observable_resource\n" " disposition: hidden\n" - " visibility-basis: service is not known before scan output inference\n" - " - information-ref: evidence.scan-output\n" - " boundary-class: archival_evidence\n" + " visibility_basis: service is not known before scan output inference\n" + " - information_ref: evidence.scan-output\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.scan-output]", + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.scan-output]", ) .replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: infer-http-service\n" - " transition-kind: inference\n" - " information-ref: nodes.web.services.http\n" + " latency_profile: terminal observation latency\n" + " - transition_id: infer-http-service\n" + " transition_kind: inference\n" + " information_ref: nodes.web.services.http\n" " trigger: interpret scan output\n" - " effective-from: episode-step:scan-0001:analysis\n" - " effective-order: 40\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: hidden\n" - " to-disposition: inferred\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:analysis\n" + " effective_order: 40\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: hidden\n" + " to_disposition: inferred\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: medium\n" - " latency-profile: participant analysis latency\n" - " - transition-id: conceal-http-service\n" - " transition-kind: concealment\n" - " information-ref: nodes.web.services.http\n" + " latency_profile: participant analysis latency\n" + " - transition_id: conceal-http-service\n" + " transition_kind: concealment\n" + " information_ref: nodes.web.services.http\n" " trigger: redacted follow-up observation\n" - " effective-from: episode-step:scan-0001:redacted-observation\n" - " effective-order: 50\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: inferred\n" - " to-disposition: concealed\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:redacted-observation\n" + " effective_order: 50\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: inferred\n" + " to_disposition: concealed\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: medium\n" - " latency-profile: redaction latency", + " latency_profile: redaction latency", ) ) @@ -1114,8 +1130,8 @@ def test_view_relation_timeline_tracks_inference_and_concealment_transitions(): def test_hidden_truth_cannot_be_observed_without_explicit_disclosure_rule(): scenario = _scenario_yaml().replace( - "observable-refs: []", - "observable-refs: [content.private-answer-key]", + "observable_refs: []", + "observable_refs: [content.private-answer-key]", ) with pytest.raises(SDLParseError) as excinfo: @@ -1128,8 +1144,8 @@ def test_hidden_truth_cannot_be_observed_without_explicit_disclosure_rule(): def test_evidence_only_refs_cannot_be_boundary_observable_refs(): scenario = _scenario_yaml().replace( - "observable-refs: []", - "observable-refs: [evidence.scan-output]", + "observable_refs: []", + "observable_refs: [evidence.scan-output]", ) with pytest.raises(SDLParseError) as excinfo: @@ -1143,26 +1159,26 @@ def test_evidence_only_refs_cannot_be_boundary_observable_refs(): def test_hidden_truth_disclosure_is_separate_from_observable_projection(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: disclose-answer-key\n" - " transition-kind: disclosure\n" - " information-ref: content.private-answer-key\n" + " latency_profile: terminal observation latency\n" + " - transition_id: disclose-answer-key\n" + " transition_kind: disclosure\n" + " information_ref: content.private-answer-key\n" " trigger: episode close adjudication\n" - " effective-from: episode-close\n" - " effective-order: 100\n" - " history-event-type: episode_close\n" - " from-disposition: hidden\n" - " to-disposition: disclosed\n" - " disclosure-rule: reveal answer key after episode close\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-close\n" + " effective_order: 100\n" + " history_event_type: episode_close\n" + " from_disposition: hidden\n" + " to_disposition: disclosed\n" + " disclosure_rule: reveal answer key after episode close\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: post-run adjudication latency\n" - " realized-backend-disclosure: emitted only in post-run adjudication view", + " latency_profile: post-run adjudication latency\n" + " realized_backend_disclosure: emitted only in post-run adjudication view", ) model = compile_runtime_model(parse_sdl(scenario)) @@ -1178,18 +1194,18 @@ def test_hidden_truth_disclosure_is_separate_from_observable_projection(): def test_hidden_truth_disclosure_does_not_make_observable_refs_safe(): scenario = _scenario_yaml() scenario = scenario.replace( - "observable-refs: []", - "observable-refs: [content.private-answer-key]", + "observable_refs: []", + "observable_refs: [content.private-answer-key]", ).replace( - " - information-ref: content.private-answer-key\n" - " boundary-class: private_answer_key\n" + " - information_ref: content.private-answer-key\n" + " boundary_class: private_answer_key\n" " disposition: hidden\n" - " visibility-basis: adjudication-only hidden truth", - " - information-ref: content.private-answer-key\n" - " boundary-class: private_answer_key\n" + " visibility_basis: adjudication-only hidden truth", + " - information_ref: content.private-answer-key\n" + " boundary_class: private_answer_key\n" " disposition: disclosed\n" - " visibility-basis: explicit evaluator disclosure\n" - " disclosure-rule: reveal answer key after episode close", + " visibility_basis: explicit evaluator disclosure\n" + " disclosure_rule: reveal answer key after episode close", ) with pytest.raises(SDLParseError) as excinfo: @@ -1200,14 +1216,14 @@ def test_hidden_truth_disclosure_does_not_make_observable_refs_safe(): def test_private_answer_key_view_rule_requires_disclosure_rule_when_exposed(): scenario = _scenario_yaml().replace( - " - information-ref: content.private-answer-key\n" - " boundary-class: private_answer_key\n" + " - information_ref: content.private-answer-key\n" + " boundary_class: private_answer_key\n" " disposition: hidden\n" - " visibility-basis: adjudication-only hidden truth", - " - information-ref: content.private-answer-key\n" - " boundary-class: private_answer_key\n" + " visibility_basis: adjudication-only hidden truth", + " - information_ref: content.private-answer-key\n" + " boundary_class: private_answer_key\n" " disposition: disclosed\n" - " visibility-basis: explicit evaluator disclosure", + " visibility_basis: explicit evaluator disclosure", ) with pytest.raises(SDLParseError) as excinfo: @@ -1218,24 +1234,24 @@ def test_private_answer_key_view_rule_requires_disclosure_rule_when_exposed(): def test_disclosure_transition_requires_disclosure_rule(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: disclose-answer-key\n" - " transition-kind: disclosure\n" - " information-ref: content.private-answer-key\n" + " latency_profile: terminal observation latency\n" + " - transition_id: disclose-answer-key\n" + " transition_kind: disclosure\n" + " information_ref: content.private-answer-key\n" " trigger: episode close adjudication\n" - " effective-from: episode-close\n" - " effective-order: 100\n" - " history-event-type: episode_close\n" - " from-disposition: hidden\n" - " to-disposition: disclosed\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-close\n" + " effective_order: 100\n" + " history_event_type: episode_close\n" + " from_disposition: hidden\n" + " to_disposition: disclosed\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: post-run adjudication latency", + " latency_profile: post-run adjudication latency", ) with pytest.raises(SDLParseError) as excinfo: @@ -1246,14 +1262,14 @@ def test_disclosure_transition_requires_disclosure_rule(): def test_transition_from_disposition_must_match_initial_view_rule(): scenario = _scenario_yaml().replace( - " - information-ref: nodes.web\n" - " boundary-class: observable_resource\n" + " - information_ref: nodes.web\n" + " boundary_class: observable_resource\n" " disposition: hidden\n" - " visibility-basis: service is not known before terminal scan output", - " - information-ref: nodes.web\n" - " boundary-class: observable_resource\n" + " visibility_basis: service is not known before terminal scan output", + " - information_ref: nodes.web\n" + " boundary_class: observable_resource\n" " disposition: observable\n" - " visibility-basis: incorrectly declared initially visible", + " visibility_basis: incorrectly declared initially visible", ) with pytest.raises(SDLParseError) as excinfo: @@ -1267,14 +1283,14 @@ def test_transition_from_disposition_must_match_initial_view_rule(): def test_sensitive_view_rule_cannot_be_directly_observable(): scenario = _scenario_yaml().replace( - " - information-ref: content.private-answer-key\n" - " boundary-class: private_answer_key\n" + " - information_ref: content.private-answer-key\n" + " boundary_class: private_answer_key\n" " disposition: hidden\n" - " visibility-basis: adjudication-only hidden truth", - " - information-ref: content.private-answer-key\n" - " boundary-class: private_answer_key\n" + " visibility_basis: adjudication-only hidden truth", + " - information_ref: content.private-answer-key\n" + " boundary_class: private_answer_key\n" " disposition: observable\n" - " visibility-basis: adjudication-only hidden truth", + " visibility_basis: adjudication-only hidden truth", ) with pytest.raises(SDLParseError) as excinfo: @@ -1287,8 +1303,8 @@ def test_hidden_truth_view_rule_cannot_be_directly_observable(): scenario = ( _scenario_yaml() .replace( - " boundary-class: private_answer_key", - " boundary-class: hidden_truth", + " boundary_class: private_answer_key", + " boundary_class: hidden_truth", ) .replace( " disposition: hidden", @@ -1304,25 +1320,25 @@ def test_hidden_truth_view_rule_cannot_be_directly_observable(): def test_sensitive_inference_transition_requires_disclosure_rule(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: infer-answer-key\n" - " transition-kind: inference\n" - " information-ref: content.private-answer-key\n" + " latency_profile: terminal observation latency\n" + " - transition_id: infer-answer-key\n" + " transition_kind: inference\n" + " information_ref: content.private-answer-key\n" " trigger: leaked benchmark clue\n" - " effective-from: episode-step:scan-0001:leak\n" - " effective-order: 40\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: hidden\n" - " to-disposition: inferred\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:leak\n" + " effective_order: 40\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: hidden\n" + " to_disposition: inferred\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: low\n" - " latency-profile: terminal observation latency", + " latency_profile: terminal observation latency", ) with pytest.raises(SDLParseError) as excinfo: @@ -1333,8 +1349,8 @@ def test_sensitive_inference_transition_requires_disclosure_rule(): def test_hidden_truth_evidence_reference_requires_evidence_only_rule(): scenario = _scenario_yaml().replace( - "evidence-refs: [evidence.scan-output]", - "evidence-refs: [evidence.scan-output, content.private-answer-key]", + "evidence_refs: [evidence.scan-output]", + "evidence_refs: [evidence.scan-output, content.private-answer-key]", ) with pytest.raises(SDLParseError) as excinfo: @@ -1347,8 +1363,8 @@ def test_hidden_truth_evidence_reference_requires_evidence_only_rule(): def test_view_rule_information_ref_must_be_declared_by_boundary_refs(): scenario = _scenario_yaml().replace( - "information-ref: nodes.web", - "information-ref: nodes.db", + "information_ref: nodes.web", + "information_ref: nodes.db", ) with pytest.raises(SDLValidationError) as excinfo: @@ -1366,16 +1382,16 @@ def test_view_rule_information_ref_must_be_declared_by_boundary_refs(): def test_view_rule_evidence_ref_must_be_declared_by_boundary_evidence_refs(): scenario = _scenario_yaml().replace( - " - information-ref: evidence.scan-output\n" - " boundary-class: archival_evidence\n" + " - information_ref: evidence.scan-output\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.scan-output]", - " - information-ref: evidence.scan-output\n" - " boundary-class: archival_evidence\n" + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.scan-output]", + " - information_ref: evidence.scan-output\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.missing]", + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.missing]", ) with pytest.raises(SDLValidationError) as excinfo: @@ -1388,8 +1404,8 @@ def test_view_rule_evidence_ref_must_be_declared_by_boundary_evidence_refs(): def test_view_transition_evidence_ref_must_be_declared_by_boundary_evidence_refs(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n certainty: high", - " evidence-refs: [evidence.missing]\n certainty: high", + " evidence_refs: [evidence.scan-output]\n certainty: high", + " evidence_refs: [evidence.missing]\n certainty: high", ) with pytest.raises(SDLValidationError) as excinfo: @@ -1403,8 +1419,8 @@ def test_view_transition_evidence_ref_must_be_declared_by_boundary_evidence_refs def test_view_transition_to_disposition_must_match_transition_kind(): scenario = _scenario_yaml().replace( - " to-disposition: discovered", - " to-disposition: inferred", + " to_disposition: discovered", + " to_disposition: inferred", ) with pytest.raises(SDLParseError) as excinfo: @@ -1417,29 +1433,29 @@ def test_view_transition_requires_matching_view_rule(): scenario = ( _scenario_yaml() .replace( - "hidden-refs: [nodes.web, content.private-answer-key]", - "hidden-refs: [nodes.web, content.private-answer-key, nodes.web.services.http]", + "hidden_refs: [nodes.web, content.private-answer-key]", + "hidden_refs: [nodes.web, content.private-answer-key, nodes.web.services.http]", ) .replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: infer-http-service\n" - " transition-kind: inference\n" - " information-ref: nodes.web.services.http\n" + " latency_profile: terminal observation latency\n" + " - transition_id: infer-http-service\n" + " transition_kind: inference\n" + " information_ref: nodes.web.services.http\n" " trigger: interpret scan output\n" - " effective-from: episode-step:scan-0001:analysis\n" - " effective-order: 40\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: hidden\n" - " to-disposition: inferred\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:analysis\n" + " effective_order: 40\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: hidden\n" + " to_disposition: inferred\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: medium\n" - " latency-profile: participant analysis latency", + " latency_profile: participant analysis latency", ) ) @@ -1451,16 +1467,16 @@ def test_view_transition_requires_matching_view_rule(): def test_view_rules_require_unique_information_refs(): scenario = _scenario_yaml().replace( - " - information-ref: evidence.scan-output\n" - " boundary-class: archival_evidence\n" + " - information_ref: evidence.scan-output\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.scan-output]", - " - information-ref: nodes.web\n" - " boundary-class: archival_evidence\n" + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.scan-output]", + " - information_ref: nodes.web\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.scan-output]", + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.scan-output]", ) with pytest.raises(SDLParseError) as excinfo: @@ -1471,25 +1487,25 @@ def test_view_rules_require_unique_information_refs(): def test_view_transitions_require_unique_transition_ids(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: discover-web-service\n" - " transition-kind: discovery\n" - " information-ref: nodes.web\n" + " latency_profile: terminal observation latency\n" + " - transition_id: discover-web-service\n" + " transition_kind: discovery\n" + " information_ref: nodes.web\n" " trigger: duplicate scan terminal observation\n" - " effective-from: episode-step:scan-0001:duplicate-terminal-observation\n" - " effective-order: 31\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: hidden\n" - " to-disposition: discovered\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:duplicate-terminal-observation\n" + " effective_order: 31\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: hidden\n" + " to_disposition: discovered\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", + " latency_profile: terminal observation latency", ) with pytest.raises(SDLParseError) as excinfo: @@ -1500,8 +1516,8 @@ def test_view_transitions_require_unique_transition_ids(): def test_view_transition_from_and_to_dispositions_must_differ(): scenario = _scenario_yaml().replace( - " from-disposition: hidden\n to-disposition: discovered", - " from-disposition: discovered\n to-disposition: discovered", + " from_disposition: hidden\n to_disposition: discovered", + " from_disposition: discovered\n to_disposition: discovered", ) with pytest.raises(SDLParseError) as excinfo: @@ -1512,25 +1528,25 @@ def test_view_transition_from_and_to_dispositions_must_differ(): def test_view_transition_from_disposition_must_match_current_relation(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: conceal-web-service\n" - " transition-kind: concealment\n" - " information-ref: nodes.web\n" + " latency_profile: terminal observation latency\n" + " - transition_id: conceal-web-service\n" + " transition_kind: concealment\n" + " information_ref: nodes.web\n" " trigger: redacted scan follow-up\n" - " effective-from: episode-step:scan-0001:redacted-observation\n" - " effective-order: 40\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: hidden\n" - " to-disposition: concealed\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:redacted-observation\n" + " effective_order: 40\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: hidden\n" + " to_disposition: concealed\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: medium\n" - " latency-profile: redaction latency", + " latency_profile: redaction latency", ) with pytest.raises(SDLParseError) as excinfo: @@ -1544,21 +1560,21 @@ def test_view_transition_from_disposition_must_match_current_relation(): def test_view_transition_effective_order_drives_timeline_not_declaration_order(): scenario = _scenario_yaml().replace( - " - transition-id: discover-web-service\n", - " - transition-id: infer-web-service\n" - " transition-kind: inference\n" - " information-ref: nodes.web\n" + " - transition_id: discover-web-service\n", + " - transition_id: infer-web-service\n" + " transition_kind: inference\n" + " information_ref: nodes.web\n" " trigger: participant interprets terminal scan observation\n" - " effective-from: episode-step:scan-0001:analysis\n" - " effective-order: 40\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: discovered\n" - " to-disposition: inferred\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:analysis\n" + " effective_order: 40\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: discovered\n" + " to_disposition: inferred\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: medium\n" - " latency-profile: participant analysis latency\n" - " - transition-id: discover-web-service\n", + " latency_profile: participant analysis latency\n" + " - transition_id: discover-web-service\n", ) model = compile_runtime_model(parse_sdl(scenario)) @@ -1577,25 +1593,25 @@ def test_view_transition_effective_order_drives_timeline_not_declaration_order() def test_view_transitions_require_unique_effective_order_values(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: duplicate-effective-order\n" - " transition-kind: discovery\n" - " information-ref: nodes.web\n" + " latency_profile: terminal observation latency\n" + " - transition_id: duplicate-effective-order\n" + " transition_kind: discovery\n" + " information_ref: nodes.web\n" " trigger: duplicate scan terminal observation\n" - " effective-from: episode-step:scan-0001:duplicate-terminal-observation\n" - " effective-order: 30\n" - " history-event-type: observation_emitted\n" - " action-instance-id: scan-0001\n" - " from-disposition: hidden\n" - " to-disposition: discovered\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-step:scan-0001:duplicate-terminal-observation\n" + " effective_order: 30\n" + " history_event_type: observation_emitted\n" + " action_instance_id: scan-0001\n" + " from_disposition: hidden\n" + " to_disposition: discovered\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", + " latency_profile: terminal observation latency", ) with pytest.raises(SDLParseError) as excinfo: @@ -1606,13 +1622,13 @@ def test_view_transitions_require_unique_effective_order_values(): def test_view_transitions_require_evidence_certainty_and_latency(): scenario = _scenario_yaml().replace( - " to-disposition: discovered\n" - " evidence-refs: [evidence.scan-output]\n" + " to_disposition: discovered\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " to-disposition: discovered\n" + " latency_profile: terminal observation latency", + " to_disposition: discovered\n" " certainty: high\n" - " latency-profile: terminal observation latency", + " latency_profile: terminal observation latency", ) with pytest.raises(SDLParseError) as excinfo: @@ -1625,11 +1641,11 @@ def test_coordination_interactions_require_related_actions(): scenario = ( _scenario_yaml() .replace( - "interaction-class: shared_state_change", - "interaction-class: coordination", + "interaction_class: shared_state_change", + "interaction_class: coordination", ) .replace( - " shared-state-refs: [nodes.web.services.http]\n", + " shared_state_refs: [nodes.web.services.http]\n", "", ) ) @@ -1644,11 +1660,11 @@ def test_contention_interactions_require_shared_state_refs(): scenario = ( _scenario_yaml() .replace( - "interaction-class: shared_state_change", - "interaction-class: contention", + "interaction_class: shared_state_change", + "interaction_class: contention", ) .replace( - " shared-state-refs: [nodes.web.services.http]\n", + " shared_state_refs: [nodes.web.services.http]\n", "", ) ) @@ -1839,25 +1855,25 @@ def test_behavior_history_rejects_observation_details_that_expose_hidden_truth() def test_behavior_history_rejects_future_episode_close_disclosure_in_observation_details(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: disclose-answer-key\n" - " transition-kind: disclosure\n" - " information-ref: content.private-answer-key\n" + " latency_profile: terminal observation latency\n" + " - transition_id: disclose-answer-key\n" + " transition_kind: disclosure\n" + " information_ref: content.private-answer-key\n" " trigger: episode close adjudication\n" - " effective-from: episode-close\n" - " effective-order: 100\n" - " history-event-type: episode_close\n" - " from-disposition: hidden\n" - " to-disposition: disclosed\n" - " disclosure-rule: reveal answer key after episode close\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-close\n" + " effective_order: 100\n" + " history_event_type: episode_close\n" + " from_disposition: hidden\n" + " to_disposition: disclosed\n" + " disclosure_rule: reveal answer key after episode close\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: post-run adjudication latency", + " latency_profile: post-run adjudication latency", ) model = compile_runtime_model(parse_sdl(scenario)) events = _complete_behavior_history_payloads(ACTION_INSTANCE) @@ -1886,25 +1902,25 @@ def test_behavior_history_rejects_future_episode_close_disclosure_in_observation def test_behavior_history_rejects_unresolved_episode_close_transition_anchor(): scenario = _scenario_yaml().replace( - " evidence-refs: [evidence.scan-output]\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency", - " evidence-refs: [evidence.scan-output]\n" + " latency_profile: terminal observation latency", + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: terminal observation latency\n" - " - transition-id: disclose-answer-key\n" - " transition-kind: disclosure\n" - " information-ref: content.private-answer-key\n" + " latency_profile: terminal observation latency\n" + " - transition_id: disclose-answer-key\n" + " transition_kind: disclosure\n" + " information_ref: content.private-answer-key\n" " trigger: episode close adjudication\n" - " effective-from: episode-close\n" - " effective-order: 100\n" - " history-event-type: episode_close\n" - " from-disposition: hidden\n" - " to-disposition: disclosed\n" - " disclosure-rule: reveal answer key after episode close\n" - " evidence-refs: [evidence.scan-output]\n" + " effective_from: episode-close\n" + " effective_order: 100\n" + " history_event_type: episode_close\n" + " from_disposition: hidden\n" + " to_disposition: disclosed\n" + " disclosure_rule: reveal answer key after episode close\n" + " evidence_refs: [evidence.scan-output]\n" " certainty: high\n" - " latency-profile: post-run adjudication latency", + " latency_profile: post-run adjudication latency", ) model = compile_runtime_model(parse_sdl(scenario)) @@ -2079,7 +2095,7 @@ def test_behavior_history_rejects_details_on_non_observation_events(): def test_behavior_history_rejects_unresolved_visibility_transition_anchor(): - scenario = _scenario_yaml().replace("action-instance-id: scan-0001", "action-instance-id: scan-9999") + scenario = _scenario_yaml().replace("action_instance_id: scan-0001", "action_instance_id: scan-9999") model = compile_runtime_model(parse_sdl(scenario)) violations = list( diff --git a/implementations/python/tests/test_sem_211_participant_action_semantics.py b/implementations/python/tests/test_sem_211_participant_action_semantics.py index db365cf41..916557c6c 100644 --- a/implementations/python/tests/test_sem_211_participant_action_semantics.py +++ b/implementations/python/tests/test_sem_211_participant_action_semantics.py @@ -48,108 +48,108 @@ def _scenario_yaml() -> str: entities: red-team: role: red - action-contracts: + action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: nmap service discovery - realization-profile: backend-declared - fidelity-claim: records participant discovery intent and terminal observation + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: nmap service discovery + realization_profile: backend-declared + fidelity_claim: records participant discovery intent and terminal observation preconditions: - - precondition-id: authority-in-scope - precondition-class: authority + - precondition_id: authority-in-scope + precondition_class: authority description: red participant is authorized to scan the web service - support-refs: [agents.red-agent, nodes.web.services.http] - - precondition-id: target-service-present - precondition-class: target + support_refs: [agents.red-agent, nodes.web.services.http] + - precondition_id: target-service-present + precondition_class: target description: target service exists in the participant action scope - support-refs: [nodes.web.services.http] - - precondition-id: backend-can-realize-scan - precondition-class: realization + support_refs: [nodes.web.services.http] + - precondition_id: backend-can-realize-scan + precondition_class: realization description: backend can realize the scan action contract - support-refs: [backend.participant-runtime] + support_refs: [backend.participant-runtime] effects: - - effect-id: discover-http-service - effect-class: intended_effect + - effect_id: discover-http-service + effect_class: intended_effect description: participant may discover the web HTTP service - target-refs: [nodes.web.services.http] - - effect-id: scan-shared-knowledge-update - effect-class: side_effect + target_refs: [nodes.web.services.http] + - effect_id: scan-shared-knowledge-update + effect_class: side_effect description: participant-local service knowledge is updated - target-refs: [nodes.web.services.http] - - effect-id: terminal-scan-observation - effect-class: observation_effect + target_refs: [nodes.web.services.http] + - effect_id: terminal-scan-observation + effect_class: observation_effect description: terminal scan observation is emitted for the participant - evidence-refs: [evidence.scan-output] - - effect-id: participant-view-discovers-service - effect-class: visibility_effect + evidence_refs: [evidence.scan-output] + - effect_id: participant-view-discovers-service + effect_class: visibility_effect description: participant view marks the web node discovered - target-refs: [nodes.web] - - effect-id: scan-evidence - effect-class: evidence_effect + target_refs: [nodes.web] + - effect_id: scan-evidence + effect_class: evidence_effect description: scan tool output is retained as run evidence - evidence-refs: [evidence.scan-output] - - effect-id: no-hidden-truth-change - effect-class: no_effect + evidence_refs: [evidence.scan-output] + - effect_id: no-hidden-truth-change + effect_class: no_effect description: scan does not expose hidden adjudication material - state-transition-effects: [participant knowledge expands] - failure-classes: + state_transition_effects: [participant knowledge expands] + failure_classes: - precondition_unsatisfied - unsupported_action - target_unavailable - timeout - backend_error - unknown - backend-failure-mappings: - - backend-error-code: backend.timeout - failure-class: timeout + backend_failure_mappings: + - backend_error_code: backend.timeout + failure_class: timeout diagnostic: backend reported action timeout - - backend-error-code: backend.not-supported - failure-class: unsupported_action + - backend_error_code: backend.not-supported + failure_class: unsupported_action diagnostic: backend lacks the scan action implementation interactions: - - interaction-class: shared_state_change + - interaction_class: shared_state_change target: nodes.web.services.http rationale: scan reads and updates participant-visible service knowledge - shared-state-refs: [nodes.web.services.http] - observation-boundaries: + shared_state_refs: [nodes.web.services.http] + observation_boundaries: red-view: - projection-basis: participant-local projection over observed services - observable-refs: [] - hidden-refs: [nodes.web] - evidence-refs: [evidence.scan-output] - redaction-policy: hidden refs never project without explicit disclosure - latency-profile: terminal observation emitted after state transition commit - view-rules: - - information-ref: nodes.web - boundary-class: observable_resource + projection_basis: participant-local projection over observed services + observable_refs: [] + hidden_refs: [nodes.web] + evidence_refs: [evidence.scan-output] + redaction_policy: hidden refs never project without explicit disclosure + latency_profile: terminal observation emitted after state transition commit + view_rules: + - information_ref: nodes.web + boundary_class: observable_resource disposition: hidden - visibility-basis: service is not known before terminal scan output - - information-ref: evidence.scan-output - boundary-class: archival_evidence + visibility_basis: service is not known before terminal scan output + - information_ref: evidence.scan-output + boundary_class: archival_evidence disposition: evidence_only - visibility-basis: archival run evidence reference - evidence-refs: [evidence.scan-output] - view-transitions: - - transition-id: discover-web-service - transition-kind: discovery - information-ref: nodes.web + visibility_basis: archival run evidence reference + evidence_refs: [evidence.scan-output] + view_transitions: + - transition_id: discover-web-service + transition_kind: discovery + information_ref: nodes.web trigger: scan terminal observation - effective-from: episode-step:scan-0001:terminal-observation - effective-order: 30 - history-event-type: observation_emitted - action-instance-id: scan-0001 - from-disposition: hidden - to-disposition: discovered - evidence-refs: [evidence.scan-output] + effective_from: episode-step:scan-0001:terminal-observation + effective_order: 30 + history_event_type: observation_emitted + action_instance_id: scan-0001 + from_disposition: hidden + to_disposition: discovered + evidence_refs: [evidence.scan-output] certainty: high - latency-profile: terminal observation latency + latency_profile: terminal observation latency agents: red-agent: entity: red-team actions: [scan] - observation-boundaries: [red-view] + observation_boundaries: [red-view] """ ) @@ -250,24 +250,24 @@ def _history_payloads_for_action_result(result: ParticipantActionResult) -> list def _scenario_with_hidden_action_result_ref() -> str: scenario = _scenario_yaml().replace( - "hidden-refs: [nodes.web]", - "hidden-refs: [nodes.web, content.private-answer-key]", + "hidden_refs: [nodes.web]", + "hidden_refs: [nodes.web, content.private-answer-key]", ) return scenario.replace( - " - information-ref: evidence.scan-output\n" - " boundary-class: archival_evidence\n" + " - information_ref: evidence.scan-output\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.scan-output]", - " - information-ref: content.private-answer-key\n" - " boundary-class: private_answer_key\n" + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.scan-output]", + " - information_ref: content.private-answer-key\n" + " boundary_class: private_answer_key\n" " disposition: hidden\n" - " visibility-basis: adjudication-only hidden truth\n" - " - information-ref: evidence.scan-output\n" - " boundary-class: archival_evidence\n" + " visibility_basis: adjudication-only hidden truth\n" + " - information_ref: evidence.scan-output\n" + " boundary_class: archival_evidence\n" " disposition: evidence_only\n" - " visibility-basis: archival run evidence reference\n" - " evidence-refs: [evidence.scan-output]", + " visibility_basis: archival run evidence reference\n" + " evidence_refs: [evidence.scan-output]", ) @@ -331,7 +331,7 @@ def test_action_contract_declares_sem_211_classes_and_compiles_them(): def test_legacy_string_preconditions_are_not_sem_211_contracts(): - scenario = _scenario_yaml().replace("precondition-class: authority", "precondition-class: legacy_string") + scenario = _scenario_yaml().replace("precondition_class: authority", "precondition_class: legacy_string") with pytest.raises(SDLParseError) as excinfo: parse_sdl(scenario) @@ -1021,8 +1021,8 @@ def test_action_result_summary_evidence_refs_must_be_grounded_in_reported_result def test_action_result_refs_must_be_authorized_by_observation_boundary(): scenario = _scenario_with_hidden_action_result_ref().replace( - "support-refs: [agents.red-agent, nodes.web.services.http]", - "support-refs: [agents.red-agent, nodes.web.services.http, content.private-answer-key]", + "support_refs: [agents.red-agent, nodes.web.services.http]", + "support_refs: [agents.red-agent, nodes.web.services.http, content.private-answer-key]", ) model = compile_runtime_model(parse_sdl(scenario)) hidden_authority = ParticipantActionPreconditionResult( @@ -1070,9 +1070,9 @@ def test_action_result_refs_must_be_authorized_by_observation_boundary(): def test_action_result_effect_targets_must_be_authorized_by_observation_boundary(): scenario = _scenario_with_hidden_action_result_ref().replace( - " target-refs: [nodes.web.services.http]\n - effect-id: terminal-scan-observation", - " target-refs: [nodes.web.services.http, content.private-answer-key]\n" - " - effect-id: terminal-scan-observation", + " target_refs: [nodes.web.services.http]\n - effect_id: terminal-scan-observation", + " target_refs: [nodes.web.services.http, content.private-answer-key]\n" + " - effect_id: terminal-scan-observation", ) model = compile_runtime_model(parse_sdl(scenario)) result = ParticipantActionResult( @@ -1114,8 +1114,8 @@ def test_action_result_effect_targets_must_be_authorized_by_observation_boundary def test_action_result_evidence_refs_must_be_authorized_by_observation_boundary(): scenario = _scenario_with_hidden_action_result_ref().replace( - "evidence-refs: [evidence.scan-output]", - "evidence-refs: [evidence.scan-output, content.private-answer-key]", + "evidence_refs: [evidence.scan-output]", + "evidence_refs: [evidence.scan-output, content.private-answer-key]", 1, ) model = compile_runtime_model(parse_sdl(scenario)) diff --git a/implementations/python/tests/test_sem_213_temporal_participant_semantics.py b/implementations/python/tests/test_sem_213_temporal_participant_semantics.py index 783bf77c5..40f3197f7 100644 --- a/implementations/python/tests/test_sem_213_temporal_participant_semantics.py +++ b/implementations/python/tests/test_sem_213_temporal_participant_semantics.py @@ -51,128 +51,128 @@ def _scenario_yaml() -> str: entities: red-team: role: red - action-contracts: + action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: nmap service discovery - realization-profile: backend-declared - fidelity-claim: records participant discovery timing without claiming portable wall-clock fidelity + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: nmap service discovery + realization_profile: backend-declared + fidelity_claim: records participant discovery timing without claiming portable wall-clock fidelity preconditions: - - precondition-id: scheduled-window-open - precondition-class: temporal + - precondition_id: scheduled-window-open + precondition_class: temporal description: scan may start only inside the scenario maintenance window - support-refs: [windows.maintenance] - - precondition-id: backend-can-realize-scan - precondition-class: realization + support_refs: [windows.maintenance] + - precondition_id: backend-can-realize-scan + precondition_class: realization description: backend can realize the scan action contract - support-refs: [backend.participant-runtime] + support_refs: [backend.participant-runtime] effects: - - effect-id: terminal-scan-observation - effect-class: observation_effect + - effect_id: terminal-scan-observation + effect_class: observation_effect description: terminal scan observation is emitted for the participant - evidence-refs: [evidence.scan-output] - failure-classes: [precondition_unsatisfied, timeout, backend_error, unknown] - temporal-contracts: - - temporal-id: scan-schedule - temporal-kind: schedule - time-domain: scenario_time - clock-authority: scenario.author.clock - event-points: [submit, start] + evidence_refs: [evidence.scan-output] + failure_classes: [precondition_unsatisfied, timeout, backend_error, unknown] + temporal_contracts: + - temporal_id: scan-schedule + temporal_kind: schedule + time_domain: scenario_time + clock_authority: scenario.author.clock + event_points: [submit, start] description: scan is eligible only during the authored maintenance window - window-ref: windows.maintenance - ordering-basis: participant schedule relation, not raw timestamp causality - backend-disclosure-refs: [timing.remote-pacing] - - temporal-id: scan-cadence - temporal-kind: cadence - time-domain: episode_step - clock-authority: processor.episode-sequence - event-points: [submit] + window_ref: windows.maintenance + ordering_basis: participant schedule relation, not raw timestamp causality + backend_disclosure_refs: [timing.remote-pacing] + - temporal_id: scan-cadence + temporal_kind: cadence + time_domain: episode_step + clock_authority: processor.episode-sequence + event_points: [submit] description: scan attempts are rate-limited per participant episode - duration-ref: cadence.scan.per-episode - reset-boundary: participant episode reset starts a new cadence segment - replay-boundary: replay preserves the original cadence segment id - randomization-basis: seeded participant episode sequence - ordering-basis: participant episode sequence - backend-disclosure-refs: [timing.remote-pacing] - - temporal-id: scan-deadline - temporal-kind: deadline - time-domain: backend_time - clock-authority: backend.adapter.clock - event-points: [submit, deadline, end] + duration_ref: cadence.scan.per-episode + reset_boundary: participant episode reset starts a new cadence segment + replay_boundary: replay preserves the original cadence segment id + randomization_basis: seeded participant episode sequence + ordering_basis: participant episode sequence + backend_disclosure_refs: [timing.remote-pacing] + - temporal_id: scan-deadline + temporal_kind: deadline + time_domain: backend_time + clock_authority: backend.adapter.clock + event_points: [submit, deadline, end] description: backend must realize the scan before the participant timeout - duration-ref: duration.scan.deadline - reset-boundary: participant episode reset clears the deadline state - replay-boundary: replay reports the original deadline state - ordering-basis: backend event order relation - backend-disclosure-refs: [timing.remote-pacing, timing.serialization] - - temporal-id: scan-dwell - temporal-kind: dwell - time-domain: scenario_time - clock-authority: scenario.author.clock - event-points: [start, end] + duration_ref: duration.scan.deadline + reset_boundary: participant episode reset clears the deadline state + replay_boundary: replay reports the original deadline state + ordering_basis: backend event order relation + backend_disclosure_refs: [timing.remote-pacing, timing.serialization] + - temporal_id: scan-dwell + temporal_kind: dwell + time_domain: scenario_time + clock_authority: scenario.author.clock + event_points: [start, end] description: target service must remain in scope for the scan dwell window - window-ref: windows.maintenance - duration-ref: duration.scan.dwell - reset-boundary: participant episode reset clears dwell accumulation - replay-boundary: replay reports original dwell evidence - ordering-basis: scenario window relation - backend-disclosure-refs: [timing.serialization] - - temporal-id: scan-latency - temporal-kind: latency - time-domain: backend_time - clock-authority: backend.adapter.clock - event-points: [submit, observed] + window_ref: windows.maintenance + duration_ref: duration.scan.dwell + reset_boundary: participant episode reset clears dwell accumulation + replay_boundary: replay reports original dwell evidence + ordering_basis: scenario window relation + backend_disclosure_refs: [timing.serialization] + - temporal_id: scan-latency + temporal_kind: latency + time_domain: backend_time + clock_authority: backend.adapter.clock + event_points: [submit, observed] description: terminal observation latency is measured from submit to observation delivery - duration-ref: duration.scan.observation-latency - reset-boundary: participant episode reset closes the latency segment - replay-boundary: replay reports original latency evidence - ordering-basis: backend delivery order relation - backend-disclosure-refs: [timing.remote-pacing] - - temporal-id: scan-window - temporal-kind: time_window - time-domain: wall_clock_time - clock-authority: study.coordinator.clock - event-points: [window_open, window_close] + duration_ref: duration.scan.observation-latency + reset_boundary: participant episode reset closes the latency segment + replay_boundary: replay reports original latency evidence + ordering_basis: backend delivery order relation + backend_disclosure_refs: [timing.remote-pacing] + - temporal_id: scan-window + temporal_kind: time_window + time_domain: wall_clock_time + clock_authority: study.coordinator.clock + event_points: [window_open, window_close] description: study-level collection window for participant attempts - window-ref: study.collection-window - reset-boundary: participant episode reset does not change the study window - replay-boundary: replay reports the original study window - randomization-basis: study coordinator seed and cohort assignment - ordering-basis: study coordinator window relation - backend-disclosure-refs: [timing.remote-pacing] - backend-timing-disclosures: - - disclosure-id: timing.remote-pacing - disclosure-kind: pacing - support-mode: disclosed_limitation + window_ref: study.collection-window + reset_boundary: participant episode reset does not change the study window + replay_boundary: replay reports the original study window + randomization_basis: study coordinator seed and cohort assignment + ordering_basis: study coordinator window relation + backend_disclosure_refs: [timing.remote-pacing] + backend_timing_disclosures: + - disclosure_id: timing.remote-pacing + disclosure_kind: pacing + support_mode: disclosed_limitation description: remote backend pacing is best-effort and not portable semantic time - affected-temporal-ids: + affected_temporal_ids: - scan-schedule - scan-cadence - scan-deadline - scan-latency - scan-window limitations: [wall-clock pacing may lag backend event time] - - disclosure-id: timing.serialization - disclosure-kind: serialization - support-mode: disclosed_limitation + - disclosure_id: timing.serialization + disclosure_kind: serialization + support_mode: disclosed_limitation description: backend serializes scan realization before emitting terminal observation - affected-temporal-ids: [scan-deadline, scan-dwell] + affected_temporal_ids: [scan-deadline, scan-dwell] limitations: [serialized order is disclosed as realized order, not simultaneity] agents: red-agent: entity: red-team actions: [scan] - observation-boundaries: [red-view] - observation-boundaries: + observation_boundaries: [red-view] + observation_boundaries: red-view: - projection-basis: participant-local projection - observable-refs: [nodes.web.services.http] - hidden-refs: [] - evidence-refs: [evidence.scan-output] - redaction-policy: no hidden refs in this SEM-213 fixture - latency-profile: terminal observation emitted after backend realization + projection_basis: participant-local projection + observable_refs: [nodes.web.services.http] + hidden_refs: [] + evidence_refs: [evidence.scan-output] + redaction_policy: no hidden refs in this SEM-213 fixture + latency_profile: terminal observation emitted after backend realization """ ) @@ -224,7 +224,7 @@ def test_temporal_action_contract_declares_sem_213_and_compiles_it() -> None: def test_temporal_claims_require_time_domain_and_clock_authority() -> None: missing_clock_authority = _scenario_yaml().replace( - " clock-authority: scenario.author.clock\n", + " clock_authority: scenario.author.clock\n", "", 1, ) @@ -235,8 +235,8 @@ def test_temporal_claims_require_time_domain_and_clock_authority() -> None: def test_temporal_backend_disclosure_refs_fail_closed() -> None: unknown_backend_disclosure = _scenario_yaml().replace( - "backend-disclosure-refs: [timing.remote-pacing]", - "backend-disclosure-refs: [timing.unknown]", + "backend_disclosure_refs: [timing.remote-pacing]", + "backend_disclosure_refs: [timing.unknown]", 1, ) @@ -246,30 +246,30 @@ def test_temporal_backend_disclosure_refs_fail_closed() -> None: def test_temporal_contract_shapes_fail_closed() -> None: deadline_without_deadline_point = _scenario_yaml().replace( - "event-points: [submit, deadline, end]", - "event-points: [submit, end]", + "event_points: [submit, deadline, end]", + "event_points: [submit, end]", 1, ) dwell_without_sustained_window = _scenario_yaml().replace( - "event-points: [start, end]", - "event-points: [start]", + "event_points: [start, end]", + "event_points: [start]", 1, ) window_without_seed_basis = _scenario_yaml().replace( - " randomization-basis: study coordinator seed and cohort assignment\n", + " randomization_basis: study coordinator seed and cohort assignment\n", "", 1, ) temporal_claim_without_disclosure = _scenario_yaml().replace( - "backend-disclosure-refs: [timing.remote-pacing]", - "backend-disclosure-refs: []", + "backend_disclosure_refs: [timing.remote-pacing]", + "backend_disclosure_refs: []", 1, ) bounded_disclosure_without_bound = ( _scenario_yaml() .replace( - "support-mode: disclosed_limitation", - "support-mode: bounded", + "support_mode: disclosed_limitation", + "support_mode: bounded", 1, ) .replace( 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 ebf3d3ca7..39f1daf1c 100644 --- a/implementations/python/tests/test_sem_215_participant_outcome_interpretation.py +++ b/implementations/python/tests/test_sem_215_participant_outcome_interpretation.py @@ -58,15 +58,38 @@ def _scenario_yaml() -> str: role: red conditions: exfil-detected: + proposition: exfil-detected command: "test -f /tmp/alert" interval: 10 + evidence_requirements: + exfil-detection-evidence: + description: Capture evidence used to decide the exfiltration detection proposition. + source_class: participant_action + scope_refs: [nodes.web.services.http] + boundary_kind: assertion_evaluation + channel: log + artifact_role: proposition_truth_evidence + sensitivity: plain + redaction: redact_secrets + integrity: checksum + retention: study_lifetime + loss_disclosure: required + propositions: + exfil-detected: + 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} + evidence_requirements: [exfil-detection-evidence] + assertions: + exfil-detected: {proposition: exfil-detected, role: postcondition, polarity: positive} objectives: exfil-objective: agent: red-agent actions: [scan] targets: [nodes.web.services.http] success: - conditions: [exfil-detected] + assertions: [exfil-detected] workflows: response-flow: start: verify @@ -74,112 +97,112 @@ def _scenario_yaml() -> str: verify: type: objective objective: exfil-objective - on-success: done - on-failure: done + on_success: done + on_failure: done done: type: end - action-contracts: + action_contracts: scan: - semantic-version: 1.0.0 - lifecycle-state: active - behavioral-granularity: atomic - procedure-basis: nmap service discovery - realization-profile: backend-declared - fidelity-claim: records participant discovery intent and terminal observation + semantic_version: 1.0.0 + lifecycle_state: active + behavioral_granularity: atomic + procedure_basis: nmap service discovery + realization_profile: backend-declared + fidelity_claim: records participant discovery intent and terminal observation preconditions: - - precondition-id: authority-in-scope - precondition-class: authority + - precondition_id: authority-in-scope + precondition_class: authority description: red participant is authorized to scan the web service - support-refs: [agents.red-agent] + support_refs: [agents.red-agent] effects: - - effect-id: scan-evidence - effect-class: evidence_effect + - effect_id: scan-evidence + effect_class: evidence_effect description: scan emits evidence even when the action fails - evidence-refs: [evidence.scan-output, evidence.alert] - - effect-id: detection-alert - effect-class: detection_effect + evidence_refs: [evidence.scan-output, evidence.alert] + - effect_id: detection-alert + effect_class: detection_effect description: scan may trigger a backend detection alert - target-refs: [alerts.ids.scan] - evidence-refs: [evidence.alert] - failure-classes: [precondition_unsatisfied, timeout, backend_error, unknown] - observation-boundaries: + target_refs: [alerts.ids.scan] + evidence_refs: [evidence.alert] + failure_classes: [precondition_unsatisfied, timeout, backend_error, unknown] + observation_boundaries: red-view: - projection-basis: participant-local projection over observed services - observable-refs: [nodes.web.services.http] - hidden-refs: [content.private-answer-key] - evidence-refs: [evidence.scan-output, evidence.alert] - redaction-policy: hidden refs never project without explicit disclosure - latency-profile: terminal observation emitted after state transition commit - view-rules: - - information-ref: nodes.web.services.http - boundary-class: observable_resource + projection_basis: participant-local projection over observed services + observable_refs: [nodes.web.services.http] + hidden_refs: [content.private-answer-key] + evidence_refs: [evidence.scan-output, evidence.alert] + redaction_policy: hidden refs never project without explicit disclosure + latency_profile: terminal observation emitted after state transition commit + view_rules: + - information_ref: nodes.web.services.http + boundary_class: observable_resource disposition: observable - visibility-basis: service is visible to the red participant - - information-ref: content.private-answer-key - boundary-class: private_answer_key + visibility_basis: service is visible to the red participant + - information_ref: content.private-answer-key + boundary_class: private_answer_key disposition: hidden - visibility-basis: adjudication-only hidden truth - - information-ref: evidence.alert - boundary-class: archival_evidence + visibility_basis: adjudication-only hidden truth + - information_ref: evidence.alert + boundary_class: archival_evidence disposition: evidence_only - visibility-basis: archival alert evidence reference - evidence-refs: [evidence.alert] + visibility_basis: archival alert evidence reference + evidence_refs: [evidence.alert] agents: red-agent: entity: red-team actions: [scan] - observation-boundaries: [red-view] - outcome-interpretation-rules: + observation_boundaries: [red-view] + outcome_interpretation_rules: scan-evidence-objective: - semantic-version: 1.0.0 - participant-scope: participant_local - observation-point-basis: terminal participant observation event - interpretation-basis: explicit SEM-215 mapping from local scan evidence to objective/evaluation meaning - evidence-refs: [evidence.alert] + semantic_version: 1.0.0 + participant_scope: participant_local + observation_point_basis: terminal participant observation event + interpretation_basis: explicit SEM-215 mapping from local scan evidence to objective/evaluation meaning + evidence_refs: [evidence.alert] limitations: - local scan result is not objective success by itself - reward remains a derived assessment signal - source-bindings: - - source-id: local-action - source-layer: participant_action_outcome + source_bindings: + - source_id: local-action + source_layer: participant_action_outcome ref: scan - interpretation-role: local action status input - evidence-refs: [evidence.scan-output] - - source-id: alert-evidence - source-layer: evidence_claim + interpretation_role: local action status input + evidence_refs: [evidence.scan-output] + - source_id: alert-evidence + source_layer: evidence_claim ref: evidence.alert - interpretation-role: alert evidence input - evidence-refs: [evidence.alert] - - source-id: scaffold - source-layer: scaffold_variant + interpretation_role: alert evidence input + evidence_refs: [evidence.alert] + - source_id: scaffold + source_layer: scaffold_variant ref: scaffold.standard - interpretation-role: benchmark context input - provenance-refs: [provenance.scaffold.standard] - target-bindings: - - target-id: objective-meaning - target-layer: objective_result + interpretation_role: benchmark context input + provenance_refs: [provenance.scaffold.standard] + target_bindings: + - target_id: objective-meaning + target_layer: objective_result ref: exfil-objective relation: evidence supports objective interpretation - evidence-refs: [evidence.alert] + evidence_refs: [evidence.alert] limitations: [objective success still requires evaluator confirmation] - - target-id: evaluation-meaning - target-layer: evaluation_result + - target_id: evaluation-meaning + target_layer: evaluation_result ref: exfil-eval relation: evidence is an input to evaluation meaning - evidence-refs: [evidence.alert] + evidence_refs: [evidence.alert] limitations: [evaluation result is not inferred from action status] - - target-id: workflow-meaning - target-layer: workflow_result + - target_id: workflow-meaning + target_layer: workflow_result ref: response-flow relation: evidence may inform workflow result interpretation - evidence-refs: [evidence.alert] + evidence_refs: [evidence.alert] limitations: [workflow completion is evaluated separately] - - target-id: reward-meaning - target-layer: reward_signal + - target_id: reward-meaning + target_layer: reward_signal ref: reward.scan-learning relation: reward relevant only under governed assessment rule - governance-ref: assessment.reward.scan-learning - evidence-refs: [evidence.alert] + governance_ref: assessment.reward.scan-learning + evidence_refs: [evidence.alert] limitations: [reward is derived, not the participant outcome] """ ) @@ -334,16 +357,16 @@ def _history_payloads( def _episode_status_scenario_yaml() -> str: return _scenario_yaml().replace( - " - source-id: scaffold\n" - " source-layer: scaffold_variant\n" + " - source_id: scaffold\n" + " source_layer: scaffold_variant\n" " ref: scaffold.standard\n" - " interpretation-role: benchmark context input\n" - " provenance-refs: [provenance.scaffold.standard]", - " - source-id: episode-terminal\n" - " source-layer: participant_episode_status\n" + " interpretation_role: benchmark context input\n" + " provenance_refs: [provenance.scaffold.standard]", + " - source_id: episode-terminal\n" + " source_layer: participant_episode_status\n" " ref: episode.terminal\n" - " interpretation-role: participant episode terminal status input\n" - " provenance-refs: [runtime.participant-episode-history.episode-1]", + " interpretation_role: participant episode terminal status input\n" + " provenance_refs: [runtime.participant-episode-history.episode-1]", 1, ) @@ -646,7 +669,7 @@ def test_outcome_evidence_refs_must_be_grounded_in_event_payload() -> None: def test_reward_interpretation_requires_governed_assessment_rule() -> None: missing_governance = _scenario_yaml().replace( - " governance-ref: assessment.reward.scan-learning\n", + " governance_ref: assessment.reward.scan-learning\n", "", 1, ) @@ -657,8 +680,8 @@ def test_reward_interpretation_requires_governed_assessment_rule() -> None: def test_outcome_interpretation_scope_is_participant_local_only() -> None: global_scope = _scenario_yaml().replace( - " participant-scope: participant_local\n", - " participant-scope: cohort_global\n", + " participant_scope: participant_local\n", + " participant_scope: cohort_global\n", 1, ) @@ -700,7 +723,7 @@ def test_reward_interpretation_records_must_match_declared_governance_ref() -> N def test_benchmark_outcome_inputs_require_explicit_provenance() -> None: missing_provenance = _scenario_yaml().replace( - " provenance-refs: [provenance.scaffold.standard]\n", + " provenance_refs: [provenance.scaffold.standard]\n", "", 1, ) @@ -711,15 +734,15 @@ def test_benchmark_outcome_inputs_require_explicit_provenance() -> None: def test_episode_status_outcome_inputs_require_explicit_provenance() -> None: missing_provenance = _scenario_yaml().replace( - " - source-id: scaffold\n" - " source-layer: scaffold_variant\n" + " - source_id: scaffold\n" + " source_layer: scaffold_variant\n" " ref: scaffold.standard\n" - " interpretation-role: benchmark context input\n" - " provenance-refs: [provenance.scaffold.standard]", - " - source-id: episode-terminal\n" - " source-layer: participant_episode_status\n" + " interpretation_role: benchmark context input\n" + " provenance_refs: [provenance.scaffold.standard]", + " - source_id: episode-terminal\n" + " source_layer: participant_episode_status\n" " ref: episode.terminal\n" - " interpretation-role: participant episode terminal status input", + " interpretation_role: participant episode terminal status input", 1, ) @@ -842,8 +865,8 @@ def test_declared_outcome_source_provenance_must_be_preserved_at_runtime() -> No def test_outcome_source_provenance_cannot_expose_hidden_boundary_refs() -> None: hidden_provenance_yaml = _scenario_yaml().replace( - " provenance-refs: [provenance.scaffold.standard]\n", - " provenance-refs: [content.private-answer-key]\n", + " provenance_refs: [provenance.scaffold.standard]\n", + " provenance_refs: [content.private-answer-key]\n", 1, ) model = compile_runtime_model(parse_sdl(hidden_provenance_yaml)) diff --git a/implementations/python/tests/test_sem_218_explicitness.py b/implementations/python/tests/test_sem_218_explicitness.py index 55ad71546..a211c34cf 100644 --- a/implementations/python/tests/test_sem_218_explicitness.py +++ b/implementations/python/tests/test_sem_218_explicitness.py @@ -35,7 +35,7 @@ def _scenario_with_explicitness_cases(): network: endpoints: - network: net - network-id-stability: unknown + network_id_stability: unknown infrastructure: net: count: 1 diff --git a/implementations/python/tests/test_semantics_assessment.py b/implementations/python/tests/test_semantics_assessment.py index 07be20094..59cd92244 100644 --- a/implementations/python/tests/test_semantics_assessment.py +++ b/implementations/python/tests/test_semantics_assessment.py @@ -5,8 +5,8 @@ scoring, reward, and evaluation outputs live in the experiment/evaluator plane (ADR-055/064/069), not in authored SDL. What remains in ``aces.core.semantics.assessment`` is the resource-kind qualifier that objective -success references carry: objective success references observable state only, so -``CONDITION`` is the sole member. +success references carry: objective success composes backend-neutral assertions, +so ``ASSERTION`` is the sole member. These tests pin that reduced surface so a future revival of the removed scoring sections is a visible, deliberate change rather than an accident. @@ -21,11 +21,11 @@ class TestAssessmentResourceKind: - def test_condition_is_the_only_member(self) -> None: - assert [kind.name for kind in AssessmentResourceKind] == ["CONDITION"] + def test_assertion_is_the_only_member(self) -> None: + assert [kind.name for kind in AssessmentResourceKind] == ["ASSERTION"] - def test_condition_value(self) -> None: - assert AssessmentResourceKind.CONDITION.value == "condition" + def test_assertion_value(self) -> None: + assert AssessmentResourceKind.ASSERTION.value == "assertion" def test_is_str_enum(self) -> None: assert issubclass(AssessmentResourceKind, str) diff --git a/implementations/python/tests/test_semantics_objectives.py b/implementations/python/tests/test_semantics_objectives.py index 59182adb1..b86c2964a 100644 --- a/implementations/python/tests/test_semantics_objectives.py +++ b/implementations/python/tests/test_semantics_objectives.py @@ -65,24 +65,51 @@ def _window_issue_codes(analysis) -> set[str]: def _write_objective_window_scenario(path: Path, *, namespace: str = "") -> None: prefix = f"{namespace}." if namespace else "" + module_descriptor = "" + if not namespace: + module_descriptor = """ +module: + id: aces/window + version: 1.0.0 + exports: + propositions: [health-state] + assertions: [health] + entities: [blue] + stories: [intro] + scripts: [timeline] + events: [kickoff] + objectives: [observe] + workflows: [flow] +""" path.write_text( f""" name: {namespace or "window"} version: 1.0.0 -conditions: - {prefix}health: - command: /bin/true - interval: 15 +{module_descriptor} entities: {prefix}blue: role: blue +propositions: + {prefix}health-state: + description: The blue entity is declared for the objective-window fixture. + subjects: [entities.{prefix}blue] + basis: declared_state + predicate: + kind: presence + property: role + semantic_ref: urn:aces:declared-property:entity-role + operator: exists +assertions: + {prefix}health: + proposition: {prefix}health-state + role: postcondition stories: {prefix}intro: scripts: [{prefix}timeline] scripts: {prefix}timeline: - start-time: 0 - end-time: 60 + start_time: 0 + end_time: 60 speed: 1 events: {prefix}kickoff: 0 @@ -92,7 +119,7 @@ def _write_objective_window_scenario(path: Path, *, namespace: str = "") -> None {prefix}observe: entity: {prefix}blue success: - conditions: [{prefix}health] + assertions: [{prefix}health] window: stories: [{prefix}intro] scripts: [{prefix}timeline] @@ -106,7 +133,7 @@ def _write_objective_window_scenario(path: Path, *, namespace: str = "") -> None start: type: objective objective: {prefix}observe - on-success: finish + on_success: finish finish: type: end """, @@ -282,7 +309,7 @@ def test_composition_ready_invariant_imported_window_analysis_uses_expanded_cano analysis = _analyze( scenario.objectives, entity_names=set(scenario.entities), - conditions_by_name=scenario.conditions, + assertions_by_name=scenario.assertions, stories_by_name=scenario.stories, scripts_by_name=scenario.scripts, events_by_name=scenario.events, @@ -309,9 +336,11 @@ def test_composition_ready_invariant_namespace_extends_window_identity_without_c self, tmp_path: Path ) -> None: plain = tmp_path / "plain.yaml" - namespaced = tmp_path / "namespaced.yaml" + imported = tmp_path / "window-module.yaml" + namespaced = tmp_path / "namespaced-root.yaml" _write_objective_window_scenario(plain) - _write_objective_window_scenario(namespaced, namespace="shared") + _write_objective_window_scenario(imported) + _write_importing_root(namespaced, imported.name, namespace="shared") plain_scenario = parse_sdl_file(plain) namespaced_scenario = parse_sdl_file(namespaced) @@ -368,9 +397,9 @@ def test_workflow_step_normalization_is_stable(self, step_refs: list[str]): assert analysis.workflow_step_refs == tuple(dict.fromkeys(step_refs)) -def _success(*, conditions=None, mode="all_of"): +def _success(*, assertions=None, mode="all_of"): return SimpleNamespace( - conditions=list(conditions or []), + assertions=list(assertions or []), mode=mode, ) @@ -391,7 +420,7 @@ def _objective(*, agent="", entity="", actions=None, targets=None, success=None, entity=entity, actions=list(actions or []), targets=list(targets or []), - success=success if success is not None else _success(conditions=["health"]), + success=success if success is not None else _success(assertions=["health"]), window=window, depends_on=list(depends_on or []), ) @@ -410,11 +439,11 @@ def _analyze(objectives, **overrides): Resource maps are bundled into ``AssessmentResourceCatalog`` / ``WindowResourceCatalog`` for the analyzer; tests still pass the per-section - overrides (``conditions_by_name``, ``stories_by_name``, …) for readability. + overrides (``assertions_by_name``, ``stories_by_name``, …) for readability. """ section_defaults = { - "conditions_by_name": {}, + "assertions_by_name": {}, "stories_by_name": {}, "scripts_by_name": {}, "events_by_name": {}, @@ -426,7 +455,7 @@ def _analyze(objectives, **overrides): "agents_by_name": {}, "entity_names": set(), "assessment_resources": AssessmentResourceCatalog( - conditions=sections["conditions_by_name"], + assertions=sections["assertions_by_name"], ), "window_resources": WindowResourceCatalog( stories=sections["stories_by_name"], @@ -444,19 +473,19 @@ class TestObjectiveSemantics: def test_well_formed_objectives_normalize_references_and_dependencies(self) -> None: analysis = _analyze( { - "base": _objective(entity="blue", success=_success(conditions=["c1"])), + "base": _objective(entity="blue", success=_success(assertions=["c1"])), "follow": _objective( agent="red", actions=["Scan"], targets=["nodes.web"], - success=_success(conditions=["c2"]), + success=_success(assertions=["c2"]), window=_window(workflows=["flow"], steps=["flow.branch"]), depends_on=["base"], ), }, agents_by_name={"red": _agent("Scan", "Exploit")}, entity_names={"blue"}, - conditions_by_name={"c1": object(), "c2": object()}, + assertions_by_name={"c1": object(), "c2": object()}, workflows_by_name={"flow": _workflow("start", "branch")}, targetable_name_index={"nodes.web": {"nodes.web"}}, ) @@ -469,15 +498,15 @@ def test_well_formed_objectives_normalize_references_and_dependencies(self) -> N "nodes.web" } assert {ref.canonical_name for ref in analysis.references_of_kind(ObjectiveReferenceKind.SUCCESS)} == { - "condition.c1", - "condition.c2", + "assertion.c1", + "assertion.c2", } success_kinds = { ref.canonical_name: ref.success_resource_kind for ref in analysis.references_of_kind(ObjectiveReferenceKind.SUCCESS) } - assert success_kinds["condition.c1"] == AssessmentResourceKind.CONDITION - assert success_kinds["condition.c2"] == AssessmentResourceKind.CONDITION + assert success_kinds["assertion.c1"] == AssessmentResourceKind.ASSERTION + assert success_kinds["assertion.c2"] == AssessmentResourceKind.ASSERTION assert {ref.canonical_name for ref in analysis.references_of_kind(ObjectiveReferenceKind.WINDOW)} == { "flow", "flow.branch", @@ -497,10 +526,10 @@ def test_well_formed_objectives_normalize_references_and_dependencies(self) -> N for ref in analysis.references_of_kind(kind): assert ref.dependency_roles == () - assert analysis.dependencies_for("base").ordering_names == ("condition.c1",) - assert analysis.dependencies_for("base").refresh_names == ("condition.c1",) - assert analysis.dependencies_for("follow").ordering_names == ("condition.c2", "objective.base") - assert analysis.dependencies_for("follow").refresh_names == ("condition.c2", "objective.base", "workflow.flow") + assert analysis.dependencies_for("base").ordering_names == ("assertion.c1",) + assert analysis.dependencies_for("base").refresh_names == ("assertion.c1",) + assert analysis.dependencies_for("follow").ordering_names == ("assertion.c2", "objective.base") + assert analysis.dependencies_for("follow").refresh_names == ("assertion.c2", "objective.base", "workflow.flow") assert "follow" in analysis.window_analyses def test_undeclared_actor_references_are_reported(self) -> None: @@ -511,7 +540,7 @@ def test_undeclared_actor_references_are_reported(self) -> None: }, agents_by_name={"red": _agent()}, entity_names={"blue"}, - conditions_by_name={"health": object()}, + assertions_by_name={"health": object()}, ) codes = {issue.code for issue in analysis.issues} assert "objective.actor-agent-undeclared" in codes @@ -521,7 +550,7 @@ def test_agent_action_must_be_declared(self) -> None: analysis = _analyze( {"a": _objective(agent="red", actions=["Persist"])}, agents_by_name={"red": _agent("Scan")}, - conditions_by_name={"health": object()}, + assertions_by_name={"health": object()}, ) issue = analysis.issues_of_code("objective.action-not-declared")[0] assert issue.ref == "Persist" @@ -531,7 +560,7 @@ def test_unresolvable_target_is_reported(self) -> None: analysis = _analyze( {"a": _objective(entity="blue", targets=["ghost"])}, entity_names={"blue"}, - conditions_by_name={"health": object()}, + assertions_by_name={"health": object()}, ) assert analysis.issues_of_code("objective.target-unresolvable")[0].ref == "ghost" @@ -539,37 +568,37 @@ def test_ambiguous_target_is_reported_with_sorted_candidates(self) -> None: analysis = _analyze( {"a": _objective(entity="blue", targets=["web"])}, entity_names={"blue"}, - conditions_by_name={"health": object()}, + assertions_by_name={"health": object()}, targetable_name_index={"web": {"nodes.web", "features.web"}}, ) issue = analysis.issues_of_code("objective.target-ambiguous")[0] assert issue.ref == "web" assert issue.candidates == ("features.web", "nodes.web") - def test_undeclared_success_condition_is_reported(self) -> None: + def test_undeclared_success_assertion_is_reported(self) -> None: analysis = _analyze( { "a": _objective( entity="blue", - success=_success(conditions=["c?"]), + success=_success(assertions=["c?"]), ) }, entity_names={"blue"}, ) codes = {issue.code for issue in analysis.issues} - assert "objective.success-condition-undeclared" in codes + assert "objective.success-assertion-undeclared" in codes def test_window_issues_are_resurfaced_under_objective_codes(self) -> None: analysis = _analyze( { "a": _objective( entity="blue", - success=_success(conditions=["health"]), + success=_success(assertions=["health"]), window=_window(scripts=["s1"], events=["evt"]), ) }, entity_names={"blue"}, - conditions_by_name={"health": object()}, + assertions_by_name={"health": object()}, scripts_by_name={"s1": SimpleNamespace(events={"kickoff": 1})}, events_by_name={"evt": SimpleNamespace()}, ) @@ -579,7 +608,7 @@ def test_undeclared_dependency_is_reported(self) -> None: analysis = _analyze( {"a": _objective(entity="blue", depends_on=["ghost"])}, entity_names={"blue"}, - conditions_by_name={"health": object()}, + assertions_by_name={"health": object()}, ) assert analysis.issues_of_code("objective.dependency-undeclared")[0].ref == "ghost" @@ -590,7 +619,7 @@ def test_dependency_cycle_is_reported_once_globally(self) -> None: "b": _objective(entity="blue", depends_on=["a"]), }, entity_names={"blue"}, - conditions_by_name={"health": object()}, + assertions_by_name={"health": object()}, ) cycle_issues = analysis.issues_of_code("objective.dependency-cycle") assert len(cycle_issues) == 1 @@ -603,7 +632,7 @@ def test_unresolved_variable_references_are_skipped(self) -> None: agent="${actor}", actions=["${act}"], targets=["${tgt}"], - success=_success(conditions=["${m}"]), + success=_success(assertions=["${m}"]), window=_window(stories=["${story}"]), depends_on=["${dep}"], ) diff --git a/implementations/python/tests/test_truth_result_contracts.py b/implementations/python/tests/test_truth_result_contracts.py new file mode 100644 index 000000000..50852facc --- /dev/null +++ b/implementations/python/tests/test_truth_result_contracts.py @@ -0,0 +1,138 @@ +"""Portable proposition truth-result contracts for issue #725.""" + +from __future__ import annotations + +from copy import deepcopy + +import pytest +from aces_contracts.contracts import PropositionTruthResultModel, schema_bundle +from pydantic import ValidationError + + +def _observed_result() -> dict[str, object]: + return { + "schema_version": "proposition-truth-result/v1", + "result_id": "truth-run-42-service-ready", + "proposition_address": "evaluation.proposition.service-available", + "assertion_address": "evaluation.assertion.service-ready", + "assertion_polarity": "positive", + "proposition_outcome": "true", + "assertion_outcome": "true", + "evaluation_basis": "observed_state", + "indeterminacy_reason": None, + "probe_binding": { + "binding_id": "http-probe-binding", + "implementation_id": "example/http-health-probe", + "implementation_version": "2.1.0", + "artifact_digest": "sha256:" + "a" * 64, + "backend_manifest_ref": "backend-manifest:range-a", + "proposition_address": "evaluation.proposition.service-available", + "capability_refs": ["predicate.string.equals", "evidence.log"], + }, + "evidence_refs": ["experiment-evidence:run-42/http-health"], + "declared_artifact_digest": None, + "temporal_context": { + "boundary_ref": "objectives.restore-service:completion", + "time_domain": "scenario_time", + "clock_authority": "experiment-clock:run-42", + }, + "loss_disclosures": [], + "unsupported_capability_refs": [], + } + + +def test_observed_decision_requires_binding_evidence_and_governed_time_context() -> None: + result = PropositionTruthResultModel.model_validate(_observed_result()) + + assert result.proposition_outcome.value == "true" + assert result.assertion_outcome.value == "true" + assert result.probe_binding is not None + assert result.probe_binding.artifact_digest.startswith("sha256:") + + +@pytest.mark.parametrize("field", ["probe_binding", "temporal_context"]) +def test_observed_decision_rejects_missing_realization_context(field: str) -> None: + payload = _observed_result() + payload[field] = None + with pytest.raises(ValidationError, match=field): + PropositionTruthResultModel.model_validate(payload) + + +def test_observed_decision_rejects_missing_evidence() -> None: + payload = _observed_result() + payload["evidence_refs"] = [] + with pytest.raises(ValidationError, match="evidence_refs"): + PropositionTruthResultModel.model_validate(payload) + + +def test_negative_assertion_must_invert_only_decided_truth() -> None: + payload = _observed_result() + payload["assertion_polarity"] = "negative" + payload["assertion_outcome"] = "false" + assert PropositionTruthResultModel.model_validate(payload).assertion_outcome.value == "false" + + payload["assertion_outcome"] = "true" + with pytest.raises(ValidationError, match="negative assertion outcome"): + PropositionTruthResultModel.model_validate(payload) + + +def test_unknown_result_preserves_typed_conflicting_evidence_reason() -> None: + payload = _observed_result() + payload.update( + proposition_outcome="unknown", + assertion_outcome="unknown", + indeterminacy_reason="conflicting_evidence", + loss_disclosures=[{"kind": "conflicting", "within_admissible_bound": False}], + ) + + result = PropositionTruthResultModel.model_validate(payload) + assert result.indeterminacy_reason.value == "conflicting_evidence" + + +def test_unsupported_is_capability_disposition_not_false_or_unknown() -> None: + payload = _observed_result() + payload.update( + proposition_outcome="unsupported", + assertion_outcome="unsupported", + indeterminacy_reason=None, + probe_binding=None, + evidence_refs=[], + temporal_context=None, + unsupported_capability_refs=["predicate.number.less_than_or_equal"], + ) + + result = PropositionTruthResultModel.model_validate(payload) + assert result.assertion_outcome.value == "unsupported" + assert result.unsupported_capability_refs == ["predicate.number.less_than_or_equal"] + + +def test_lossy_evidence_cannot_decide_truth_outside_admitted_bound() -> None: + payload = _observed_result() + payload["loss_disclosures"] = [{"kind": "lossy", "within_admissible_bound": False}] + with pytest.raises(ValidationError, match="outside the admitted bound"): + PropositionTruthResultModel.model_validate(payload) + + +def test_distinct_backend_bindings_preserve_equivalent_semantic_results() -> None: + left = PropositionTruthResultModel.model_validate(_observed_result()) + right_payload = deepcopy(_observed_result()) + right_payload["probe_binding"] = { + "binding_id": "agentless-api-binding", + "implementation_id": "example/range-api-observer", + "implementation_version": "7.0.3", + "artifact_digest": "sha256:" + "b" * 64, + "backend_manifest_ref": "backend-manifest:range-b", + "proposition_address": "evaluation.proposition.service-available", + "capability_refs": ["predicate.string.equals", "evidence.api-response"], + } + right_payload["evidence_refs"] = ["experiment-evidence:run-42/range-api-health"] + right = PropositionTruthResultModel.model_validate(right_payload) + + assert left.semantic_claim() == right.semantic_claim() + assert left.probe_binding != right.probe_binding + + +def test_truth_result_schema_is_published_by_the_reference_generator() -> None: + schema = schema_bundle()["proposition-truth-result-v1"] + assert schema["additionalProperties"] is False + assert schema["properties"]["schema_version"]["const"] == "proposition-truth-result/v1" diff --git a/implementations/python/tests/test_version_classification.py b/implementations/python/tests/test_version_classification.py new file mode 100644 index 000000000..ba8f136e5 --- /dev/null +++ b/implementations/python/tests/test_version_classification.py @@ -0,0 +1,86 @@ +"""GOV-901 version-literal classification tests. + +The ecosystem versioning policy +(``specs/evolution/versioning-deprecation-and-migration.md``) requires version +identifiers on the Python-distribution surface to derive from the installed +distribution metadata (the release-please-owned source of truth), and to fall +back to the honest PEP 440 sentinel ``0.0.0+unknown`` when the distribution is +not installed — never a plausible-looking hard-coded release such as ``0.1.0`` +that would imply a compatibility guarantee the repository cannot honour. +""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + +from typer.testing import CliRunner + +NOT_INSTALLED_SENTINEL = "0.0.0+unknown" +DISHONEST_LITERAL = "0.1.0" + + +def test_aces_namespace_version_derives_from_distribution() -> None: + import aces + + assert aces.__version__ == version("aces-sdl") + + +def test_package_version_helper_uses_supplied_sentinel(monkeypatch) -> None: + import aces._compat as compat + + def _raise(_distribution: str) -> str: + raise PackageNotFoundError + + monkeypatch.setattr(compat, "version", _raise) + assert compat.package_version("aces-sdl", default=NOT_INSTALLED_SENTINEL) == NOT_INSTALLED_SENTINEL + + +def test_cli_version_reports_installed_distribution() -> None: + from aces_cli.main import app + + result = CliRunner().invoke(app, ["--version"]) + + assert result.exit_code == 0 + assert version("aces-sdl") in result.stdout + + +def test_cli_version_fallback_is_honest_sentinel(monkeypatch) -> None: + import aces_cli.main as cli_main + + def _raise(_distribution: str) -> str: + raise PackageNotFoundError + + monkeypatch.setattr(cli_main, "version", _raise) + result = CliRunner().invoke(cli_main.app, ["--version"]) + + assert result.exit_code == 0 + assert NOT_INSTALLED_SENTINEL in result.stdout + assert DISHONEST_LITERAL not in result.stdout + + +def test_control_plane_api_version_derives_from_distribution() -> None: + from aces_runtime.control_plane_api import _control_plane_api_version + + assert _control_plane_api_version() == version("aces-sdl") + + +def test_control_plane_api_version_fallback_is_honest_sentinel(monkeypatch) -> None: + import aces_runtime.control_plane_api as control_plane_api + + def _raise(_distribution: str) -> str: + raise PackageNotFoundError + + monkeypatch.setattr(control_plane_api, "distribution_version", _raise) + + assert control_plane_api._control_plane_api_version() == NOT_INSTALLED_SENTINEL + + +def test_control_plane_app_openapi_version_matches_distribution() -> None: + from aces_runtime.control_plane import RuntimeControlPlane + from aces_runtime.control_plane_api import create_control_plane_app + + from aces.backends.stubs import create_stub_target + + app = create_control_plane_app(RuntimeControlPlane(create_stub_target())) + + assert app.version == version("aces-sdl") diff --git a/implementations/python/tests/test_yaml_mapping_keys.py b/implementations/python/tests/test_yaml_mapping_keys.py new file mode 100644 index 000000000..35657e5da --- /dev/null +++ b/implementations/python/tests/test_yaml_mapping_keys.py @@ -0,0 +1,388 @@ +"""Fail-closed tests for authored SDL mapping keys.""" + +from pathlib import Path + +import pytest +from aces_sdl import SDLMigrationPolicy, SDLParseDiagnostic, SDLParseError, parse_sdl, parse_sdl_file +from aces_sdl.language_service import ( + apply_structured_edit, + language_completions, + language_diagnostics, + language_format, + language_references, +) +from hypothesis import given +from hypothesis import strategies as st + +FIXTURE_DIR = Path(__file__).parent / "data" / "sdl" / "invalid" + + +def _conflicts(source: str, *, migration: bool = True): + with pytest.raises(SDLParseError) as excinfo: + parse_sdl( + source, + skip_semantic_validation=True, + migration_policy=SDLMigrationPolicy.ACCEPT if migration else SDLMigrationPolicy.REJECT, + ) + diagnostics = excinfo.value.diagnostics + assert diagnostics + assert all(item.code == "sdl.mapping_key_conflict" for item in diagnostics) + return diagnostics + + +def test_exact_duplicate_root_key_is_rejected_before_model_construction() -> None: + diagnostics = _conflicts("name: first\nname: second\n") + + assert len(diagnostics) == 1 + assert diagnostics[0].pointer == "/name" + assert diagnostics[0].authored_keys == ("name", "name") + + +def test_normalized_nested_field_aliases_conflict() -> None: + diagnostics = _conflicts( + """\ +name: aliases +nodes: + sw: + Type: switch + type: switch +""" + ) + + assert diagnostics[0].pointer == "/nodes/sw/type" + assert diagnostics[0].authored_keys == ("Type", "type") + + +def test_exact_duplicate_literal_identifier_is_rejected() -> None: + diagnostics = _conflicts( + """\ +name: literal-duplicate +nodes: + sw: {type: switch} + sw: {type: switch} +""" + ) + + assert diagnostics[0].pointer == "/nodes/sw" + + +def test_literal_identifiers_are_not_field_normalized() -> None: + scenario = parse_sdl( + """\ +name: literal-identifiers +nodes: + web-app: {type: switch} + web_app: {type: switch} +""" + ) + + assert tuple(scenario.nodes) == ("web-app", "web_app") + + +def test_yaml_12_string_like_identifiers_remain_distinct_strings() -> None: + scenario = parse_sdl( + """\ +name: boolean-like-identifiers +nodes: + on: {type: switch} + "true": {type: switch} + off: {type: switch} + "false": {type: switch} +""" + ) + + assert tuple(scenario.nodes) == ("on", "true", "off", "false") + + +def test_core_resolved_non_string_mapping_key_is_rejected_with_a_source_range() -> None: + with pytest.raises(SDLParseError) as excinfo: + parse_sdl("name: invalid-key\nnodes:\n 1: {type: switch}\n") + + diagnostic = excinfo.value.diagnostics[0] + assert diagnostic.code == "sdl.mapping_key_type" + assert diagnostic.pointer == "/nodes/1" + assert diagnostic.primary_range.start.line == 3 + assert diagnostic.primary_range.start.column == 3 + + +def test_merge_source_and_local_field_must_be_disjoint() -> None: + diagnostics = _conflicts( + """\ +name: merge-conflict +nodes: + first: + type: vm + resources: &resources + ram: 1 gib + cpu: 1 + second: + type: vm + resources: + <<: *resources + cpu: 2 +""", + migration=True, + ) + + assert diagnostics[0].pointer == "/nodes/second/resources/cpu" + assert diagnostics[0].authored_keys == ("cpu", "cpu") + + +def test_merge_sources_must_be_pairwise_disjoint_and_collect_all_conflicts() -> None: + diagnostics = _conflicts( + """\ +name: merge-sources +nodes: + first: + type: vm + resources: &first + ram: 1 gib + cpu: 1 + second: + type: vm + resources: &second + RAM: 2 gib + CPU: 2 + third: + type: vm + resources: + <<: [*first, *second] +""", + migration=True, + ) + + assert [item.pointer for item in diagnostics] == [ + "/nodes/third/resources/ram", + "/nodes/third/resources/cpu", + ] + assert diagnostics[0].authored_keys == ("ram", "RAM") + + +def test_disjoint_merge_is_valid_only_in_migration_mode() -> None: + content = """\ +name: merge-disjoint +nodes: + first: + type: vm + resources: &resources + ram: 1 gib + cpu: 1 + second: + type: vm + resources: + <<: *resources +""" + with pytest.raises(SDLParseError) as excinfo: + parse_sdl(content) + assert excinfo.value.diagnostics[0].code == "sdl.noncanonical_merge" + + scenario = parse_sdl( + content, + migration_policy=SDLMigrationPolicy.ACCEPT, + ) + + assert scenario.nodes["second"].resources.cpu == 1 + assert [item.code for item in scenario.source_diagnostics] == ["sdl.noncanonical_merge"] + + +def test_cyclic_alias_graph_fails_cleanly() -> None: + with pytest.raises(SDLParseError) as excinfo: + parse_sdl( + """\ +name: cyclic-alias +nodes: &nodes + sw: + type: switch + roles: *nodes +""" + ) + + assert excinfo.value.diagnostics[0].code == "sdl.alias_cycle" + + +def test_non_printable_input_fails_cleanly_across_loader_entry_points() -> None: + with pytest.raises(SDLParseError, match="special characters are not allowed"): + parse_sdl("\x1b") + + payload = language_references("\x1b", "symbol") + assert payload["status"] == "invalid" + assert payload["diagnostics"][0]["code"] == "sdl.parse" + + +def test_collects_conflicts_in_document_order() -> None: + diagnostics = _conflicts( + """\ +Name: first +name: second +Version: 1.0.0 +version: 2.0.0 +""" + ) + + assert [item.pointer for item in diagnostics] == ["/name", "/version"] + + +def test_conflict_diagnostic_has_one_based_token_ranges() -> None: + diagnostic = _conflicts("Name: first\nname: second\n")[0] + + assert diagnostic.primary_range.as_dict() == { + "start": {"line": 2, "column": 1}, + "end": {"line": 2, "column": 5}, + } + assert diagnostic.related_range.as_dict() == { + "start": {"line": 1, "column": 1}, + "end": {"line": 1, "column": 5}, + } + assert isinstance(diagnostic, SDLParseDiagnostic) + + +def test_conflict_diagnostic_never_exposes_mapping_values() -> None: + with pytest.raises(SDLParseError) as excinfo: + parse_sdl("name: public\nname: TOP-SECRET-VALUE\n") + + assert "TOP-SECRET-VALUE" not in str(excinfo.value) + + +def test_conflict_pointer_uses_rfc_6901_escaping() -> None: + diagnostics = _conflicts( + """\ +name: escaped-pointer +nodes: + "a/b~c": {type: switch} + "a/b~c": {type: switch} +""" + ) + + assert diagnostics[0].pointer == "/nodes/a~1b~0c" + + +def test_file_entry_point_preserves_file_and_key_diagnostics(tmp_path: Path) -> None: + source = tmp_path / "scenario.yaml" + source.write_text("name: first\nname: second\n", encoding="utf-8") + + with pytest.raises(SDLParseError) as excinfo: + parse_sdl_file(source) + + assert excinfo.value.path == source + assert excinfo.value.diagnostics[0].pointer == "/name" + + +def test_language_diagnostics_preserve_structured_conflict_fields() -> None: + payload = language_diagnostics("Name: first\nname: second\n") + + assert payload["status"] == "invalid" + assert payload["diagnostics"] == [ + { + "stage": "parse", + "severity": "error", + "code": "sdl.mapping_key_conflict", + "message": "Structural field keys 'Name' and 'name' both address 'name'.", + "path": "/name", + "authored_keys": ["Name", "name"], + "range": { + "start": {"line": 2, "column": 1}, + "end": {"line": 2, "column": 5}, + }, + "related": [ + { + "message": "First authored key 'Name'.", + "range": { + "start": {"line": 1, "column": 1}, + "end": {"line": 1, "column": 5}, + }, + } + ], + } + ] + + +def test_language_references_rejects_ambiguous_documents() -> None: + payload = language_references( + "name: refs\nnodes:\n sw: {type: switch}\n sw: {type: switch}\n", + "sw", + ) + + assert payload["status"] == "invalid" + assert payload["diagnostics"][0]["code"] == "sdl.mapping_key_conflict" + + +@pytest.mark.parametrize( + "operation", + [ + lambda source: language_format(source), + lambda source: language_completions(source), + lambda source: apply_structured_edit(source, operation="set", pointer="/description", value="x"), + ], + ids=["format", "completions", "structured-edit"], +) +def test_authoring_reads_reject_ambiguity_before_rewriting(operation) -> None: + payload = operation("Name: first\nname: second\n") + + assert payload["status"] == "invalid" + assert payload["diagnostics"][0]["code"] == "sdl.mapping_key_conflict" + + +def test_imported_module_uses_the_same_mapping_key_boundary(tmp_path: Path) -> None: + imported = tmp_path / "common.yaml" + imported.write_text("name: common\nnodes:\n sw: {type: switch}\n sw: {type: switch}\n", encoding="utf-8") + root = tmp_path / "root.yaml" + root.write_text( + "name: root\nimports:\n - path: common.yaml\n namespace: shared\n", + encoding="utf-8", + ) + + with pytest.raises(SDLParseError) as excinfo: + parse_sdl_file(root) + + assert excinfo.value.path == imported + assert excinfo.value.diagnostics[0].pointer == "/nodes/sw" + + +@pytest.mark.parametrize("fixture", sorted(FIXTURE_DIR.glob("mapping-key-*.yaml")), ids=lambda path: path.stem) +def test_negative_mapping_key_fixtures_fail_closed(fixture: Path) -> None: + with pytest.raises(SDLParseError): + parse_sdl_file(fixture, skip_semantic_validation=True) + + +_STRUCTURAL_ALIAS_PAIRS = st.sampled_from( + [ + ("Password-Strength", "password_strength"), + ("PASSWORD_STRENGTH", "password-strength"), + ("password-strength", "Password_Strength"), + ] +) + + +@given(_STRUCTURAL_ALIAS_PAIRS) +def test_property_distinct_structural_aliases_never_overwrite(pair: tuple[str, str]) -> None: + first, second = pair + diagnostics = _conflicts( + f"""\ +name: generated-aliases +nodes: + host: {{type: switch}} +accounts: + alice: + username: alice + node: host + {first}: strong + {second}: weak +""" + ) + + assert diagnostics[0].pointer == "/accounts/alice/password_strength" + + +@given(st.sampled_from([("web-app", "web_app"), ("db-1", "db_1"), ("a-b", "a_b")])) +def test_property_literal_identifier_aliases_remain_distinct(pair: tuple[str, str]) -> None: + first, second = pair + scenario = parse_sdl( + f"""\ +name: generated-identifiers +nodes: + {first}: {{type: switch}} + {second}: {{type: switch}} +""" + ) + + assert set(scenario.nodes) == {first, second} diff --git a/implementations/python/uv.lock b/implementations/python/uv.lock index 58771e0d9..7b7ea43c6 100644 --- a/implementations/python/uv.lock +++ b/implementations/python/uv.lock @@ -20,16 +20,19 @@ wheels = [ [[package]] name = "aces-sdl" -version = "0.3.0" +version = "0.19.1" source = { editable = "." } dependencies = [ { name = "asyncssh" }, + { name = "blake3" }, { name = "cryptography" }, + { name = "defusedxml" }, { name = "fastapi" }, { name = "mcp" }, { name = "packaging" }, { name = "pydantic" }, { name = "pyyaml" }, + { name = "rfc8785" }, { name = "rich" }, { name = "sse-starlette" }, { name = "typer" }, @@ -56,8 +59,10 @@ docs = [ [package.metadata] requires-dist = [ { name = "asyncssh", specifier = ">=2.23.0" }, + { name = "blake3", specifier = ">=1.0.8,<2" }, { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "cryptography", specifier = ">=46.0.7" }, + { name = "defusedxml", specifier = ">=0.7.1" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "furo", marker = "extra == 'docs'", specifier = ">=2024.5.6" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" }, @@ -69,6 +74,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" }, { name = "pyyaml", specifier = ">=6.0" }, + { name = "rfc8785", specifier = ">=0.1.4,<0.2" }, { name = "rich", specifier = ">=13.0.0" }, { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.3.0" }, { name = "sphinx-autobuild", marker = "extra == 'docs'", specifier = ">=2024.4.16" }, @@ -163,6 +169,82 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "blake3" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/6a/4cc5a9dd40fd8a6d283fd3761e5f59c490109571ef8e3c73245417e5a305/blake3-1.0.9.tar.gz", hash = "sha256:5fa374fa5070ca084368776c19b420157eb0f2d3f091343d6bc59189929d62e2", size = 116872, upload-time = "2026-06-22T18:02:25.366Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/12/aa8d72228b6ff61c675bd6f55ab138a91d71499c8a707cc9fb2052f1d2b5/blake3-1.0.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f169519c7ef25ef2c446b05e2f08e7e59fae312d569f98a3134b38d4caf7abd4", size = 346253, upload-time = "2026-06-22T18:00:15.537Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/820d2f729dfe152d5ebde16390f808c762dce3f21fb764ab033803ff2b1a/blake3-1.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5e1f21b49492d01fa5a02084894c491ab9e7a1867fced107f7126c80d067c94", size = 335497, upload-time = "2026-06-22T18:00:16.942Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d6/d5462ec19a7f3d084fe327e08618fa107799ee708df04b3a2d620bd62816/blake3-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ee96daaa850700fd342a811fa10a8780fd2e8464a71b83a1779c7b6becd3dd5", size = 377621, upload-time = "2026-06-22T18:00:18.389Z" }, + { url = "https://files.pythonhosted.org/packages/92/98/dbc433f2a45be1b2344a6035d4212dfb6e6eb45046ad15103ead9c82d491/blake3-1.0.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09deb024cd75cb200e7f647cd038800e6edc8f190c8188e0c69ec1c2b920e125", size = 377495, upload-time = "2026-06-22T18:00:20.067Z" }, + { url = "https://files.pythonhosted.org/packages/e0/3d/c7a699fb60d8ed31f3f28e6aec7658d29e45ec89e7054906b3040ce3ee65/blake3-1.0.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c99afb0459c82dd13e456b6b68d45c4768b539ca998dacd3ed726f1e75e91dc", size = 451158, upload-time = "2026-06-22T18:00:21.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a1/0b1b0dbf2dd772483e372237bb65385602b019e24b67424b1fc9e5447837/blake3-1.0.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28528d1f29e6f3d45faf3482e1197e5e175730eef38bdc74e56ee11b68e0ad0d", size = 491988, upload-time = "2026-06-22T18:00:22.984Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d1/ed319477f6d263a4f6b7e9aa465b06be5235a854923edbc9ea09508b6638/blake3-1.0.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65c0c20014df687694af5ccf0cec3bdb194511da8ebd50c30b0fd55c83fa4fd5", size = 386848, upload-time = "2026-06-22T18:00:24.319Z" }, + { url = "https://files.pythonhosted.org/packages/80/3e/a4cfb269f3e0955598b415a7843c358c4f79e826e3c9118dc9fb1f101ee6/blake3-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:964b642631a3c8fe117b3439c8ae64a9a0981af9444e409656d1f1e464bfa125", size = 387842, upload-time = "2026-06-22T18:00:25.589Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/d4ee3d89eece42f86eb46663aa42702000516b7ffbc53f60b918efe95b57/blake3-1.0.9-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2fd000708662b04be211a22c1095b65fe399d7276e9f3bb2fd1ef8aacc545791", size = 384317, upload-time = "2026-06-22T18:00:26.891Z" }, + { url = "https://files.pythonhosted.org/packages/3a/aa/317106349d10de3b51332ad1e761f4864ebe887854396b75975304dcfbd1/blake3-1.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82ecade6ac425fdfc39a4371d6d9232fd6e5c28748fd8d3489016ead17407014", size = 553005, upload-time = "2026-06-22T18:00:28.246Z" }, + { url = "https://files.pythonhosted.org/packages/39/cc/7fbce61a0b24bda1aac99da674bd74ac2b687b61db071c888ffdb30cb47a/blake3-1.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b4102ba86b86c992a931b4a88c58a632d6097461e14a1e63ebd2ecb98ff0898f", size = 595086, upload-time = "2026-06-22T18:00:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/6ddc7a8b582a0871f23d6db722f4950a8918096d5fa10f9f0f992c2aea39/blake3-1.0.9-cp311-cp311-win32.whl", hash = "sha256:2f4ce45da903f3d0a7e342fa70c7cce9c10cef6b529eadb4d6213be0ab0eaf84", size = 231230, upload-time = "2026-06-22T18:00:31.247Z" }, + { url = "https://files.pythonhosted.org/packages/23/68/ea698e6df48eeb417671544cfbb18c60f863cb689306cc52f19666dd98f8/blake3-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:d819457dccfd82fe34684ec99e36725f747bd5761a0e17f537387fb31d121193", size = 220622, upload-time = "2026-06-22T18:00:32.495Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/9bdf8345c70993aaef635398f52edfb915d6e8ad2c000c801204e387c456/blake3-1.0.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a70c20542d5e7960983a0ff32999049a2b0e5ef1f22dbbbdfb51cf04828a4156", size = 344587, upload-time = "2026-06-22T18:00:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/be8b1f7f85b12bb45a0fade6ca7bdbf83a507d23d0b6141ba29fe69c8cea/blake3-1.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:72cdecf088a9d25e6ec79948a578995649b0dbee407e7a46c543a9ecc0f6f281", size = 328864, upload-time = "2026-06-22T18:00:35.59Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/66580635d744c826671fd219938caffb16281a26f62c4f856695d4233677/blake3-1.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42fa57bf462285ef16400601b0fd32214c248ba92505bbb94b1221ab9af5a092", size = 373795, upload-time = "2026-06-22T18:00:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/b5b17d3004bb81a5732c0b176c812703d200ed8c652b3b7713b9633bbe10/blake3-1.0.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b25ccde5a64be070f20e5c7a81da70292db40b164b6c77588cbd6230856badbb", size = 374183, upload-time = "2026-06-22T18:00:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/3c/63/0d209c44b2041bbe130ced12a23c92dd995fbfe5bce7ee77fffea16f5cb0/blake3-1.0.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a800b87433955f37691b5f361ad29c7dd3ee089c9cd109adc5aea8e24bc4c1f", size = 446783, upload-time = "2026-06-22T18:00:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/c5/51/efd1f9b8a9d3e9a0e235f3ced99a738529a1019fe78b3988e29d9c2fbba6/blake3-1.0.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6879739e7904b9c42afbedbcc2e8c36cebe140fb3fc3f5c492993579cf5cd516", size = 487369, upload-time = "2026-06-22T18:00:40.875Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/a8dcaea9e0b26e419a540ca0cd6203c9fbb505e85b02b03c5a59bf9e6a45/blake3-1.0.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6edeb3d49a24c307995899b70dd47aa901d0e9ad51d2f8a79aba4f074f32d8c5", size = 383845, upload-time = "2026-06-22T18:00:42.251Z" }, + { url = "https://files.pythonhosted.org/packages/f6/10/e9907f5b86410d5071982aaf05d149ca4d4fd8acab7e77eebbc9a333c7b4/blake3-1.0.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcd56a7a972c4185070f7042ccc20166927eec3c0f98b8405f375d007b604a0b", size = 383851, upload-time = "2026-06-22T18:00:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/c7863a185550706a9624f6aa7b6d46470aaed0bb46a827c5cda2a7d03151/blake3-1.0.9-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:a288664d08dee154cc496e06e62517fc9e655ecec12b0d7db538d244ac79edf1", size = 380067, upload-time = "2026-06-22T18:00:45.249Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/e7af679c719368b400c9ba9c3460072aac2ba077ddbd4bc806fef28cda03/blake3-1.0.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:91db52a809b68b5bebe7c413ddcd230e1f759398e7fa7a873104595a4fa648b6", size = 549471, upload-time = "2026-06-22T18:00:46.793Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/37c1dd3539b7bd9b6d2eef019802aacdb4a3d48ab484b140603bbf9c5b5a/blake3-1.0.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfaa671b07eb73883162ca940442193868358b0b904cfa266e4b74131ce966da", size = 591396, upload-time = "2026-06-22T18:00:48.122Z" }, + { url = "https://files.pythonhosted.org/packages/ae/55/4f0a23b72795292e74084834130900ea778c0583004519c86698dfffe1a5/blake3-1.0.9-cp312-cp312-win32.whl", hash = "sha256:ae47c3d5729ff89baa6ddf6de47fcfcc915985d39eb1bfcd6db653331f3c6fcc", size = 229271, upload-time = "2026-06-22T18:00:49.377Z" }, + { url = "https://files.pythonhosted.org/packages/12/91/7db93e4689f0f145bcb954dc62936e5f5090548a9fa20c6bbebfaeaa648a/blake3-1.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:15566065ff90ab3da46ec0be1417406f00507af902b6fb0fbc6563e77f02fc42", size = 218220, upload-time = "2026-06-22T18:00:50.659Z" }, + { url = "https://files.pythonhosted.org/packages/41/1b/95b473d649f5322e69674622a307ffdb4f0b63adb0a0adcbc5cb8a8833c2/blake3-1.0.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:69ff5aebc7650954443aa701feff2028d7c7ea5b5e18ee265f15e2104e892328", size = 343869, upload-time = "2026-06-22T18:00:51.936Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9d/adec22c719d8451af1dc9e624bf5907008ef1e0afa51aa69fd1e8c91e60e/blake3-1.0.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0cdfeff65488089ef86f7587c76055ff72b28d28d10e427b547f5711477c376d", size = 328482, upload-time = "2026-06-22T18:00:53.39Z" }, + { url = "https://files.pythonhosted.org/packages/5e/aa/0a6967ff9a6ae182419a681aed54f7338b34a1f71372e90f787a2afa42e6/blake3-1.0.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:766f1555cbe614f14f399c2fbec0983568d20edb36837ba04040807eb9e1a609", size = 373616, upload-time = "2026-06-22T18:00:54.701Z" }, + { url = "https://files.pythonhosted.org/packages/1c/51/5d4e198bf3ae902c6697ad6ec77d7210736ad8f680980e8b648dcfcd09a0/blake3-1.0.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:128a62136c9a39c7cb9fdaa5fb38471f2418853da7f5a89f31495735d0ba6f2c", size = 374149, upload-time = "2026-06-22T18:00:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/7e/62/d3c7c364925b3f10828e5137376f3947f112c32188e899b42f09c2fde98a/blake3-1.0.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1ea0bf17b184b03444007646d902207d2b4d4f3e91a0cac3836552d83db74b9", size = 446151, upload-time = "2026-06-22T18:00:57.378Z" }, + { url = "https://files.pythonhosted.org/packages/b1/01/55b89389c5036c9d24b1d762d6265e91552e10b76a3c99fece3c4a7a4783/blake3-1.0.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73a48f7e9f0e047f51a445d9b0361ab1907bdc72b6857815a84dacd2e59556f8", size = 487256, upload-time = "2026-06-22T18:00:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7a/a21b52253292ad3e4df63ea4a01ce11d3ee8f4a8a8d80eaf0c7ce92a62bd/blake3-1.0.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b27550ada40f839aca64c66127940e4318bb6ef3e291890ef913017f6f637448", size = 383977, upload-time = "2026-06-22T18:01:00.192Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/fe7188201a29ee9b042616c786a98afd864d537ca96198e64c3fe4ff13a9/blake3-1.0.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c84dbc2a31eda88b55bbf5c5b711037bf0698eba0fd1faf06bdaf313c39048", size = 383615, upload-time = "2026-06-22T18:01:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/22/08/f6a213b950e30fe9ef7d7fc061ec388e66ed62643570226882e6f7136ea3/blake3-1.0.9-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:dab59b324aa65c09e937d6c43de5de85ec9581627f4e79dcc9806d85b54a1c34", size = 380288, upload-time = "2026-06-22T18:01:03.025Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/b171e47c1b835483bcf1545ebc289458165f8dc0f5c7f74a9176d7e9af03/blake3-1.0.9-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:eca281fedcbe5c56655bd5a4176e6036eddbbe57df96114a03838fce08b1e0ca", size = 549122, upload-time = "2026-06-22T18:01:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d8/7bf71c2c85a0951e406971f151435e0751716907e3924c6c48a2d6dae0db/blake3-1.0.9-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3cbe7f190164896dc3908e920716ee66bc31d40f1a0fb603ed59ac53290fb9cf", size = 591183, upload-time = "2026-06-22T18:01:06.259Z" }, + { url = "https://files.pythonhosted.org/packages/20/85/34c3ea03cc90b2516628494ab3e0a98aec4ca8b04d037840ccd390e480ca/blake3-1.0.9-cp313-cp313-win32.whl", hash = "sha256:508ccaf8f9377cc47e6026c2897fdc37de61faeb1420dc023b6379cc2474eb65", size = 229053, upload-time = "2026-06-22T18:01:07.638Z" }, + { url = "https://files.pythonhosted.org/packages/db/2e/f09e8ed426f360aa2005206466ceab2f707486eb5d9db7051dbcbae056d1/blake3-1.0.9-cp313-cp313-win_amd64.whl", hash = "sha256:caded2806d2cbeed638c5e2517ed8b2a94165b3452fda35e72896142d22070e0", size = 217589, upload-time = "2026-06-22T18:01:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4b/b2dd7c25378a3b5de30ed908d38e6427bc4c644c0c12e8359361abd3a9ca/blake3-1.0.9-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ab0c030cf6644c30e786b0e785bde4e4596013ae9ea6ce9877e39d52383e25d7", size = 345406, upload-time = "2026-06-22T18:01:10.311Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dc/c0dab2963ddf04a4a938363f61716f9b75de6d3a9bc4a89e78f0854d4d31/blake3-1.0.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83b4a2336105af3800f7e17ac4b943f293a3927a2d66a6308d50dba944a6953e", size = 330077, upload-time = "2026-06-22T18:01:11.926Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/d03950a86d105a6332a8c422cb87658a7d247e214f1ea8f29ed09ff04e00/blake3-1.0.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95fc3545f80901b0dcd0508d16bc40f15ae39556709fa6cf86675f742d4f3c9c", size = 375147, upload-time = "2026-06-22T18:01:13.198Z" }, + { url = "https://files.pythonhosted.org/packages/10/75/711b1842e0a90aaad6a1c9a9022e90aa16206ac1f224516118bc24482532/blake3-1.0.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1bd981dc318c05375c3160a99df493b7cc4c83fffa1a34d14b18a071b47b262b", size = 373711, upload-time = "2026-06-22T18:01:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a0/f512799d1d0c0b4718fa6f0e99ccbe108e98bac7bf82c200803a62b57876/blake3-1.0.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:689a7e4069de681d9c5d9445b8b6473ee880ad04d7960a6789c60bd788980250", size = 446993, upload-time = "2026-06-22T18:01:15.924Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/6636ae8a46fc3352694188f5a5a325567782bc88fd1823b0b67be2c92184/blake3-1.0.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8adb0b0032e53919ee95b3d4f911448d3268316c28cd7df232ff2a1e7c9a4ba4", size = 488478, upload-time = "2026-06-22T18:01:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c5/a2b3c086f7e37c9db6017dc2890a76ad2a729e4a554896e855e511811e6b/blake3-1.0.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32bd4521ec2d477627ad93eb70f9ac4d01e12d1489024159bcaeff79466332f6", size = 384900, upload-time = "2026-06-22T18:01:18.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b8/1298806dd6c464a6f807df24c9640ad3bf27ee54ff4de82b2b5a823a8aba/blake3-1.0.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65d77eb05331495485048f6804f53885b192b998acb7e6fe1487d941bf08435", size = 384333, upload-time = "2026-06-22T18:01:20.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cc/0c29d9404155adfd6db716e9765d36ea6cbed287060759f5d764f0d9d99e/blake3-1.0.9-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ca7dfe8fb197ff8a3f5c915424183ccd52a99e8afb12680f51b2e1f4c9c6c97f", size = 381142, upload-time = "2026-06-22T18:01:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/d6/91/9af20d563f0ced71e08a60fc0ee534146da4e265710ed6792d5d799f4c0f/blake3-1.0.9-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f5c9d57f0dcb92243b6ae575c3065793edc9df9008d0ebd98d8245cdeb7c3f84", size = 550587, upload-time = "2026-06-22T18:01:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fa/06f46fc0aa486b799d776f9a80ed0b3605e2be1570cf48007860948aa5d9/blake3-1.0.9-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:172d44245a19dfec08ab771c1b7a506b97783163cdc65f559fe020007e403c99", size = 591888, upload-time = "2026-06-22T18:01:24.805Z" }, + { url = "https://files.pythonhosted.org/packages/50/68/d6198f4069a7c4a184ed854df45b82cc3e2d4b0be476b2a3ee65ad2344cf/blake3-1.0.9-cp314-cp314-win32.whl", hash = "sha256:249e5964fa9e768924bc7cc3d4efe75a425bb5dd3fb7671c3eda8eeddfa50591", size = 229410, upload-time = "2026-06-22T18:01:26.24Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/f29af72a8312b3827b50e55491f1bf9ae2347591de5c47365c5cbd2525a9/blake3-1.0.9-cp314-cp314-win_amd64.whl", hash = "sha256:0aba416bb2e3ef0c65e74d5eba21062483c714cd78e7e303c9d03c547fc7d015", size = 218526, upload-time = "2026-06-22T18:01:27.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/7e/d932fe437ccf656cfba77abc466fb3d1a0ce3c31df92e760d9e4c34932b4/blake3-1.0.9-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5b35abe24a66a7b3db423eb4f8668ed7be1a362aa9c0024ab6483ec0b2c16058", size = 345049, upload-time = "2026-06-22T18:01:29.228Z" }, + { url = "https://files.pythonhosted.org/packages/55/1e/d92fb284fcacf86f5d1083e29d0a8c834b60432786928915238d9760f514/blake3-1.0.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bbdff61e049297ef3180867ce1f079cea7e5b372fd76953c3183da5b8124206", size = 329367, upload-time = "2026-06-22T18:01:30.566Z" }, + { url = "https://files.pythonhosted.org/packages/9d/da/e25fa75d5bfea4527fc21024dde86a9376db798e469a084741968299f215/blake3-1.0.9-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a69fcedf06785bb81d4d3d39f95ee65dbaf2cb246e174cfc9ff64d027f7551", size = 374203, upload-time = "2026-06-22T18:01:31.998Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4d/0224916202b773dfdf08dcbe4ed1ad1018d4ddcd4df7a7e2978d28f89b74/blake3-1.0.9-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5d5bf0f68cd77108a942c95db98e960d9c3d5643b95172f783822ce22667759", size = 373713, upload-time = "2026-06-22T18:01:33.387Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e5/4ba968831b7afaec431c588c826cef76a96d6d6976188ed07d932072e673/blake3-1.0.9-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9767f16199b99aa022b61ff825ac4dbd39864bf637ae712605a2ce1f8b6a55e0", size = 446574, upload-time = "2026-06-22T18:01:34.687Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f5/08a9099c7177f282d2563abe4f7cc626c636642f7979cf58f2ab7ded2096/blake3-1.0.9-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865a8cfb2b3d7c0baf5267f2fa6816a3384e836cd1bd0caf359f406cb1e8fba", size = 487232, upload-time = "2026-06-22T18:01:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/9392bf1ebc81b5b09ce58b94613fa2d37308e825ff2dc7b54d00ee622c77/blake3-1.0.9-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42609e4adc4b2d7423137f2cb35135bca598b925c5af09d2bc0a2c368b25aeb1", size = 384751, upload-time = "2026-06-22T18:01:37.512Z" }, + { url = "https://files.pythonhosted.org/packages/84/fc/b6e9aef02ca14ef62fa47783b9eeeb5b2d3f73fdf698d8bb94c36f5dd69f/blake3-1.0.9-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7f648fa425138452d1e585ac625c7aefddb946d9765906c4c12d564a1523cd8", size = 384546, upload-time = "2026-06-22T18:01:38.868Z" }, + { url = "https://files.pythonhosted.org/packages/ff/cb/452e92dba9402b36a953aa8b9b06253445ccce43dcd0bcf521c5e3c3e15d/blake3-1.0.9-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:9cef6d4d07a7de0c44f5ba17f6383d55276d9efc8d601f75113538fcaa35008b", size = 380596, upload-time = "2026-06-22T18:01:40.412Z" }, + { url = "https://files.pythonhosted.org/packages/b2/01/7a84a7e10c5d14e6ed8a4403bd7f64c1e01f8ebabea0d6fe5f093b894cbd/blake3-1.0.9-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:28404301de485e9546365d01b30f65eaa835520c4211d6ef61242975b6722b60", size = 550032, upload-time = "2026-06-22T18:01:41.955Z" }, + { url = "https://files.pythonhosted.org/packages/58/7d/7aea0222f59cf84044ec52e2bfdaa0e3c355d221292b0ea1b722cf1edd6c/blake3-1.0.9-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8a99f896e7718050ed033a888245098aab3d6a5338f91cc9450c563b53f90ad5", size = 592244, upload-time = "2026-06-22T18:01:43.426Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e5/b44c230108745ff9c70c7bbafe22563772bc0c22322a8d15c10455f6ca02/blake3-1.0.9-cp314-cp314t-win32.whl", hash = "sha256:021309d760b390706fecf13498f9a25aa8f689bbb65a0896029b8fa223aae18b", size = 229481, upload-time = "2026-06-22T18:01:45.307Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/ac03f37dc9aeebf398d42089720648b3bc8438e733d3e522196c5d12ab39/blake3-1.0.9-cp314-cp314t-win_amd64.whl", hash = "sha256:5ea0c60dd9c1e3d05610606579e4bf80f562854c46ed55f9ee8545e18987a480", size = 217979, upload-time = "2026-06-22T18:01:46.629Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -507,6 +589,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "docutils" version = "0.22.4" @@ -1184,6 +1275,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] +[[package]] +name = "rfc8785" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/2f/fa1d2e740c490191b572d33dbca5daa180cb423c24396b856f5886371d8b/rfc8785-0.1.4.tar.gz", hash = "sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da", size = 14321, upload-time = "2024-09-27T16:33:31.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/78/119878110660b2ad709888c8a1614fce7e2fab39080ab960656dc8605bf6/rfc8785-0.1.4-py3-none-any.whl", hash = "sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48", size = 9240, upload-time = "2024-09-27T16:33:29.683Z" }, +] + [[package]] name = "rich" version = "14.3.3" diff --git a/noxfile.py b/noxfile.py index eb21fbc68..308d69e20 100644 --- a/noxfile.py +++ b/noxfile.py @@ -521,6 +521,10 @@ def _run_policy(session: nox.Session, reporter: SessionReporter, *args: str) -> "policy / authority boundary ADR", "skipped on staged check; runs on push and verify", ) + reporter.skip( + "policy / deprecation lifecycle records", + "skipped on staged check; runs on push and verify", + ) reporter.skip( "policy / concept authority governance", "skipped on staged check; runs on push and verify", @@ -550,6 +554,10 @@ def _run_policy(session: nox.Session, reporter: SessionReporter, *args: str) -> "policy / authority boundary ADR", lambda: _run_project_python(session, "tools/check_authority_boundary.py"), ) + reporter.run( + "policy / deprecation lifecycle records", + lambda: _run_project_python(session, "tools/check_deprecation_lifecycle.py"), + ) reporter.run( "policy / concept authority governance", lambda: _run_project_python(session, "tools/check_concept_authority_governance.py"), @@ -603,6 +611,18 @@ def _run_contracts(session: nox.Session, reporter: SessionReporter, *args: str) "contracts / generated schema drift", lambda: _run_project_python(session, "tools/check_generated_schemas.py"), ) + reporter.run( + "contracts / SDL catalog parity", + lambda: _run_project_python(session, "tools/check_sdl_catalog_parity.py"), + ) + reporter.run( + "contracts / SDL lineage provenance", + lambda: _run_project_python(session, "tools/check_sdl_lineage.py"), + ) + reporter.run( + "contracts / scientific-scenario completeness", + lambda: _run_project_python(session, "tools/check_scientific_scenario_completeness.py"), + ) reporter.run( "contracts / json artifact validation", lambda: _run_project_python(session, "tools/check_json_artifacts.py", *json_artifact_args), diff --git a/specs/README.md b/specs/README.md index 6abdc3677..090958390 100644 --- a/specs/README.md +++ b/specs/README.md @@ -36,6 +36,8 @@ hook). `prose` family of the `specs/` root. - `concept-authority/` — concept-family and controlled-vocabulary authority artifacts (governed by ADR-012) +- `evolution/` — ecosystem versioning, deprecation, removal, and migration + governance for GOV-901, GOV-902, and GOV-903 (governed by ADR-075) - `sdl/` — the language-neutral normative SDL authoring specification (the catalog set the published `contracts/schemas/sdl/` schemas must agree with; governed by ADR-001 and ADR-009) @@ -44,3 +46,6 @@ hook). - `supply-chain/` — normative prose for the Packaging & Supply Chain wave, including the reusable-asset trust/authenticity/integrity policy (GOV-913, governed by ADR-071) +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/agent-guidance/agent-guidance.yaml b/specs/agent-guidance/agent-guidance.yaml index c97995236..a08a68a1d 100644 --- a/specs/agent-guidance/agent-guidance.yaml +++ b/specs/agent-guidance/agent-guidance.yaml @@ -21,10 +21,12 @@ source_refs: - docs/explain/reference/documentation-style-guide.md - docs/explain/reference/normative-artifact-authority.md - docs/explain/sdl/language-service.md + - specs/sdl/scientific-scenario-completeness.md recommended_workflow: - aces_tool_surface - aces_agent_guidance + - aces_intended_use_profiles - sdl_overview - sdl_section_reference - sdl_completions @@ -73,6 +75,20 @@ guidance: - implementations/python/packages/aces_mcp/tools/operations.py - docs/decisions/adrs/adr-021-falsification-first-claim-evidence-gate.md + - id: valid-sdl-not-scientific-completeness + audience: [contributor, operator] + surfaces: [mcp, sdl, assessment, docs] + statement: >- + Structural and semantic SDL validity establishes only the valid-fragment + profile. Do not describe a scenario as deployable, participant-ready, + controlled, reproducible, conformant, or behaviorally equivalent unless + the separately versioned scientific-completeness assessment supports + that exact claim and its required external bindings. + source_refs: + - specs/sdl/scientific-scenario-completeness.md + - contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json + - contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json + invariants: - id: validate-before-return audience: [contributor, operator] @@ -154,6 +170,7 @@ guidance: source_refs: - docs/decisions/adrs/adr-021-falsification-first-claim-evidence-gate.md - docs/explain/sdl/limitations.md + - specs/sdl/scientific-scenario-completeness.md - id: canonical-verification-graph audience: [contributor] diff --git a/specs/authority/README.md b/specs/authority/README.md index 7328782a6..40ca46961 100644 --- a/specs/authority/README.md +++ b/specs/authority/README.md @@ -12,7 +12,7 @@ decided in prose. `contracts/schemas/`, `contracts/fixtures/`, `contracts/profiles/`, `contracts/concept-authority/`), each non-normative root (`implementations/`, `docs/`, `examples/`, `research/`, `notes/`, - `tools/`, `changelog.d/`), the legacy top-level directories ADR-009 + `tools/`), the legacy top-level directories ADR-009 transitioned out (`schemas/`, `conformance/`, `src/`), the schema-authority direction (no published schema may live under `implementations/`), and the `normative_artifact_families` block described diff --git a/specs/authority/authority-boundary.yaml b/specs/authority/authority-boundary.yaml index 6ed5b04f3..9b469c7f5 100644 --- a/specs/authority/authority-boundary.yaml +++ b/specs/authority/authority-boundary.yaml @@ -57,14 +57,24 @@ authority_roots: - id: normative_profiles root: contracts/profiles/ - authority: capability profile declarations + authority: capability and intended-use profile declarations family: profiles + - id: normative_realization_envelopes + root: contracts/realization-envelopes/ + authority: configuration-bound backend realization envelope declarations + family: realization-envelopes + - id: normative_concept_authority root: contracts/concept-authority/ authority: concept-family and controlled-vocabulary authority artifacts family: concept-authority + - id: normative_provenance + root: contracts/provenance/ + authority: revision-pinned SDL lineage and third-party provenance records + family: provenance + # Roots that are non-normative. These consume authority; they do not define it. # A reference implementation, an explanatory doc, a worked example, a research # note, and the tooling tree all live here. Adding a new top-level directory @@ -94,10 +104,6 @@ non_normative_roots: root: tools/ note: maintenance, codegen, and release helpers - - id: changelog_fragments - root: changelog.d/ - note: towncrier changelog fragments; collated into CHANGELOG.md at release - # 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: @@ -142,3 +148,10 @@ normative_artifact_families: family: governance-guidance requirement_refs: - AUT-811 + + - id: deprecation_records + artifact: specs/evolution/deprecation-records.yaml + authority: ecosystem deprecation and lifecycle records (GOV-902) + family: deprecation-records + requirement_refs: + - GOV-902 diff --git a/specs/evolution/README.md b/specs/evolution/README.md new file mode 100644 index 000000000..5cef79f50 --- /dev/null +++ b/specs/evolution/README.md @@ -0,0 +1,16 @@ +# Evolution Governance + +This directory contains normative ACES ecosystem policy for artifact +evolution: versioning, compatibility, deprecation, removal, and migration. + +The governing decision is +[ADR-075](../../docs/decisions/adrs/adr-075-ecosystem-versioning-deprecation-and-migration-governance.md). + +## Files + +- [`versioning-deprecation-and-migration.md`](versioning-deprecation-and-migration.md) + defines the normative surface-class matrix and rules for GOV-901, GOV-902, + and GOV-903. +- [`deprecation-records.yaml`](deprecation-records.yaml) is the reviewable + record surface for GOV-902 deprecation and lifecycle notices, validated by + `tools/check_deprecation_lifecycle.py` against the complete-record contract. diff --git a/specs/evolution/deprecation-records.yaml b/specs/evolution/deprecation-records.yaml new file mode 100644 index 000000000..16ca29014 --- /dev/null +++ b/specs/evolution/deprecation-records.yaml @@ -0,0 +1,63 @@ +# Ecosystem Deprecation Records (GOV-902) +# +# Canonical, reviewable record surface for repository-governed deprecation and +# removal notices across ecosystem surfaces. Required reading: +# +# specs/evolution/versioning-deprecation-and-migration.md +# -- the normative operational policy (Deprecation / Removal sections); +# each record below satisfies the complete-record contract it defines +# docs/decisions/adrs/adr-075-ecosystem-versioning-deprecation-and-migration-governance.md +# -- the architectural decision that mandates explicit deprecation records +# +# This file is a CI-time GOVERNANCE record surface, not a runtime lifecycle +# registry, migration service, persistence layer, or endpoint (ADR-075 forbids +# those). tools/check_deprecation_lifecycle.py validates every record against +# the 7-field contract and fails closed on an incomplete record, so +# supersession and removal stay predictable and reviewable (GOV-902). Each +# record points back to its owning surface's existing authority; it does not +# replace that authority. A deprecation is a lifecycle notice for a +# still-supported surface (status: deprecated); removal (status: removed) is +# allowed only after a record reaches its removal-eligibility rule, and for a +# published-json-schema surface it references the ADR-061 removed_schemas +# tombstone rather than duplicating it. +# +# Adding the next deprecation is one record here plus, when it is a new surface +# family, a row in the surface-class matrix — never a central runtime service. + +policy: ecosystem-deprecation-records + +requirement_refs: + - GOV-902 + +adr_refs: + - ADR-075 + +spec: specs/evolution/versioning-deprecation-and-migration.md + +records: + # The legacy `aces.*` re-export namespace. ADR-010 treats it as + # compatibility-only and one-directional; the spec (migration section) blesses + # it staying in the compatibility tree. It carried no complete lifecycle + # record until GOV-902. + - id: aces-compat-namespace + surface_class: python-distribution + identifier: "implementations/python/src/aces/ (the legacy `aces.*` re-export namespace)" + status: deprecated + first_notice: "ADR-010 (repository realignment order and compatibility policy)" + replacement: "the owning packages under implementations/python/packages/ (aces_sdl, aces_contracts, aces_cli, aces_runtime, and siblings)" + migration_reference: "docs/decisions/adrs/adr-010-repository-realignment-order-and-compatibility-policy.md; docs/explain/sdl/runtime-architecture.md" + notice_window: "Supported through the pre-1.0 package series; removal eligible no earlier than the first major (post-1.0) package release and only via a complete removal record." + verification_evidence: "tools/check_repo_policy.py enforces the compatibility-wrapper-only rule for aces.* modules and the one-way import ban (owning packages must not import aces.*), proving the layer remains a supported wrapper during the notice window." + + # 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. + - id: sdl-import-path-field + surface_class: sdl-scenario-module + identifier: "ImportDecl.path (the module-import `path:` field)" + status: deprecated + first_notice: "ADR-053 (SDL module composition for inventory-backed scenarios)" + replacement: "the `source:` field (path form `local:{path}`)" + migration_reference: "docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md; docs/explain/sdl/parser.md" + notice_window: "Supported indefinitely as a backward-compatible alias; removal eligible only after a complete removal record and a deterministic source rewrite per the migration rules." + verification_evidence: "aces_sdl.scenario.ImportDecl.normalized_source maps a `path:` value to `local:{path}`, and validate_source_fields keeps deprecated-but-supported use non-fatal; parser tests exercise both the `source:` and `path:` forms." diff --git a/specs/evolution/versioning-deprecation-and-migration.md b/specs/evolution/versioning-deprecation-and-migration.md new file mode 100644 index 000000000..320f9777b --- /dev/null +++ b/specs/evolution/versioning-deprecation-and-migration.md @@ -0,0 +1,203 @@ +# Versioning, Deprecation, and Migration Governance + +This specification is normative for ACES ecosystem evolution policy. It +implements the joint design surface for GOV-901, GOV-902, and GOV-903. + +## Scope + +This policy governs versioning, compatibility claims, deprecation records, +removal eligibility, and migration guidance for repository-governed ecosystem +surfaces. + +It does not create a universal runtime version registry, cross-package +migration service, API endpoint, database table, or exception hierarchy. Each +surface remains owned by its existing authority, checker, schema, validator, +or adapter boundary. + +## Terms + +**Version identifier** means a surface-local value that identifies a release, +lineage, payload shape, source artifact, or domain artifact. + +**Producer** means the surface that writes, publishes, emits, or serves an +artifact. + +**Consumer** means the surface that reads, imports, validates, executes, or +otherwise depends on the artifact. + +**Backward compatibility** means a newer consumer accepts and preserves the +meaning of older producer output for the named surface and dimension. + +**Forward compatibility** means an older consumer accepts and safely handles +newer producer output for the named surface and dimension. + +**Full compatibility** means both backward and forward compatibility hold for +the named surface and dimension. + +**Structural acceptance** means the consumer accepts the artifact's syntactic +or schema shape. + +**Semantic equivalence** means accepted input preserves the same ACES meaning. + +**Behavioral compatibility** means runtime, CLI, API, validation, or +conformance behavior remains within the documented contract. + +**Operational interoperability** means independently released components can +work together under documented admission, capability, and deployment rules. + +## Surface-Class Matrix + +Each surface class owns its version meaning and compatibility rule. + +| Surface class | Authority | Identifier syntax | Compatibility relation | Lifecycle record | Migration evidence | +|---|---|---|---|---|---| +| Python distribution | `implementations/python/pyproject.toml`, release-please config, Git tag, PyPI artifact | SemVer package version and matching `vX.Y.Z` tag | Consumer code imports installed package APIs for the documented release | Release PR, GitHub Release, generated `CHANGELOG.md` | Release notes and package/API docs | +| Published JSON Schema | ADR-061 and `contracts/schema-publication-manifest.json` | Contract id such as `backend-manifest-v2`; wire discriminator such as `backend-manifest/v2` | Schema lineage and stability-specific structural compatibility | Manifest `stability`, `last_change`, and `removed_schemas` tombstones | Schema diff, fixtures, checker output, and contract docs | +| Closed contract DTO and wire envelope | `aces_contracts.versions`, contract models, manifest authority | Exact discriminator such as `workflow-step-state/v1` | Exact payload-shape selection by owning reader | Contract ADR/spec and release notes | Reader validation tests and conformance fixtures | +| Processor/backend support declaration | Manifest models and manifest authority allowlists | Exact governed contract ids, not version ranges | Declared support for named counterpart contract ids | Manifest contract lineage and conformance result | Manifest update, fixture, and conformance report | +| Apparatus or implementation identity | Processor, backend, and participant manifests | Product/component identity version string | Operational interoperability declared by manifest capability blocks | Manifest release and conformance profile | Backend/processor/participant conformance evidence | +| SDL scenario and module | SDL spec, module registry, lockfile, trust policy | Scenario/module version and import version constraint | Import constraint satisfaction plus registry, digest, signature, and semantic validation | SDL/module documentation and release notes | Migration note or deterministic source rewrite | +| Experiment task, run, study, evidence, or domain artifact | Owning experiment-core spec and contract | Domain-specific artifact version | Domain semantic or provenance compatibility | Owning ADR/spec and contract lineage | Domain fixtures, replay/evidence checks, or migration note | +| ADR | ADR-000 and ADR-059 | ADR number plus status | Citable accepted content, not runtime compatibility | Status, pin, amendment row, or supersession | New ADR or recorded amendment | +| Ground Control requirement | Ground Control requirement graph | Requirement UID and status | Traceability/workflow state, not artifact compatibility | Requirement status and IMPLEMENTS/TESTS/DOCUMENTS links | Post-merge traceability reconciliation | + +New surface classes extend this table and their owning checker or +documentation. They do not create a central runtime registry by default. + +## Compatibility Claims + +A compatibility claim is valid only when it names: + +- the surface class; +- producer and consumer; +- direction: backward, forward, or full; +- dimension: structural, semantic, behavioral, or operational; +- version or lineage range under discussion; and +- verification evidence. + +A bare statement that a change is "compatible" is incomplete. + +Structural acceptance is not semantic compatibility. A JSON Schema accepting +an object does not prove the object preserves SDL meaning, conformance +behavior, or runtime interoperability. + +For published JSON Schemas, ADR-061 remains authoritative. Optional properties, +enum additions, and looser constraints can be additive to schema structure, but +they are not automatically forward-compatible with older closed readers. A PR +claiming end-to-end compatibility for such a change must show reader evidence +or scope the claim to structural schema compatibility only. + +Before the Python package reaches 1.0, release-please may classify breaking +changes as minor releases according to the repository's release configuration. +Consumers must not infer post-1.0 SemVer stability from a `0.y.z` package +release unless a specific surface policy states it. + +## Deprecation + +A deprecation is a lifecycle notice for a still-supported surface. It is not a +schema stability class, ADR status, requirement status, or semantic validation +error by itself. + +A complete deprecation record names: + +- exact surface and identifier; +- first release, contract lineage, or documentation version carrying the + notice; +- replacement surface or explicit no-replacement rationale; +- migration reference; +- minimum notice window or removal eligibility rule; +- verification evidence that the old surface remains supported during the + notice window; and +- security exception, if the ordinary notice window is shortened. + +Deprecated-but-supported use must remain non-fatal. The owning channel may emit +a bounded notice, advisory, warning, release-note entry, or documentation +marker. Actual removal or unsupported use fails through the owning surface's +existing SDL, contract, CLI, conformance, or HTTP error envelope. + +Security exceptions may shorten ordinary notice only when the record names the +affected versions, impact, mitigation, migration path, and review authority. +The word "security" is not a blanket bypass for unreviewed breaking changes. + +Repository-governed deprecation records are recorded in +`specs/evolution/deprecation-records.yaml` and validated by +`tools/check_deprecation_lifecycle.py`, which fails closed on any record that +omits a required field. That ledger is the single reviewable surface for these +notices — a CI-time governance record, not a runtime lifecycle registry, +migration service, or endpoint — and each record cites its owning surface's +existing authority rather than replacing it. A record whose `status` is +`removed` on a published JSON Schema surface references the ADR-061 +`removed_schemas` tombstone rather than duplicating it. + +## Removal + +Removal is allowed only after a complete deprecation record reaches its removal +eligibility rule, or under a documented security exception. + +Published JSON Schema removal follows ADR-061 and must leave the required +manifest tombstone. Other surface removals use their owning evidence: +release notes, ADR/spec updates, fixtures, tests, conformance reports, or +diagnostics. + +Removal must not silently reinterpret old input as new input. If a removed +surface appears, the owning reader fails with a bounded, surface-specific +message and, when available, points to the migration reference. + +## Migration + +Migration guidance is surface-specific and version-pair oriented. A migration +record names: + +- source surface and version or lineage; +- target surface and version or lineage; +- transformation type: manual, assisted, or automated; +- data preservation rules; +- ambiguous or lossy cases; +- validation command or evidence; and +- rollback or no-rollback statement. + +Human-readable migration notes belong under the existing documentation +boundary. Automated migrators are justified only for deterministic +transformations. They must be idempotent, preserve source data, report +ambiguous or lossy cases, and fail closed rather than drop unknown fields. + +Compatibility adapters stay at the owning boundary: + +- legacy `aces.*` re-exports stay in the compatibility tree; +- SDL normalization and module composition stay in `aces_sdl`; +- contract readers stay in `aces_contracts`; +- CLI presentation stays in `aces_cli`; and +- backend/runtime layers do not reinterpret authored source to hide migration + needs. + +## Verification + +Changes governed by this policy use the repository's existing gates unless a +surface adds a more specific checker: + +- release classification and version bump evidence: PR title guard, + release-please config, release PR, Git tag, GitHub Release, and PyPI + artifact; +- version-identifier derivation: the docs build, CLI `--version`, and + control-plane OpenAPI description versions derive from the installed + distribution metadata (the release-please-owned source of truth) and fall + back to the honest PEP 440 `0.0.0+unknown` sentinel when the distribution is + not installed, rather than a hard-coded literal that would imply an + unearned release; verified by version-classification tests; +- published schema evolution: schema publication checker, generated-schema + parity, fixture validation, manifest hashes, change ledger, and tombstones; +- ADR evolution: ADR index and accepted-content pin gate; +- deprecation and lifecycle records: `specs/evolution/deprecation-records.yaml` + validated by `tools/check_deprecation_lifecycle.py` (complete-record contract, + removal-eligibility, and — for published schemas — a required ADR-061 + `removed_schemas` tombstone reference); +- SDL/module evolution: parser, validator, module registry, lockfile, trust, + signature, digest, and semantic tests; +- contract and manifest evolution: closed DTO validation, manifest authority, + fixtures, profiles, and conformance checks; and +- repository completion: `tools/check_repo_policy.py`, + `tools/check_requirement_governance.py`, `tools/verify_all.py`, and the + canonical nox verification graph. + +No secret, environment dump, credential, or private configuration is required +to validate this policy. diff --git a/specs/formal/README.md b/specs/formal/README.md index 8b6bb7220..934ce3318 100644 --- a/specs/formal/README.md +++ b/specs/formal/README.md @@ -15,6 +15,7 @@ Examples: - `specs/formal/participant-runtime/` - `specs/formal/experiment-core/` - `specs/formal/validation-admission-profiles/` +- `specs/formal/sdl-phases/` Cross-domain semantic notes that constrain future phases may also live at the top level when they apply across multiple domains, for example diff --git a/specs/formal/assurance-fulfillment.yaml b/specs/formal/assurance-fulfillment.yaml index 4377c05ca..7e699e275 100644 --- a/specs/formal/assurance-fulfillment.yaml +++ b/specs/formal/assurance-fulfillment.yaml @@ -26,6 +26,7 @@ policy_ref: specs/formal/assurance-policy.yaml adr_refs: - ADR-007 - ADR-018 + - ADR-078 # Registry of classified formal-spec subsystems. Every immediate subdirectory of # specs/formal/ carrying a README.md must appear here (the checker fails on any @@ -64,6 +65,9 @@ subsystems: - id: validation-admission-profiles path: specs/formal/validation-admission-profiles fm_level: FM2 + - id: sdl-phases + path: specs/formal/sdl-phases + fm_level: FM2 # Fulfillment, keyed by subsystem id. delivered_artifacts name a concrete, # non-empty repo path that is the named kind of evidence. waived_artifacts record @@ -351,3 +355,15 @@ entries: rationale: >- No property-based or differential validation-basis coverage ships with this design-only issue. + + - subsystem: sdl-phases + delivered_artifacts: + - kind: invariant_list + path: specs/formal/sdl-phases/README.md + - kind: unit_tests + path: implementations/python/tests/test_sdl_phase_contracts.py + - kind: typed_ir_or_contract_coverage + path: implementations/python/packages/aces_sdl/phase_contracts.py + - kind: property_based_or_differential_tests + path: implementations/python/tests/test_sdl_phase_contracts.py + waived_artifacts: [] diff --git a/specs/formal/experiment-core/README.md b/specs/formal/experiment-core/README.md index f69dcc3e5..62ba5ecc7 100644 --- a/specs/formal/experiment-core/README.md +++ b/specs/formal/experiment-core/README.md @@ -13,6 +13,7 @@ reproducibility/replay claim-support interpretation: - `experiment-capture-spec-v1` - `experiment-evidence-record-v1` - `experiment-derived-measure-v1` +- `experiment-authoring-input-v1` (pre-run authoring input; ADR-074) - optional `backend-manifest-v2` `capabilities.observation` - canonical run traceability and realized-form disclosures inside `experiment-run-v1` @@ -20,6 +21,12 @@ reproducibility/replay claim-support interpretation: The contracts describe cyber range experiment artifacts. They do not implement execution, storage, scheduling, APIs, or analysis engines. +`experiment-authoring-input-v1` (ADR-074) is the one input contract in this +domain: the pre-run experiment *specification* that binds a task to a run plan +before execution. It is the authoring counterpart to the archival run, study, +and apparatus-context outputs, analogous to how `sdl-authoring-input-v1` is the +authored counterpart to `instantiated-scenario-v1`. + ## FM Classification Classification: FM2, Semantic Graph / Constraint. @@ -45,7 +52,9 @@ Rationale: and `docs/decisions/adrs/adr-065-experiment-run-provenance-contract-boundary.md`, and - `docs/decisions/adrs/adr-068-experiment-trials-replication-and-replay-claims.md`. + `docs/decisions/adrs/adr-068-experiment-trials-replication-and-replay-claims.md`, + and + `docs/decisions/adrs/adr-074-experiment-authoring-input-contract-boundary.md`. - Machine-readable schemas: `contracts/schemas/experiment-core/`. - Contract source: `implementations/python/packages/aces_contracts/contracts.py`. - Schema generation: `tools/generate_contract_schemas.py`. @@ -288,6 +297,32 @@ context, selected manifests, capability declarations, measurement channels, task/scenario snapshot identity, non-opaque parameters, and stochastic controls. +### Experiment Authoring Input + +An experiment authoring input is the pre-run *specification* of an experiment, +distinct from the archival run/study/apparatus-context records it will later +produce. It binds: + +- a `task_ref` to a separately authored `experiment-task-v1`, and optionally an + `intended_scenario_ref` to a scenario snapshot; +- declared apparatus intent (the input-shaped apparatus constraints), distinct + from the run-scoped observed apparatus context; +- a run plan: stochastic controls (seeds), an episode control (turn order, + logical step count, termination), either a condition `allocation` plan or a + scalar `target_run_count`, red-variant selections keyed by variant id, and an + optional clock intent; +- study factors keyed by factor id; +- capture-specification references, validity notes, and supporting artifacts. + +An authoring input is not a run, study, or apparatus-context record. It carries +no execution provenance, results, or observed evidence. It reuses the archival +family's input-shaped value models (apparatus constraints, run allocation, +stochastic controls, factors, clock context) by reference or embedding and does +not re-declare task, scenario, or capture-specification meaning. The reused run- +allocation model annotates its semantic invariant against `experiment-study-v1` +even when embedded here; that annotation is descriptive only — the ACES model +validator runs regardless of the embedding contract. + ## Invariants ### Separation @@ -375,6 +410,20 @@ controls. claim/report refs. ACES MUST NOT publish parallel replay-run, reproducibility-claim, replay-claim, or provenance-graph root schemas for the same facts unless a later ADR supersedes this boundary. +24. An experiment authoring input MUST reference its task with a `task` + reference and MUST NOT embed or re-declare task, scenario, or + capture-specification meaning; scenario intent uses `scenario` or + `scenario-snapshot` references and capture intent uses `capture-spec` + references. +25. An experiment authoring input run plan MUST declare exactly one run-count + source: either a condition `allocation` plan or a scalar `target_run_count`. + Red-variant selection map keys MUST match their embedded `variant_id`, and + declared allocation blocking factors MUST resolve to declared spec factors. +26. An experiment authoring input is an input artifact. It MUST NOT be treated + as, or substituted for, an `experiment-run-v1`, `experiment-study-v1`, or + `experiment-apparatus-context-v1` record. Its declared apparatus intent uses + the input-shaped apparatus constraints, not the run-scoped observed + apparatus context. ### Provenance @@ -531,3 +580,5 @@ base. The most load-bearing criteria are: graph root schemas for facts already carried by experiment-core contracts. - Runtime replay execution, replay scheduling, artifact dereference APIs, retention storage, or query services. +- Orchestration or execution of authored experiment specifications, or + producing archival run/study records from an `experiment-authoring-input-v1`. diff --git a/specs/formal/objectives/declarative-objective-semantics.md b/specs/formal/objectives/declarative-objective-semantics.md index d8ed1702f..9b8994764 100644 --- a/specs/formal/objectives/declarative-objective-semantics.md +++ b/specs/formal/objectives/declarative-objective-semantics.md @@ -8,7 +8,7 @@ Objective semantics are declarative SDL meaning. They answer: - which declared actor owns the objective - which declared scenario elements the objective names as targets -- which observable `conditions` define success +- which invariant and postcondition assertions define success - which window bounds when the objective matters - which other objectives must precede it @@ -22,12 +22,11 @@ The implementation must build on these existing authorities: - SDL structure: `aces_sdl.objectives.Objective`, `ObjectiveSuccess`, and `ObjectiveWindow` -- parser/model gates: `SDLModel`, parser key normalization, variable-key - rejection, and `SDLParseError` +- parser/model gates: `sdl-yaml/v1`, `SDLModel`, canonical-field enforcement, + explicit migration, variable-key rejection, and `SDLParseError` - static validation: `SemanticValidator` and `SDLValidationError` - objective-window analysis: `aces_sdl.semantics.objectives` -- condition resolution: the targetable named-reference index over declared - `conditions` +- assertion resolution over declared backend-neutral propositions - runtime compilation: `compile_runtime_model()` and `aces_processor.models.ObjectiveRuntime` - dependency graph semantics: `aces_processor.semantics.planner` @@ -59,11 +58,11 @@ Target resolution: Success interpretation: -- success references only declared `conditions` (observable state). The OCR +- success references only declared invariant or postcondition assertions. The OCR scoring surfaces `metrics`, `evaluations`, `tlos`, and `goals` were removed by [ADR-073](../../../docs/decisions/adrs/adr-073-scoring-reward-language-scope.md); objective success is no longer expressible against a graded score -- objective success mode describes interpretation of the referenced conditions; +- objective success mode composes the referenced assertion outcomes; it must not encode evaluator implementation mechanics - runtime result and execution contracts remain the portable observation boundary for evaluated success @@ -96,8 +95,8 @@ Dependency roles (which references propagate through the planner): compiles actor or target into runtime addresses lifts the role constant in lockstep - the derived per-objective `ordering_names` / `refresh_names` tuples are - kind-qualified (`condition.`, `objective.`, `story.`, `script.`, - `event.`, `workflow.`) so a condition and an objective with the same SDL + kind-qualified (`assertion.`, `objective.`, `story.`, `script.`, + `event.`, `workflow.`) so an assertion and an objective with the same SDL name remain distinguishable ## Cross-Cutting Gates diff --git a/specs/formal/objectives/proposition-and-assertion-semantics.md b/specs/formal/objectives/proposition-and-assertion-semantics.md new file mode 100644 index 000000000..063e0b843 --- /dev/null +++ b/specs/formal/objectives/proposition-and-assertion-semantics.md @@ -0,0 +1,171 @@ +# Proposition, Assertion, and Truth-Result Semantics + +## Authority and claim level + +This FM2 specification realizes [ADR-079](../../../docs/decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md). +The published JSON Schemas govern serialized shapes. The equations below govern +the reference truth algebra. Runtime tests and fixtures provide bounded +assurance; ACES does not claim a mechanized proof. + +## Domains + +For an instantiated scenario `S`: + +```text +Subject(S) = finite set of canonically addressable SDL resources +Truth = {T, F, U} +Support = {supported, unsupported} +PortableOutcome = {true, false, unknown, unsupported} +``` + +`unsupported` belongs to `PortableOutcome` but not `Truth`. It reports a failed +realization obligation, not a fact about the scenario world. + +A proposition `p` is: + +```text +p = (subjects, basis, predicate, quantifier, threshold, evidence_requirements) +``` + +where `subjects` is non-empty and finite; `basis` is `declared_state` or +`observed_state`; `predicate` is one member of the closed typed union; and +`threshold` exists exactly when `quantifier = at_least`. + +The predicate is evaluated once per resolved subject. The proposition outcome +is the finite aggregation of those per-subject outcomes: `all` uses `all_of`, +`any` uses `any_of`, and `at_least(k)` uses the same `at_least(k)` table defined +below. Subject order has no semantic effect. + +An assertion `a` is: + +```text +a = (proposition_ref, role, polarity) +role in {precondition, invariant, postcondition} +polarity in {positive, negative} +``` + +The owning objective/event/workflow/participant reference supplies scope. A +role does not create a clock. + +## Predicate typing + +```text +presence : property -> exists +boolean : property x {equals, not_equals} x Bool +string : property x {equals, not_equals} x String + | property x {in, not_in} x NonEmptyUniqueList(String) +number : property x OrderedComparison x FiniteNumber x Unit +``` + +Every property has a stable HTTPS/URN semantic reference. Numeric units have a +separate semantic reference. No implicit string-to-number or Boolean-to-number +coercion is permitted. + +## Negation and composition + +Let portable negation `not4` be: + +| x | `not4(x)` | +|---|---| +| true | false | +| false | true | +| unknown | unknown | +| unsupported | unsupported | + +Positive assertions preserve a proposition outcome; negative assertions apply +`not4`. + +For non-empty sequence `xs`: + +```text +all_of(xs) = false if false in xs + = true if every x is true + = unsupported if unsupported in xs + = unknown otherwise + +any_of(xs) = true if true in xs + = false if every x is false + = unsupported if unsupported in xs + = unknown otherwise +``` + +For `at_least(k, xs)`, let `t` be the count of true values and `d` the count of +unknown plus unsupported values: + +```text +at_least(k, xs) = true if t >= k + = false if t + d < k + = unsupported if unsupported in xs + = unknown otherwise +``` + +`1 <= k <= |xs|`. Empty composition is undefined and rejected. + +## Evidence admission + +For an observed-state result `r`: + +```text +Decided(r) => ProbeBinding(r) + and NonEmpty(EvidenceRefs(r)) + and GovernedTemporalContext(r) +``` + +For a declared-state result: + +```text +Decided(r) => InstantiatedArtifactDigest(r) +``` + +For every decided result: + +```text +all(loss.within_admissible_bound for loss in r.loss_disclosures) +``` + +Missing, stale, partial, conflicting, redacted, lossy, or probe-failure evidence +produces `unknown` with a typed reason unless an admitted rule proves the loss +is within its bound. Missing capability produces `unsupported` with at least +one capability reference. + +## Role admission + +- Objective success references only invariant or postcondition assertions. +- Event triggers, workflow state predicates, and participant starting state + reference only precondition assertions. +- False, unknown, or unsupported preconditions do not establish applicability. +- A postcondition is not established by command completion or evaluator + lifecycle success. +- A sampled point does not prove an interval invariant without a governed + coverage contract. + +## Equivalence boundary + +`semantic_claim(r)` projects: + +```text +(proposition, assertion, polarity, proposition_outcome, assertion_outcome, + basis, indeterminacy_reason, temporal_context, loss_disclosures, + unsupported_capabilities) +``` + +Equality of this projection permits comparing the same claim realized by +different bindings while retaining distinct binding/evidence provenance. It is +not a bisimulation claim. Park-style bisimulation would additionally require a +labelled transition system, an observation boundary, hidden-action treatment, +and a relation preserved by every transition. + +## Realization mapping + +The compiler emits separate proposition and assertion resources. A condition +binding may point to a proposition resource but cannot change its predicate. +The backend manifest declares supported predicate families, quantifiers, +portable outcomes, evidence channels, time domains, and binding-provenance +preservation. Admission reports unsupported instead of substituting a weaker +predicate or clock. + +A runtime truth result is admitted only if its proposition and assertion +addresses resolve to semantic entries in the same snapshot, the proposition +entry has the same evaluation basis, and the assertion entry has the same +proposition link and polarity. This prevents a structurally valid envelope from +changing or detaching the compiled claim. diff --git a/specs/formal/participant-behavior-model/README.md b/specs/formal/participant-behavior-model/README.md index 8b976d9e1..135f0f9d5 100644 --- a/specs/formal/participant-behavior-model/README.md +++ b/specs/formal/participant-behavior-model/README.md @@ -31,7 +31,7 @@ Existing coverage: - `controlled-vocabularies-v1` already defines `participant-decision-surface-modes` and `participant-offensive-behavior-activities`. -- Issue #206 adds SDL `behavior-specifications` authoring, semantic +- Issue #206 adds SDL `behavior_specifications` authoring, semantic validation, generated schema coverage, and compiled `participant.behavior-specification.*` runtime records for ACT-606. @@ -268,7 +268,7 @@ Rules: Implementation issue #206 adds the executable SDL authoring and validation surface for this aggregate. The Python reference implementation parses -`behavior-specifications`, validates participant, role, action, observation, +`behavior_specifications`, validates participant, role, action, observation, outcome, authority, extension, and governed-mode refs, includes the surface in generated SDL schemas, and compiles stable `participant.behavior-specification.` runtime records without creating a @@ -282,7 +282,7 @@ Authority and scope are authored semantics. The model distinguishes: | --- | --- | --- | | `starting_accounts` | Initial declared access anchors | proof of authority | | `initial_knowledge` | Participant starting knowledge refs | hidden truth | -| `starting_conditions` | Declared state or setup preconditions | setup commands | +| `starting_assertions` | Precondition assertions over declared propositions | setup commands or proof that a probe passed | | `authority_anchors` | Declared bases for allowed or expected action | bearer tokens, HTTP auth, OS user | | `operating_scope` | Declared targetable action/observation boundary | backend sandbox, process boundary | | action preconditions | Contract-level applicability checks | free-form policy prose | @@ -312,7 +312,7 @@ Executable ACT-607 work must extend the existing participant-authoring and behavior surfaces. The canonical incumbents are: - SDL authored semantics: `agents.*.starting_accounts`, - `initial_knowledge`, `starting_conditions`, `authority_anchors`, + `initial_knowledge`, `starting_assertions`, `authority_anchors`, `operating_scope`, and behavior-specification `authority_scope_refs`. - Action and observation semantics: typed participant action preconditions, governed failure classes such as `authority_denied`, observation boundaries, diff --git a/specs/formal/participant-semantics/README.md b/specs/formal/participant-semantics/README.md index db60de0b0..bc36b8da4 100644 --- a/specs/formal/participant-semantics/README.md +++ b/specs/formal/participant-semantics/README.md @@ -23,7 +23,7 @@ What exists: - `agents.*` declares participant framing inputs: `entity`, `actions`, `starting_accounts`, `initial_knowledge`, `allowed_subnets`, - `starting_conditions`, `authority_anchors`, and `operating_scope`. + `starting_assertions`, `authority_anchors`, and `operating_scope`. - ADR-020 defines identity, role, starting conditions, authority anchors, and operating scope as authored participant framing. - ADR-013 and the participant-episode contracts define lifecycle state and diff --git a/specs/formal/realization/envelope-semantics.md b/specs/formal/realization/envelope-semantics.md index 86fadb6e4..b5ce4e6c9 100644 --- a/specs/formal/realization/envelope-semantics.md +++ b/specs/formal/realization/envelope-semantics.md @@ -18,11 +18,10 @@ This note governs: - envelope subsumption; - deterministic witness generation; - negative conformance for closed envelopes; -- backend-manifest carriage constraints for future schema evolution. +- backend-manifest carriage constraints for schema evolution. Out of scope: -- publishing a concrete JSON schema for envelope expressions; - replacing `run_target_conformance(reference_scenario=...)`; - adding implementation helpers, CLI commands, APIs, persistence, or runtime behavior; @@ -31,15 +30,29 @@ Out of scope: ## Realization Status -This spec is design authority. It is not yet executable. The relation helper, -schema carrier, fixtures, property tests, target-conformance integration, and -manifest evolution are downstream implementation work. +This spec is design authority. The relation helper, the envelope expression +contract model, fixtures, and property tests are implemented by issue #668: -Until that work lands: +- `aces_contracts.realization_envelope` carries the closed, versioned envelope + expression (`realization-envelope/v1`) — the admitted-fragment domain kinds, + scoped bindings, posture, closure, and witness policy of this note; +- `aces_sdl.realization_envelope` implements `member`, `subsumes`, `witness`, and + `generate_negative_probes` as one deterministic engine over that contract. + +Issue #100 publishes the schema carrier at +`contracts/schemas/realization-envelope/realization-envelope-v1.json`, packages governed +backend instances under `contracts/realization-envelopes/`, and carries an +immutable envelope/configuration identity through backend-manifest-v2, +provisioning-plan-v1, and runtime-snapshot-v1. Backend carriers embed this same +expression and add closed realization/observation disclosures; they do not +introduce a second set language. + +The remaining downstream boundary is target-conformance integration: - SEM-218 remains the active exact/constrained/open realization authority; -- `backend-manifest-v2.realization_support` remains the coarse capability and - disclosure surface; +- `backend-manifest-v2.realization_support` remains the coarse capability floor, + while the selected envelope is the configuration-bound value and disclosure + authority; - `run_target_conformance(reference_scenario=...)` remains the temporary #663 bridge for fixed-topology and simulation backends. @@ -118,6 +131,16 @@ An envelope expression consists of: 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 +`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 +checks therefore MUST NOT treat that field as an unspecified realizable +dimension. This projection does not weaken provenance validation or remove the +field from the artifact. + ## Required Semantics ### R1 - Envelopes denote sets @@ -127,9 +150,9 @@ scenario instance `s` is a member of envelope `E` exactly when: 1. `s` is structurally and semantically valid under the ordinary SDL rules; 2. every effective binding in `E` is satisfied by the corresponding value or - child scope in `s`; + child scope in the realization projection of `s`; 3. every closed-world scope in `E` has no unspecified realizable dimension in - `s`; + the realization projection of `s`; 4. every governed reference in `E` resolves under the applicable concept or SDL authority. diff --git a/specs/formal/sdl-phases/README.md b/specs/formal/sdl-phases/README.md new file mode 100644 index 000000000..aa89a63c2 --- /dev/null +++ b/specs/formal/sdl-phases/README.md @@ -0,0 +1,178 @@ +# SDL Phase Contracts + +Classification: FM2, refinement/constraint semantics. + +This note states the invariants for ADR-078. It is a lightweight formal model, +not a machine-checked proof and not an operational semantics for scenario +behavior. + +## Domains + +Let: + +- `C` be the closed set of executable scenario-content fields; +- `A` be a normalized authoring object; +- `E` be an expanded authoring object; +- `I` be an instantiated scenario; +- `S` be an instantiated snapshot; +- `tokens(x)` be every `${portable-id}` token occurring in a string value of + `x` (mapping keys are not substitution sites); and +- `fields(x)` be the object's top-level member names. + +The phase shapes are: + +```text +fields(A) subset-of C union {module, imports, variables} +fields(E) subset-of C union {variables, expansion_provenance} +fields(I) subset-of C union {instantiation_provenance} +fields(S) = {profile, scenario} +``` + +The subset relation accounts for optional members of a closed shape. No member +outside the relevant set is admitted. `name` is required in `A`, `E`, and `I`; +`instantiation_provenance` is additionally required in `I`; both `profile` and +`scenario` are required in `S`. + +## Transition Relations + +The supported transitions are partial functions: + +```text +normalize : Source -> A or error +expand : A -> E or error +bind : (A or E) x Parameters -> I or error +snapshot : I -> S or error +``` + +`expand` may be the identity on executable content when no imports exist, but +the trusted expanded representation remains distinct. There is no supported +transition from `I` back to `A` or `E`, and no parser treats `S` as source. + +## Invariants + +### P1: Phase exclusion + +```text +{module, imports} intersect fields(E) = empty +{module, imports, variables} intersect fields(I) = empty +{module, imports, variables} intersect fields(S) = empty +``` + +The last line applies to snapshot-envelope members; the nested `scenario` must +itself satisfy the instantiated rule. + +### P2: Concreteness + +```text +tokens(I) = empty +tokens(S) = empty +``` + +This includes provenance strings. Substitution is single-pass, but a selected +value that reintroduces a token is not an admitted concrete artifact. + +### P3: Binding environment closure + +For provenance `P`, define its qualified binding environment: + +```text +B(P)[root_binding.parameter] = root_binding.value +B(P)[import.namespace ++ import_binding.parameter] = import_binding.value +``` + +Root binding identities have one segment. Import-local binding identities have +one segment. Import namespaces are non-empty segment tuples. Every resulting +key in `B(P)` is unique. + +### P4: Capability evidence consistency + +For each capability constraint `q` in `P`: + +```text +q.parameter in domain(B(P)) +B(P)[q.parameter] in-json q.allowed_values +resolve_json_pointer(I, q.field_pointer) =json B(P)[q.parameter] +``` + +`q.field_pointer` is exactly `/nodes//os` or +`/infrastructure//count`. `=json` distinguishes booleans from +numbers; numeric JSON values compare by numeric value. `in-json` uses the same +equality. Allowed domains contain no duplicate JSON values. + +### P5: Explicitness evidence closure + +For every explicitness record `r` and every parameter identity `p` in +`r.parameters`: + +```text +p in domain(B(P)) +``` + +Model paths are unique within one provenance object. This preserves SEM-218 +classification across serialization without reconstructing variable +declarations. + +### P6: Import evidence order and portability + +Resolved import records occur in declared preorder: an import record precedes +the records for its nested imports, and sibling subtrees retain declaration +order. Namespace tuples recover nesting without parsing dotted display text. + +Requested/resolved source identities contain no absolute host/cache path or +registry user information. Digest fields are empty only when the source class +does not produce that digest; otherwise they use `sha256:<64 lowercase hex>`. + +### P7: Admission + +```text +admitted(I) iff + closed_shape(I) + and tokens(I) = empty + and provenance_consistent(I) + and semantic_validator(I) succeeds +``` + +JSON Schema checks `closed_shape` and token/scalar syntax. Runtime model +validation checks provenance relations. Semantic validation checks the SDL +reference and graph rules. No one layer is described as proving all three. + +### P8: Canonical snapshot identity + +For admitted `I`: + +```text +S(I) = { + "profile": "aces-sdl-instantiated-snapshot/v1", + "scenario": I +} + +bytes(I) = JCS(S(I)) +digest(I) = "sha256:" ++ lowercase_hex(SHA256(bytes(I))) +``` + +Changing concrete content or provenance changes the snapshot input. Equality +of `digest(I)` is identity under this canonical profile, not behavioral +equivalence or bisimilarity. + +## Evidence Mapping + +- typed phase and provenance records: + `implementations/python/packages/aces_sdl/scenario.py` and + `phase_contracts.py` +- transition and admission functions: + `implementations/python/packages/aces_sdl/instantiate.py` +- typed processor lowering: + `implementations/python/packages/aces_processor/models.py` and + `implementations/python/packages/aces_processor/planner.py` +- canonical snapshot: + `implementations/python/packages/aces_sdl/canonical.py` +- external contracts: + `contracts/schemas/sdl/instantiated-scenario-v1.json` and + `instantiated-scenario-snapshot-v1.json` +- model/schema/cross-phase tests: + `implementations/python/tests/test_sdl_phase_contracts.py` and + `test_instantiated_scenario_schema.py` + +The serialized round-trip tests are differential evidence: compilation of an +in-process artifact and its JSON-deserialized counterpart must agree, including +capability and explicitness semantics. diff --git a/specs/sdl/README.md b/specs/sdl/README.md index a8621ecf0..a2fd28020 100644 --- a/specs/sdl/README.md +++ b/specs/sdl/README.md @@ -1,7 +1,8 @@ -# SDL Authoring Specification +# SDL Specification Status: **normative**. This directory is the language-neutral authority for the -ACES **Scenario Description Language (SDL)** authoring model. It is binding on +ACES **Scenario Description Language (SDL)** authoring model and portable +derived phase contracts. It is binding on the ecosystem independent of any reference implementation or code-generation pipeline, per [ADR-009](../../docs/decisions/adrs/adr-009-normative-artifact-authority-and-repository-structure.md) and the [authority boundary manifest](../authority/authority-boundary.yaml). @@ -28,9 +29,12 @@ Three artifact classes describe the SDL, with distinct authority: The prose and the schemas describe the same language and **MUST** agree; a divergence between them is a defect to be reconciled, not a license for either to override the other. Where this prose states a structural fact - (a section's presence, requiredness, or value shape), the published - `sdl-authoring-input-v1.json` schema is the authoritative enumeration the - prose is written to match. + (a section's presence, requiredness, or value shape), the applicable + `sdl-authoring-input-v1`, `instantiated-scenario-v1`, or + `instantiated-scenario-snapshot-v1` schema is the authoritative enumeration + the prose is written to match. The authoring schema validates the normalized + object, not raw YAML presentation; `document-model.md` §1 and the + `contracts/fixtures/sdl/sdl-yaml-v1/` corpus define the raw source profile. 3. **Reference implementations (`implementations/`)** consume both. No Python model, validator function, or generator defines ecosystem meaning; it is evidence of one conforming realisation. This specification names @@ -62,13 +66,14 @@ tests, rather than a prose rewrite. The catalogs are: | File | Catalog | Covers | |------|---------|--------| -| [`document-model.md`](document-model.md) | — | Document encoding, the metadata/composition vs. authoring-section split, requiredness, identifier rules, structural closure, and the authoring → instantiated → expanded phases. | +| [`document-model.md`](document-model.md) | — | Document encoding, the metadata/composition vs. authoring-section split, requiredness, identifier rules, structural closure, and the normalized -> expanded -> instantiated -> snapshot phases. | | [`sections.md`](sections.md) | **1. Section catalog** | Every top-level field: kind, value shape (map-keyed vs. list-valued vs. scalar), requiredness, key shape, and the sections it references. | | [`references.md`](references.md) | **2. Reference-resolution catalog** | Reference forms (bare, qualified, nested runtime-family, workflow-step, module-composed), the resolution algorithm, the fail-closed ambiguity rule, and the cross-section reference-edge catalog. | | [`variables-and-instantiation.md`](variables-and-instantiation.md) | **3. Variable / instantiation catalog** | Variable types, defaults, `allowed_values`, `${…}` substitution, the instantiation algorithm, and post-instantiation exclusions. | | [`runtime-inventory.md`](runtime-inventory.md) | **4. Runtime-family index** | The node-scoped runtime-inventory index — family key, collection name, primary `_id`, child-ref collections, owning ADR — and the shared invariants stated once, delegating per-field semantics to the family ADRs. | | [`observability-and-evidence.md`](observability-and-evidence.md) | **5. Observability and evidence planes** | Scenario-native observability, authored evidence requirements, processor/backend operational observability, captured evidence, derived analysis, and augmentation classification rules. | -| [`diagnostics.md`](diagnostics.md) | — | The parse / semantic-validation / instantiation diagnostic stages and the normative error-vs-advisory classification criterion. | +| [`scientific-scenario-completeness.md`](scientific-scenario-completeness.md) | — | REV1 intended-use profiles, atomic concern dispositions, separately revisioned delivery assessment, computed completeness, and explicit scientific non-claims. | +| [`diagnostics.md`](diagnostics.md) | — | The parse / semantic-validation / instantiation stages, direct-artifact admission, and the normative error-vs-advisory classification criterion. | ## Acceptance-question map @@ -76,10 +81,15 @@ An implementer can answer each structural question from the named file alone: - *Which sections exist, which are required, and what shape is each?* → [`document-model.md`](document-model.md) and [`sections.md`](sections.md). + The nox contracts gate runs `tools/check_sdl_catalog_parity.py` to prove the + catalog, published schema, and reference registries remain reconciled. +- *Which raw YAML documents are canonical SDL, and what receives a stable + semantic digest?* → [`document-model.md`](document-model.md) §§1, 5, 7-9. - *What is a valid identifier for a user-defined key?* → [`document-model.md`](document-model.md). - *How does a reference resolve, and what happens when it is dangling or - ambiguous?* → [`references.md`](references.md). + ambiguous?* → [`references.md`](references.md), including its checked + editor-visible edge index and distinct candidate-domain classifications. - *What is legal to instantiate, and what does instantiation reject?* → [`variables-and-instantiation.md`](variables-and-instantiation.md). - *What is the runtime-inventory surface and which ADR owns each family?* → @@ -89,11 +99,15 @@ An implementer can answer each structural question from the named file alone: [`observability-and-evidence.md`](observability-and-evidence.md). - *When is a problem an error versus an advisory?* → [`diagnostics.md`](diagnostics.md). +- *When is valid SDL sufficient for deployment, participant evaluation, + controlled experimentation, or a reproducible benchmark claim?* → + [`scientific-scenario-completeness.md`](scientific-scenario-completeness.md). ## Scope -In scope: the SDL authoring model — document structure, references, variables, -instantiation, the runtime-inventory index, observability/evidence plane +In scope: the SDL authoring model and portable derived phase contracts — +document structure, references, variables, instantiation/provenance, +canonical snapshots, the runtime-inventory index, observability/evidence plane rules, and the diagnostic boundary. Out of scope: delivery-level concerns (container, infrastructure-as-code, and diff --git a/specs/sdl/diagnostics.md b/specs/sdl/diagnostics.md index 8cdeff3a0..fdd23e3d8 100644 --- a/specs/sdl/diagnostics.md +++ b/specs/sdl/diagnostics.md @@ -8,13 +8,16 @@ new diagnostic mechanism, and it does not reclassify any existing condition. ## 1. Diagnostic stages -An SDL document is checked at three stages, in order. Each is **fail-closed**: +An authored SDL document is checked at three transformation stages, in order. +Portable instantiated artifacts also have an admission gate. Each is +**fail-closed**: a problem at a stage stops the document from advancing past that stage. -1. **Parse / structural.** YAML loading and structural shape: the root is a - mapping, keys are strings, values have the right shapes, and **no unknown key - is present** ([document-model.md §4](document-model.md)). A structural problem - is a parse error. +1. **Source / parse / structural.** `sdl-yaml/v1` decoding, operational bounds, + structural shape, and typed construction: the root is a mapping, keys are + strings, mapping entries remain unique, canonical structural fields are + exact, values have the right shapes, and **no unknown key is present** + ([document-model.md §§1, 4-5](document-model.md)). A problem is a parse error. 2. **Semantic validation.** Cross-section reference resolution ([references.md](references.md)), uniqueness, acyclicity, control-flow closure, and the runtime-family invariants @@ -26,14 +29,24 @@ a problem at a stage stops the document from advancing past that stage. problem here is an instantiation error. Instantiation re-runs semantic validation on the concrete document, so semantic errors can also surface at this stage. +4. **Instantiated-artifact admission.** A direct or deserialized + `instantiated-scenario-v1` payload is checked for its closed phase shape, + required and internally consistent provenance, absence of substitution + tokens, and ordinary semantic validity before compilation or canonical + snapshot creation. Structural/provenance failures use the instantiation + error surface; semantic failures use the validation error surface. Admission + never trusts a caller-set private validation flag. ## 2. Collect-all semantics The semantic-validation and instantiation stages **collect all errors in a pass** and report them together, rather than failing at the first problem. An author fixing a document sees the full set of errors a stage found, not one error at a -time. (Parsing may stop at the first structural fault that prevents -interpretation.) +time. Parsing may stop at the first structural fault that prevents composition +of a YAML node graph. Once that graph is available, the mapping-key preflight +collects all exact duplicates, migration-merge conflicts, canonical-field +violations, and field-alias collisions before construction; none is hidden by +a last-write-wins mapping. ## 3. Errors are fatal @@ -62,6 +75,12 @@ The boundary rule is symmetric and **MUST** be honoured: Existing advisory conditions, documented here by reference (not redefined): +- **Explicit source migration.** A migration operation may accept a recognized + legacy field spelling or disjoint `<<` merge and emit a source-ranged warning. + This does not reclassify the construct as valid canonical source: strict + `sdl-yaml/v1` decoding still rejects it, and the migrated output must pass + strict decoding. Ambiguity and unknown fields remain fatal in migration mode. + - **VM without resources.** A virtual-machine node declared without a `resources` block is **valid** SDL; it is flagged as an advisory because it may be undeployable unless a backend supplies defaults. It is not an error. @@ -127,3 +146,105 @@ A future change that moves a condition between the error and advisory channels, or that adds a new diagnostic category, **MUST** apply this criterion and be reflected here, in the published schemas, and in the reference implementation together, so the boundary stays single-sourced. + +## 6. Mapping-key diagnostics + +Exact duplicate keys, conflicting effective keys introduced by `<<`, and +distinct structural field spellings that normalise to one field use the stable +diagnostic code `sdl.mapping_key_conflict`. They are fatal at the `parse` stage +and **MUST** be raised before Pydantic or any other SDL model constructor sees +the mapping. + +An explicitly non-string or complex mapping key uses +`sdl.mapping_key_type`. A cyclic YAML alias graph uses `sdl.alias_cycle`. These +conditions are likewise fatal at the `parse` stage and carry the canonical +target path and primary source range; they do not have a second authored-key +range when no competing declaration exists. + +Each diagnostic **MUST** carry: + +1. the canonical target path as an RFC 6901 JSON Pointer, with schema field + segments canonicalised and user-defined/native-map segments preserved and + escaped; +2. both authored key spellings (the spellings are identical for an exact + duplicate); and +3. the one-based line and column range of both key tokens. The later/conflicting + key is the primary range and the earlier key is a related range. When the + conflict is contributed through a YAML merge, the ranges identify the + original key declarations and the canonical path identifies the effective + target mapping. + +The reference implementation continues to use `SDLParseError` for this failure; +it **MUST NOT** introduce a parallel exception hierarchy. Public structured +adapters (including language-service and MCP responses) preserve the code, +stage, canonical path, and both ranges. Plain-text CLI/library rendering may +format the same fields as prose but must not replace them with raw YAML values or +silently downgrade the error to a generic model-validation failure. + +## 7. Source-profile and migration diagnostics + +Source diagnostics use the same structured envelope as mapping-key diagnostics. +Each carries a stable code, `parse` stage, severity, message, RFC 6901 path, +one-based half-open source range, and source identity when file-backed. A +diagnostic never includes the mapped value, whole source document, parameter +map, secret, or traceback. + +| Code | Meaning | Strict severity | Migration severity | +|------|---------|-----------------|--------------------| +| `sdl.utf8` | Input cannot be represented as valid UTF-8 | error | error | +| `sdl.source_format` | Unsupported source-profile identifier | error | error | +| `sdl.migration_policy` | Unknown migration-policy identifier | error | error | +| `sdl.parse` | YAML syntax, stream, or composition failure | error | error | +| `sdl.directive` | YAML directive is present | error | error | +| `sdl.explicit_tag` | Explicit YAML tag is present | error | error | +| `sdl.source_limit` | A `sdl-yaml/v1` resource bound is exceeded | error | error | +| `sdl.non_json_value` | Constructed value is outside the SDL JSON domain | error | error | +| `sdl.mapping_key_type` | Mapping key does not construct as a string | error | error | +| `sdl.mapping_key_conflict` | Duplicate or canonicalized collision | error | error | +| `sdl.alias_cycle` | Alias graph is cyclic | error | error | +| `sdl.identifier.invalid` | Declaration identity violates the portable local-id contract | error | error | +| `sdl.model.invalid` | Typed model field violates its declared structural contract | error | error | +| `sdl.noncanonical_field` | Recognized legacy structural-field spelling | error | warning | +| `sdl.noncanonical_merge` | YAML 1.1 `<<` migration syntax | error | warning | + +For `sdl.noncanonical_field`, `authored_keys` contains the authored and +canonical spellings, and the path points to the canonical field. For +`sdl.noncanonical_merge`, the path points to the effective mapping. Warnings +are retained on the successfully migrated scenario and by formatting, MCP, and +CLI adapters. Strict validation is the default at every ordinary parse ingress; +migration acceptance requires an explicit caller choice. + +An identifier diagnostic points to the exact defining key or scalar-id token +and carries that token's source range. Its bounded message states the grammar +without echoing the invalid spelling, adjacent value, document fragment, +parameter map, or traceback. `SDLMigrationPolicy.ACCEPT` does not demote or +rewrite an invalid identity; identifier migration requires an explicit atomic +rename of the declaration and all resolved references. + +A typed-model diagnostic preserves the validator-owned contract statement so +an author can determine why the field is invalid. The parser excludes +Pydantic's input rendering and documentation URL, removes framework prefixes, +escapes control characters, and bounds each message to 512 characters before +placing it in `sdl.model.invalid`. The JSON Pointer and source range remain the +authoritative locator; a raw `ValidationError`, input object, traceback, or +unbounded validator rendering is never exposed. + +## 8. Instantiation and artifact-admission disclosure + +Instantiation and instantiated-artifact admission diagnostics identify a +bounded variable/field location and failure class. They **MUST NOT** render a +supplied parameter value, an `allowed_values` domain, a complete parameter map, +the concrete artifact, trust-policy contents, credentials, a raw Pydantic input +dump, documentation URL, or traceback. + +When structural reconstruction fails, the public diagnostic renders an RFC 6901 +location plus a stable validation category. The reference implementation wraps +this in `SDLInstantiationError`; a raw framework `ValidationError` is not the +public artifact-admission contract. Semantic errors discovered after successful +structural admission remain `SDLValidationError` and retain the collect-all +semantics of the semantic pass. + +Resolved values necessarily occur in the concrete fields they populate and in +the portable replay binding record. Authoring, MCP, compiler, and operation +summary surfaces **MUST NOT** echo a second raw binding map. Count-only summaries +are permitted. diff --git a/specs/sdl/document-model.md b/specs/sdl/document-model.md index 8feed241d..16c59fda3 100644 --- a/specs/sdl/document-model.md +++ b/specs/sdl/document-model.md @@ -10,16 +10,58 @@ See [`sections.md`](sections.md) for the per-section catalog, [`variables-and-instantiation.md`](variables-and-instantiation.md) for variable and instantiation rules. -## 1. Encoding +## 1. Source profile: `sdl-yaml/v1` -1. An SDL document **MUST** be a YAML 1.1/1.2 document whose top-level value is a - mapping. A document whose root is a sequence, scalar, or null is not a valid - SDL document. -2. Every top-level key **MUST** be a string. Field keys are matched - case-sensitively after enum/value normalisation (§5); a key that is not a - defined top-level field is rejected (§4). -3. A document **MUST** be loadable by a safe YAML loader. Constructor tags that - instantiate arbitrary types **MUST NOT** be honoured. +Canonical SDL source uses the versioned profile `sdl-yaml/v1`. A conforming +strict decoder **MUST** apply all of the following rules before model +construction: + +1. The byte stream **MUST** be valid UTF-8 and contain exactly one YAML 1.2.2 + document. Its root **MUST** be a mapping. A sequence, scalar, null root, or + multi-document stream is invalid. +2. Untagged plain scalars **MUST** resolve with the YAML 1.2.2 Core schema + ([§10.3](https://yaml.org/spec/1.2.2/#103-core-schema)). Thus `true`, `FALSE`, + `null`, `0o12`, and `0x0a` are typed Core values, while `yes`, `on`, a date + spelling, `1_000`, and `-0x0a` are strings. This rule applies in key position: + unquoted `true` is a boolean key and is invalid, while unquoted `on` is a + string identifier. +3. Every mapping key **MUST** construct as a string. Complex keys and scalars + resolved to null, boolean, integer, or float are invalid as keys. Quoting a + Core-looking identifier is sufficient to keep it a string. +4. Explicit tags and YAML directives **MUST NOT** appear. Only safe standard + construction of implicitly resolved Core values is permitted. Document + start/end markers are presentation syntax, not directives, and **MAY** appear. +5. Constructed values **MUST** be in the JSON data domain: null, boolean, + arbitrary-precision integer, finite binary64-compatible float, Unicode + string, sequence, or string-keyed mapping. Timestamps, sets, language-native + objects, non-finite floats, and other tag-specific values are invalid. +6. Anchors and aliases **MAY** share acyclic representation nodes; their names + and sharing carry no SDL meaning. Cyclic aliases are invalid. The YAML 1.1 + `<<` merge extension is not YAML 1.2.2 Core syntax and is invalid canonical + SDL; §5 defines its explicit migration treatment. +7. Every authored mapping entry **MUST** remain distinguishable until key + uniqueness is checked. Exact duplicates and canonical-field collisions are + invalid; a decoder **MUST NOT** construct a last-write-wins mapping first. +8. No Unicode normalization is performed. Code-point sequences remain as + authored after YAML escape processing. + +Each source document has fixed denial-of-service bounds: at most 8 MiB of UTF-8 source, +1 MiB in one scalar, depth 128, 100,000 unique representation nodes, 256 alias +occurrences, and 250,000 nodes of alias-expanded traversal work. Exceeding any +bound is a source error. A future syntax, scalar policy, or incompatible limit +set requires a new source-profile identifier. + +A file-backed composition request additionally carries one aggregate budget +across the complete import graph. It bounds the number of imports, aggregate +decoded scalar bytes, aggregate structured nodes, recursion depth, generated +namespace depth, and qualified-name length. The structured-node count is also a +conservative declaration bound because every declaration consumes at least one +counted node. Exceeding any aggregate bound is a source error; recursively +starting a fresh per-file budget is non-conforming. + +This uniqueness rule follows the YAML 1.2.2 representation model, in which a +mapping is an unordered association of unique keys and non-unique keys are a +loading failure ([YAML 1.2.2 §§3.2.1.1, 3.3](https://yaml.org/spec/1.2.2/)). ## 2. Top-level organisation @@ -34,15 +76,14 @@ The complete, authoritative enumeration of top-level fields, their kinds, value shapes, and requiredness is the [section catalog](sections.md), which is written to match `contracts/schemas/sdl/sdl-authoring-input-v1.json`. -> **Reconciliation note.** Earlier descriptions of the SDL spoke of "21 named -> sections, all dicts." That count is stale. The live authoring contract has a -> larger section set and is **not** uniformly map-keyed: `forwarding_agents` is -> list-valued, and the participant surfaces (`action_contracts`, +> **Reconciliation note.** Earlier descriptions treated the authoring surface +> as uniformly map-keyed. The live contract is **not** uniform: +> `forwarding_agents` is list-valued, and the participant surfaces (`action_contracts`, > `observation_boundaries`, `outcome_interpretation_rules`) are present. The -> section catalog states the live set; this specification reconciles the +> section catalog states and mechanically checks the live set; this specification reconciles the > language to the published schema rather than freezing a historical count. -## 3. Requiredness +## 3. Normalized-authoring requiredness 1. `name` is the only **REQUIRED** top-level field. A document without `name` is invalid. @@ -54,6 +95,11 @@ to match `contracts/schemas/sdl/sdl-authoring-input-v1.json`. schema and, for runtime families, by the owning family ADR ([runtime-inventory.md](runtime-inventory.md)). +These rules describe `sdl-authoring-input-v1`. Derived phase contracts add +their own required members: an instantiated scenario requires +`instantiation_provenance`, and an instantiated snapshot requires both +`profile` and `scenario` (§7). + ## 4. Structural closure (fail-closed) 1. SDL models are **closed**: an unknown key — at the top level or anywhere @@ -65,73 +111,148 @@ to match `contracts/schemas/sdl/sdl-authoring-input-v1.json`. 3. Closure exists so that a typo in a field name (`vulnerabilites`) fails the document rather than silently dropping content. Authors **MUST NOT** rely on undeclared keys to carry data. +4. Anchors and aliases do not weaken closure or uniqueness. Migration-mode + merge input is accepted only when every inherited and local effective field + is disjoint; override precedence is never SDL semantics. > *Implementation evidence (non-normative): the reference models set > `extra="forbid"` on the shared SDL base model.* -## 5. Value normalisation +## 5. Canonical fields, literal keys, and migration 1. Enum-valued fields accept their value case-insensitively, and accept a hyphen as an alias for an underscore in the value text, so that an authoring value such as `search-index` and `search_index` denote the same enum member. This is an authoring convenience; the normalised (canonical) form is what the document means. -2. Normalisation applies to enum **values**, not to user-defined identifier - **keys**. A user-defined key is preserved verbatim as the element's - identifier (§6). -3. A field that holds a variable placeholder (`${…}`) is **not** normalised as an +2. A structural field key **MUST** use its exact lower-case `snake_case` schema + spelling. `semantic_version` is canonical; `semantic-version`, + `Semantic_Version`, and `SEMANTIC_VERSION` are not canonical SDL source. +3. A tool **MAY** offer an explicit migration operation that recognizes legacy + case and hyphen spellings and `<<` merges. Migration is never implicit: the + strict/default policy rejects each construct. An accepting migration policy + **MUST** emit a source-ranged advisory for every rewritten field or merge, + and its output **MUST** pass strict `sdl-yaml/v1` decoding. Unrecognized + fields remain errors. +4. Migration aliases do not create a precedence rule. If two keys in one + effective mapping address the same canonical field, including through a YAML + merge, the mapping is ambiguous and **MUST** fail during parsing before model + construction. The diagnostic contract is defined in + [diagnostics.md §6](diagnostics.md). +5. Field recognition applies only while traversing a schema-defined + structural mapping. It **MUST NOT** be applied to user-defined identifier + maps, extension maps, or native option/label maps. Keys in those maps are + preserved verbatim, including case, hyphens, and underscores; only exact + duplicate identifiers are rejected. Core scalar resolution still applies, + so boolean/null/numeric-looking identifiers must be quoted when necessary. +6. A field that holds a variable placeholder (`${…}`) is **not** normalised as an enum value; the placeholder is preserved until instantiation ([variables-and-instantiation.md](variables-and-instantiation.md)). -## 6. Identifier rules for user-defined keys +Formally, let `c_scope(k)` lowercase a key and replace hyphens with underscores +in a structural mapping, and be the identity function in a literal mapping. +Canonical source additionally requires `c_scope(k) = k`. For every +pair of distinct entries `i` and `j` in an effective mapping (including merge +contributions), well-formedness requires +`c_scope(key_i) != c_scope(key_j)`. This injectivity condition is checked over +the authored node graph; mapped values do not participate in the comparison or +its diagnostics. -A user-defined key in a map-valued section is the **identifier** by which an -element is referenced from elsewhere ([references.md](references.md)). The -following rules govern identifiers: +## 6. Portable identifiers and declaration identity -1. **Preservation.** A map key is preserved verbatim as the element identifier; - it is not lowercased, trimmed, or otherwise rewritten. -2. **Uniqueness.** An identifier **MUST** be unique within its collection. Map - semantics make duplicate keys within one section ill-formed; runtime-family - `_id` values **MUST** likewise be unique within their collection - ([runtime-inventory.md](runtime-inventory.md)). -3. **No placeholders in defining keys.** An identifier-defining key **MUST NOT** - be a variable placeholder. Variables parameterise *values*, never the - identity of an element. (`${x}: …` as a section entry is invalid.) -4. **Node identifiers** **MUST** be at most 35 characters. A node identifier - **MAY** contain `.` — dotted node identifiers such as `wazuh.manager` are - used to name service families — and reference resolution accounts for dotted - node names ([references.md](references.md)). -5. **Workflow step identifiers** **MUST NOT** contain `.`, because `.` is the - path separator used to address a step from an objective window - (`.`). -6. **Runtime `_id` values** are stable, symbol-shaped handles: they - identify an element across references and **MUST NOT** carry whitespace or - quoting that would make them unaddressable in a qualified path. - -Beyond these rules, identifier *spelling* is the author's choice; the language -does not impose a global identifier grammar on ordinary section keys. - -## 7. Document phases - -An SDL document passes through up to three forms. Each form is a superset shape -of the authored document with progressively fewer unresolved constructs: - -1. **Authored.** The document as written. It **MAY** contain module imports and - `${…}` variable placeholders. Full semantic validation - ([references.md](references.md), [diagnostics.md](diagnostics.md)) applies to - the authored document, treating unresolved placeholders per §5.3. -2. **Expanded.** If the document declares a module or imports - ([sections.md](sections.md) — `module`, `imports`), module composition is - applied **before** full semantic validation, producing an expanded document - in which imported content has been merged under its namespace - ([ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md)). -3. **Instantiated.** Instantiation resolves variables against supplied - parameters and defaults, producing a concrete document with no surviving - variable definitions or unresolved placeholders - ([variables-and-instantiation.md](variables-and-instantiation.md)). +Every ACES-local **declaration identity** uses one portable local-identifier +grammar: + +```text +portable-id = id-start *63id-char +id-start = %x61-7A / DIGIT +id-char = id-start / "-" / "_" +``` + +Equivalently, a portable id is a full-string match for +`^[a-z0-9][a-z0-9_-]{0,63}$`. Implementations **MUST** use full-match semantics; +a `$`-anchored regex alone is insufficient in engines that match before a final +line terminator. The spelling is exact: an implementation **MUST NOT** trim, +case-fold, Unicode-normalize, escape, sanitize, or repair it. Uppercase, +non-ASCII, whitespace, controls, `.`, `/`, `:`, and `${…}` are invalid. + +The rule applies by semantic role, not field spelling. It covers `Scenario.name`; +map-valued section and variable keys; nested entity, role, and workflow-step +keys; named services, ACLs, and content items; scenario-level forwarding-agent +ids; and every ACES-local primary or child id in the runtime-family registry. +It does **not** apply merely because a field is called `name` or ends in `_id`. +Display labels, usernames, DNS names, URLs, paths, LDAP DNs, environment names, +versions, external/native/provider ids, and opaque evidence refs retain their +owning types. Where one object needs both a stable identity and a filename or +label, those are separate fields. + +Identifiers **MUST** be unique within their owning collection. Before aliases +are deduplicated, every declaration is also entered in a document-scoped typed +address index; two distinct declarations that render the same canonical address +make the document invalid. The admitted-document invariant is: + +```text +for all d1,d2 in Declarations(document): + render(address(d1)) = render(address(d2)) implies d1 = d2 +``` + +Variables parameterise values, never declarations. A placeholder **MUST NOT** +appear in any defining identity, and changing a parameter mapping **MUST NOT** +change the declaration set or any canonical address. Node local ids retain the +stricter 35-character maximum. Because YAML Core resolution precedes model +construction, an all-digit id must be quoted so it remains a string. + +Composition is the only operation that creates a **qualified name**: zero or +more portable namespace segments followed by one portable local id, rendered +with `.` separators. The reserved `__private` namespace segment may be generated +for non-exported module declarations but is invalid author input. Qualified +names are bounded to 2048 characters. Raw and normalized authoring objects admit +local ids only; expanded and instantiated objects may carry generated qualified +top-level identities. Nested owner-local ids, including node runtime-family ids, +remain local. + +## 7. Document phases and schema boundaries + +SDL has one source presentation followed by four distinct data forms. Their +object shapes are closed independently; a field absent from a phase is +forbidden rather than represented by an empty compatibility shell. + +| Form | Required/phase-specific members | Forbidden authoring machinery | Publication | +|------|---------------------------------|-------------------------------|-------------| +| Source | YAML presentation governed by `sdl-yaml/v1` | n/a | source profile and YAML fixtures | +| Normalized authoring | executable sections; `name`; optional `module`, `imports`, `variables` | none | `sdl-authoring-input-v1` | +| Expanded authoring | executable sections; `name`; root `variables`; typed `expansion_provenance` | `module`, `imports` | internal trusted representation | +| Instantiated | executable sections; `name`; required `instantiation_provenance` | `module`, `imports`, `variables`, any `${…}` token | `instantiated-scenario-v1` | +| Canonical instantiated snapshot | required `profile` and admitted `scenario` | all authoring machinery at the envelope; the nested scenario obeys the instantiated row | `instantiated-scenario-snapshot-v1` | -The authored → expanded → instantiated progression is the two-phase +The **normalized authoring object** exists after safe source construction, +canonical field recognition, shorthand expansion, enum/scalar typing, and +structural closure. It may contain imports and placeholders. Its published +schema validates that JSON-compatible object, not raw YAML or presentation +syntax. + +The **expanded authoring object** exists after trusted module resolution and +namespace rewriting but before final root-variable binding. Public exports have +their declared namespace prefix and non-exported declarations have the generated +`__private` prefix +([ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md)). +Composition consumes `module` and `imports`; their verified resolution facts +move into typed expansion provenance. Only the composition path may create this +internal representation or generated qualified declaration keys. Full semantic +validation applies to it. + +The **instantiated scenario** exists after the public binding operation has +validated its input, selected and checked every binding, substituted values, +rebuilt the closed concrete shape, checked provenance consistency, and rerun +semantic validation. Its provenance is part of the portable artifact, not +Python-private context. Direct/deserialized artifacts must pass the same +structural and semantic admission before 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. + +The source -> normalized -> expanded -> instantiated progression refines the two-phase authoring/instantiation model of [ADR-001](../../docs/decisions/adrs/adr-001-scenario-description-language.md) and the runtime-layering boundary of @@ -139,3 +260,71 @@ the runtime-layering boundary of [ADR-036](../../docs/decisions/adrs/adr-036-sdl-processor-runtime-module-boundaries.md): delivery-level realisation is downstream of, and out of scope for, the authoring model. + +## 8. Canonical semantic identity + +The canonicalization profile `aces-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 +module bundle, evidence, or a run. + +The canonical input is the following JSON object: + +```json +{ + "profile": "aces-sdl-semantic/v1", + "scenario": {}, + "module_variable_specs": {}, + "module_node_variable_refs": {} +} +``` + +`scenario` is the validated expanded authoring object serialized with canonical +wire field names while omitting fields that were not authored or introduced by +normalization/composition. The two legacy-named module maps are compatibility +projections derived from typed expansion provenance: the imported finite domains +and the concrete fields that depend on them. They preserve this profile's bytes +and meaning; they are not live variable definitions and do not appear as fields +of `instantiated-scenario-v1`. Array order is significant; object member order +is not. Authored omission is significant, so an omitted optional field and an +explicitly authored default are distinct. + +The envelope **MUST** satisfy the I-JSON input constraints and be serialized +with the JSON Canonicalization Scheme (JCS), RFC 8785. JCS preserves Unicode +code points without normalization, sorts object properties by UTF-16 code +units, preserves array order, emits UTF-8, and rejects non-finite or out-of-domain +numbers. The profile digest is SHA-256 over those bytes and is rendered +`sha256:<64 lower-case hexadecimal digits>`. A change to the envelope, +presence rule, or canonicalization algorithm requires a new profile identifier. + +JCS is the serialization rule for this profile; it is not the source of SDL's +identifier grammar or address semantics. RFC 8785 does not normalize Unicode, +and SDL likewise preserves display/data strings exactly while restricting only +declaration identities to the portable ASCII grammar above. + +## 9. Canonical instantiated snapshot + +The `aces-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", + "scenario": {} +} +``` + +`scenario` **MUST** validate against `instantiated-scenario-v1`, pass provenance +consistency checks, and pass the ordinary SDL semantic validator. Snapshot +construction is not a way to canonicalize an invalid direct model. The profile +and scenario members are both required; unknown envelope members are forbidden. + +The envelope is serialized and digested with the same JCS/SHA-256 rules in §8. +Any change to concrete content or `instantiation_provenance` changes the +canonical input. This digest establishes artifact identity under the named +profile. It does **not** establish that the recorded derivation activity +occurred, that external module sources remain available, that two scenarios +produce the same behavior, or that two executions are observationally +equivalent. diff --git a/specs/sdl/observability-and-evidence.md b/specs/sdl/observability-and-evidence.md index 7ec0b64b4..3f331c6d9 100644 --- a/specs/sdl/observability-and-evidence.md +++ b/specs/sdl/observability-and-evidence.md @@ -1,9 +1,11 @@ # Catalog 5 - Observability and Evidence Planes -This catalog states SDL authoring rules for ADR-066. It is normative for SDL -meaning, but it does not add new top-level fields by itself. Future executable -work that adds a field must still update `sections.md`, `references.md`, the -published SDL schemas, the reference implementation, fixtures, and tests. +This catalog states SDL authoring rules for ADR-066. The implemented +`evidence_requirements` top-level section carries authored capture intent; +scenario-native observability remains expressed through the relevant node +runtime families. A future field still has to update `sections.md`, +`references.md`, the published SDL schemas, the reference implementation, +fixtures, and tests. ## Plane Rule diff --git a/specs/sdl/references.md b/specs/sdl/references.md index 4edf92480..ba465b304 100644 --- a/specs/sdl/references.md +++ b/specs/sdl/references.md @@ -24,8 +24,8 @@ A reference is a string that names a target element. Five forms exist: `…...` to any depth the family defines ([runtime-inventory.md](runtime-inventory.md)). 4. **Workflow-step** — `.`, naming a step within a workflow. - Used by objective windows. Because `.` separates the workflow from the step, - workflow **step** identifiers MUST NOT contain `.` + Used by objective windows. The workflow portion may be a composition-generated + qualified name and the step is exactly one portable local-id segment ([document-model.md §6](document-model.md)). 5. **Module-composed (namespaced)** — after a module import is expanded, imported elements are addressed under their import namespace, and node segments are @@ -33,13 +33,11 @@ A reference is a string that names a target element. Five forms exist: the expanded document ([ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md)). -### Dotted node identifiers - -A node identifier MAY itself contain `.` (e.g. `wazuh.manager`). A qualified -reference that traverses a node segment therefore resolves the **longest** -node-identifier match rather than splitting on the first `.`. Resolution MUST -account for dotted node names so that `nodes.wazuh.manager.runtime.…` addresses -the `wazuh.manager` node, not a `wazuh` node with a `manager` member. +Dots are path syntax, never authored identifier content. A dotted node key in a +raw or normalized authoring object is invalid. A dotted node segment seen after +composition is a validated namespace path and is carried structurally until the +canonical renderer produces the external string; it is not recovered with a +longest-match rule. ## 2. Resolution algorithm @@ -47,11 +45,15 @@ the `wazuh.manager` node, not a `wazuh` node with a `manager` member. referencing field accepts. 2. A field defines its **candidate set** — the section or sections a value may name. Some fields accept a single section (e.g. an objective's `success` → - `conditions`); others accept a set of targetable sections (e.g. an + `assertions`); others accept a set of targetable sections (e.g. an objective's `target`, a relationship's `source`/`target`). The candidate set is part of each field's definition and is reflected in the edge catalog (§5). 3. A **bare** reference resolves against the candidate set. A **qualified** - reference resolves against the named section/path and MUST match it exactly. + reference resolves by exact lookup of the typed canonical address and + **MUST** match it exactly. Implementations **MUST NOT** discover ownership by + `split`, `partition`, `rsplit`, longest-prefix guessing, declaration order, + or first match. Compact aliases such as `.` are + constructed and resolved from declared workflow/step pairs. 4. Some targetable candidate sets are deliberately restricted. For example, an objective `target` excludes the `variables`, `objectives`, and `workflows` prefixes; an agent `operating_scope` is restricted to VM nodes, @@ -59,6 +61,10 @@ the `wazuh.manager` node, not a `wazuh` node with a `manager` member. field's candidate set does not resolve and fails as dangling (§4). 5. Resolution is **declaration-based**: only declared elements are resolution targets. There is no implicit creation of a target by referencing it. +6. Alias lookup occurs only after the canonical declaration index has retained + kind and provenance for every declaration and rejected address collisions. + A set or map that has already erased a duplicate rendering is not evidence of + uniqueness. ## 3. Unresolved variable placeholders @@ -72,6 +78,10 @@ the `wazuh.manager` node, not a `wazuh` node with a `manager` member. 3. After instantiation substitutes a concrete value, the normal reference rules (§2) apply to that value. A reference that becomes dangling or ambiguous only after substitution fails at instantiation. +4. A direct or deserialized instantiated artifact is not exempt. Its required + provenance can explain binding and resolution inputs, but it does not create + declarations or authorize references. Artifact admission reruns the same + declaration-index and reference checks before compilation or snapshotting. ## 4. Failure semantics (fail-closed) @@ -102,15 +112,15 @@ The SDL carries no graded scoring pipeline: the OCR-inherited `metrics`, `evaluations`, `tlos`, and `goals` sections were removed with [ADR-073](../../docs/decisions/adrs/adr-073-scoring-reward-language-scope.md), so no reference edge targets them. Graded scoring, reward, and evaluation outputs -live in the experiment/evaluator plane (ADR-055/064/069). `conditions` remain the -observable-state target for objective success and workflow predicates. +live in the experiment/evaluator plane (ADR-055/064/069). `conditions` remain +probe implementations; propositions and assertions carry portable truth. ### Narrative chain | Source | Field | Target | |--------|-------|--------| | `injects` | from/to entity | `entities` | -| `events` | condition refs | `conditions` | +| `events` | precondition assertion refs | `assertions` | | `events` | inject refs | `injects` | | `scripts` | event refs | `events` | | `stories` | script refs | `scripts` | @@ -136,14 +146,14 @@ observable-state target for objective success and workflow predicates. | `agents` | subnets / initial-knowledge subnets | switch-backed `infrastructure` | | `agents` | initial-knowledge hosts | `nodes` (VM) | | `agents` | initial-knowledge services | declared services on nodes | -| `agents` | starting conditions | `conditions` | +| `agents` | starting assertions | `assertions` (preconditions) | | `agents` | actions / observation boundaries | `action_contracts` / `observation_boundaries` | | `action_contracts` | interaction related-action | `action_contracts` | | `observation_boundaries` | view-rule information refs | own observable/hidden/evidence refs | | `objectives` | actor | `agents` or flattened `entities` | | `objectives` | action | the bound agent's `action_contracts` | | `objectives` | target | targetable elements (excl. `variables`/`objectives`/`workflows`) | -| `objectives` | success criteria | `conditions` (observable state only, [ADR-073](../../docs/decisions/adrs/adr-073-scoring-reward-language-scope.md)) | +| `objectives` | success criteria | `assertions` (invariants/postconditions, [ADR-079](../../docs/decisions/adrs/adr-079-backend-neutral-proposition-and-truth-semantics.md)) | | `objectives` | window | `stories`/`scripts`/`events`/`workflows` (with closure rules) | | `objectives` | depends_on | `objectives` (acyclic) | | `outcome_interpretation_rules` | source | `action_contracts`/`objectives`/`workflows` | @@ -171,7 +181,7 @@ element is the source or channel. | `workflows` | start | own steps | | `workflows` | step successors (`on_success`/`on_failure`) | own steps | | `workflows` | compensation | other `workflows` | -| `workflows` | predicate condition refs | `conditions` (observable state) | +| `workflows` | predicate assertion refs | `assertions` (preconditions) | | `workflows` | predicate step refs | own steps (executable) | Parallel/join control flow MUST be closed: every branch reaches its join and no @@ -198,6 +208,77 @@ any role-bearing refs |--------|-------|--------| | any field | `${name}` placeholder | a declared `variables` entry (name only, at authoring time) | +## 6. Machine-checkable reference-edge index + +This index gives every editor-visible reference field a stable candidate-domain +token and makes the participant behavior surface explicit. It complements the +semantic detail above: subtype-specific relationship and nested-runtime rules +remain narrower than the broad completion domain recorded here. `targetable` +means the declaration index excluding `variables`, `evidence_requirements`, +`objectives`, and `workflows`; it is not a synonym for every named object. +`derived:*`, `vocabulary:*`, `registry:*`, `contract:*`, and `opaque:*` name +deliberately distinct resolution mechanisms and MUST NOT be collapsed into a +generic symbol lookup. + +| Source path | Candidate domain | Resolution phase | Failure | Semantic owner | +| --- | --- | --- | --- | --- | +| `nodes.*.features[]` | `features` | semantic validation | fatal dangling or ambiguous | [node validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py) | +| `nodes.*.conditions[]` | `conditions` | semantic validation | fatal dangling or ambiguous | [node validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py) | +| `conditions.*.proposition` | `propositions` | semantic validation | fatal dangling or ambiguous when present | [proposition validator](../../implementations/python/packages/aces_sdl/validator/_propositions.py) | +| `propositions.*.subjects[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [proposition validator](../../implementations/python/packages/aces_sdl/validator/_propositions.py) | +| `propositions.*.evidence_requirements[]` | `evidence_requirements` | semantic validation | fatal dangling or ambiguous | [proposition validator](../../implementations/python/packages/aces_sdl/validator/_propositions.py) | +| `assertions.*.proposition` | `propositions` | semantic validation | fatal dangling or ambiguous | [proposition validator](../../implementations/python/packages/aces_sdl/validator/_propositions.py) | +| `nodes.*.injects[]` | `injects` | semantic validation | fatal dangling or ambiguous | [node validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py) | +| `nodes.*.vulnerabilities[]` | `vulnerabilities` | semantic validation | fatal dangling or ambiguous | [node validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py) | +| `infrastructure.*.links[]` | `infrastructure` | semantic validation | fatal dangling or ambiguous | [infrastructure validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py) | +| `infrastructure.*.dependencies[]` | `infrastructure` | semantic validation | fatal dangling or ambiguous | [infrastructure validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py) | +| `features.*.dependencies[]` | `features` | semantic validation | fatal dangling, ambiguous, or cyclic | [section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py) | +| `entities.*.vulnerabilities[]` | `vulnerabilities` | semantic validation | fatal dangling or ambiguous | [section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py) | +| `injects.*.from_entity` | `entities` | semantic validation | fatal dangling or ambiguous | [section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py) | +| `injects.*.to_entities[]` | `entities` | semantic validation | fatal dangling or ambiguous | [section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py) | +| `events.*.assertions[]` | `assertions` | semantic validation | fatal dangling, ambiguous, or non-precondition role | [proposition validator](../../implementations/python/packages/aces_sdl/validator/_propositions.py) | +| `events.*.injects[]` | `injects` | semantic validation | fatal dangling or ambiguous | [section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py) | +| `scripts.*.events[]` | `events` | semantic validation | fatal dangling or ambiguous | [section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py) | +| `stories.*.scripts[]` | `scripts` | semantic validation | fatal dangling or ambiguous | [section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py) | +| `content.*.target` | `nodes` | semantic validation | fatal unless target is a VM node | [content validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py) | +| `accounts.*.node` | `nodes` | semantic validation | fatal unless target is a VM node | [account validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py) | +| `relationships.*.source` | `targetable` | semantic validation | fatal dangling or ambiguous; subtype may narrow domain | [relationship validator](../../implementations/python/packages/aces_sdl/validator/_relationships.py) | +| `relationships.*.target` | `targetable` | semantic validation | fatal dangling or ambiguous; subtype may narrow domain | [relationship validator](../../implementations/python/packages/aces_sdl/validator/_relationships.py) | +| `agents.*.entity` | `entities` | semantic validation | fatal dangling or ambiguous | [participant validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py) | +| `agents.*.starting_accounts[]` | `accounts` | semantic validation | fatal dangling or ambiguous | [participant validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py) | +| `agents.*.starting_assertions[]` | `assertions` | semantic validation | fatal dangling, ambiguous, or non-precondition role | [proposition validator](../../implementations/python/packages/aces_sdl/validator/_propositions.py) | +| `action_contracts.*.interactions.*.related_action_ref` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [participant semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `observation_boundaries.*.view_rules.*.information_refs[]` | `derived:boundary_information` | semantic validation | fatal outside declared boundary information | [participant semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `outcome_interpretation_rules.*.source_ref` | `action_contracts,objectives,workflows` | semantic validation | fatal dangling or ambiguous | [outcome semantics](../../implementations/python/packages/aces_sdl/semantics/participant_outcome.py) | +| `behavior_specifications.*.participant_refs[]` | `agents` | semantic validation | fatal dangling or ambiguous | [behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `behavior_specifications.*.participant_role_refs[]` | `derived:agent_roles` | semantic validation | fatal unless bound by a referenced participant | [behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `behavior_specifications.*.action_contract_refs[]` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `behavior_specifications.*.observation_boundary_refs[]` | `observation_boundaries` | semantic validation | fatal dangling or ambiguous | [behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `behavior_specifications.*.outcome_interpretation_rule_refs[]` | `outcome_interpretation_rules` | semantic validation | fatal dangling or ambiguous | [behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `behavior_specifications.*.authority_scope_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [behavior validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py) | +| `behavior_specifications.*.behavior_mode` | `vocabulary:behavior_mode` | structural validation | fatal invalid vocabulary value | [behavior model](behavior-specifications.md) | +| `behavior_specifications.*.ai_offensive_behavior_refs[]` | `vocabulary:ai_offensive_behavior` | semantic validation | fatal unknown vocabulary identifier | [behavior model](behavior-specifications.md) | +| `behavior_specifications.*.offensive_behavior_refs[]` | `vocabulary:offensive_behavior` | semantic validation | fatal unknown vocabulary identifier | [behavior model](behavior-specifications.md) | +| `behavior_specifications.*.realization_profile_ref` | `opaque:realization_profile` | structural validation | fatal invalid reference shape; resolution belongs to realization | [behavior model](behavior-specifications.md) | +| `behavior_specifications.*.backend_feature_support_refs[]` | `registry:behavior_features` | semantic validation | fatal unsupported feature identifier | [behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `behavior_specifications.*.evidence_contract_refs[]` | `contract:participant_evidence` | semantic validation | fatal unknown contract identifier | [behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py) | +| `evidence_requirements.*.source_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence validator](../../implementations/python/packages/aces_sdl/validator/_evidence_requirements.py) | +| `evidence_requirements.*.scope_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence validator](../../implementations/python/packages/aces_sdl/validator/_evidence_requirements.py) | +| `evidence_requirements.*.channel_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence validator](../../implementations/python/packages/aces_sdl/validator/_evidence_requirements.py) | +| `evidence_requirements.*.trigger_ref` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence validator](../../implementations/python/packages/aces_sdl/validator/_evidence_requirements.py) | +| `evidence_requirements.*.boundary_ref` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence validator](../../implementations/python/packages/aces_sdl/validator/_evidence_requirements.py) | +| `objectives.*.agent` | `agents` | semantic validation | fatal dangling or ambiguous | [objective semantics](objective-semantics.md) | +| `objectives.*.entity` | `entities` | semantic validation | fatal dangling or ambiguous | [objective semantics](objective-semantics.md) | +| `objectives.*.targets[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [objective semantics](objective-semantics.md) | +| `objectives.*.success.assertions[]` | `assertions` | semantic validation | fatal dangling, ambiguous, or precondition role | [objective semantics](objective-semantics.md) | +| `objectives.*.depends_on[]` | `objectives` | semantic validation | fatal dangling, ambiguous, or cyclic | [objective semantics](objective-semantics.md) | +| `workflows.*.start` | `workflow_steps` | semantic validation | fatal dangling step | [workflow semantics](workflow-semantics.md) | +| `workflows.*.steps.*.when.assertions[]` | `assertions` | semantic validation | fatal dangling, ambiguous, or non-precondition role | [workflow semantics](workflow-semantics.md) | + +The index is checked against language-service completion metadata and against a +required behavior-edge set. Adding a completion-aware field or behavior +reference without a corresponding row fails the repository contract gate. + ## Extending the reference catalog A new reference edge is added by defining the field's candidate set, adding a diff --git a/specs/sdl/runtime-inventory.md b/specs/sdl/runtime-inventory.md index 8d9c6d7fc..e6a495bb9 100644 --- a/specs/sdl/runtime-inventory.md +++ b/specs/sdl/runtime-inventory.md @@ -31,25 +31,25 @@ under `runtime`, a primary `_id`, an addressable child-collection tree, and an owning ADR. The owning ADR is the normative authority for that family's fields, enums, and profiles; this index does not restate them. -| Family key | `runtime.` | Primary id | Child collections (id) | Owning ADR | +| Family key | `runtime.` | Primary id | Addressable child paths (`collection:id`) | Owning ADR | |------------|------------------------|------------|------------------------|-----------| -| `service-listeners` | `service_listeners` | `service_listener_id` | — | [ADR-043](../../docs/decisions/adrs/adr-043-runtime-service-listener-surface.md) | -| `applications` | `applications` | `application_id` | — | [ADR-026](../../docs/decisions/adrs/adr-026-application-http-surface-inventory.md) | -| `database-services` | `database_services` | `database_service_id` | `databases` (`database_id`) | [ADR-029](../../docs/decisions/adrs/adr-029-database-logical-state-runtime-surface.md) | -| `dns-services` | `dns_services` | `dns_service_id` | `zones` (`zone_id`) → `rrsets` (`rrset_id`) | [ADR-039](../../docs/decisions/adrs/adr-039-dns-service-runtime-inventory.md) | -| `identity-authorities` | `identity_authorities` | `identity_authority_id` | `services`, `subjects`, `policies`, `relationships` | [ADR-032](../../docs/decisions/adrs/adr-032-directory-domain-identity-runtime-surface.md) | -| `file-services` | `file_services` | `file_service_id` | `shares`, `principals`, `access_rules`, `access_observations` | [ADR-037](../../docs/decisions/adrs/adr-037-runtime-file-service-and-filesystem-presence-semantics.md) | -| `mail-services` | `mail_services` | `mail_service_id` | `components`, `listeners`, `domains`, `mailbox_stores`, `mailboxes`, `aliases`, `routing_rules`, `queues`, `settings` | [ADR-038](../../docs/decisions/adrs/adr-038-runtime-mail-service-logical-state.md) | -| `network-sensors` | `network_sensors` | `network_sensor_id` | — | [ADR-042](../../docs/decisions/adrs/adr-042-network-sensor-runtime-monitoring.md) | -| `network-detection-engines` | `network_detection_engines` | `network_detection_engine_id` | `rule_sources`, `network_sets`, `output_streams`, `control_channels` | [ADR-044](../../docs/decisions/adrs/adr-044-network-detection-engine-runtime-inventory.md) | -| `security-monitoring-managers` | `security_monitoring_managers` | `security_monitoring_manager_id` | `listeners`, `components`, `agents`, `agent_groups`, `content_sets`, `detection_definitions`, `settings` | [ADR-040](../../docs/decisions/adrs/adr-040-security-monitoring-manager-runtime-inventory.md), [ADR-045](../../docs/decisions/adrs/adr-045-security-monitoring-detection-definition-semantics.md) | -| `ssh-servers` | `ssh_servers` | `ssh_server_id` | `match_rules` (`match_id`) | [ADR-031](../../docs/decisions/adrs/adr-031-ssh-server-configuration-surface.md) | -| `app-authorizations` | `app_authorizations` | `app_authorization_id` | `principals`, `roles`, `permission_grants`, `role_mappings`, `tenants` | [ADR-046](../../docs/decisions/adrs/adr-046-app-authorization-runtime-inventory.md) | -| `scheduled-jobs` | `scheduled_jobs` | `scheduled_job_id` | — | [ADR-047](../../docs/decisions/adrs/adr-047-scheduled-job-runtime-inventory.md) | -| `datastore-services` | `datastore_services` | `datastore_service_id` | `nodes` (`node_id`) → `plugins`, `endpoints`; `partitions`, `templates`, `mappings`, `settings` | [ADR-048](../../docs/decisions/adrs/adr-048-datastore-service-runtime-inventory.md), [ADR-058](../../docs/decisions/adrs/adr-058-datastore-node-engine-provenance-and-endpoints.md) | -| `platform-applications` | `platform_applications` | `platform_application_id` | `organizations`, `tenants`, `content_objects`, `markings`, `upstream_bindings`, `connectors`, `settings` | [ADR-049](../../docs/decisions/adrs/adr-049-platform-application-runtime-inventory.md) | -| `forwarding-agents` | `forwarding_agents` | `forwarding_agent_id` | `sources`, `transforms`, `ship_targets`, `reload_channels`, `settings` | [ADR-050](../../docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md) | -| `orchestration-authorities` | `orchestration_authorities` | `orchestration_authority_id` | `spawn_templates`, `realized_children` | [ADR-051](../../docs/decisions/adrs/adr-051-orchestration-authority-runtime-inventory.md) | +| `service-listeners` | `service_listeners` | `service_listener_id` | none | [ADR-043](../../docs/decisions/adrs/adr-043-runtime-service-listener-surface.md) | +| `applications` | `applications` | `application_id` | none | [ADR-026](../../docs/decisions/adrs/adr-026-application-http-surface-inventory.md) | +| `database-services` | `database_services` | `database_service_id` | `databases:database_id` | [ADR-029](../../docs/decisions/adrs/adr-029-database-logical-state-runtime-surface.md) | +| `dns-services` | `dns_services` | `dns_service_id` | `zones:zone_id, zones:zone_id/rrsets:rrset_id` | [ADR-039](../../docs/decisions/adrs/adr-039-dns-service-runtime-inventory.md) | +| `identity-authorities` | `identity_authorities` | `identity_authority_id` | `services:service_id, subjects:subject_id, policies:policy_id, relationships:relationship_id` | [ADR-032](../../docs/decisions/adrs/adr-032-directory-domain-identity-runtime-surface.md) | +| `file-services` | `file_services` | `file_service_id` | `shares:share_id, principals:principal_id, access_rules:rule_id, access_observations:observation_id` | [ADR-037](../../docs/decisions/adrs/adr-037-runtime-file-service-and-filesystem-presence-semantics.md) | +| `mail-services` | `mail_services` | `mail_service_id` | `components:component_id, listeners:listener_id, domains:domain_id, mailbox_stores:store_id, mailboxes:mailbox_id, aliases:alias_id, routing_rules:rule_id, queues:queue_id, settings:setting_id` | [ADR-038](../../docs/decisions/adrs/adr-038-runtime-mail-service-logical-state.md) | +| `network-sensors` | `network_sensors` | `network_sensor_id` | none | [ADR-042](../../docs/decisions/adrs/adr-042-network-sensor-runtime-monitoring.md) | +| `network-detection-engines` | `network_detection_engines` | `network_detection_engine_id` | `rule_sources:source_id, network_sets:set_id, output_streams:stream_id, control_channels:channel_id` | [ADR-044](../../docs/decisions/adrs/adr-044-network-detection-engine-runtime-inventory.md) | +| `security-monitoring-managers` | `security_monitoring_managers` | `security_monitoring_manager_id` | `listeners:listener_id, components:component_id, agents:agent_id, agent_groups:group_id, content_sets:content_id, detection_definitions:definition_id, settings:setting_id` | [ADR-040](../../docs/decisions/adrs/adr-040-security-monitoring-manager-runtime-inventory.md), [ADR-045](../../docs/decisions/adrs/adr-045-security-monitoring-detection-definition-semantics.md) | +| `ssh-servers` | `ssh_servers` | `ssh_server_id` | `match_rules:match_id` | [ADR-031](../../docs/decisions/adrs/adr-031-ssh-server-configuration-surface.md) | +| `app-authorizations` | `app_authorizations` | `app_authorization_id` | `principals:principal_id, roles:role_id, permission_grants:grant_id, role_mappings:mapping_id, tenants:tenant_id` | [ADR-046](../../docs/decisions/adrs/adr-046-app-authorization-runtime-inventory.md) | +| `scheduled-jobs` | `scheduled_jobs` | `scheduled_job_id` | none | [ADR-047](../../docs/decisions/adrs/adr-047-scheduled-job-runtime-inventory.md) | +| `datastore-services` | `datastore_services` | `datastore_service_id` | `nodes:node_id, nodes:node_id/plugins:plugin_id, nodes:node_id/endpoints:endpoint_id, partitions:partition_id, templates:template_id, mappings:mapping_id, settings:setting_id` | [ADR-048](../../docs/decisions/adrs/adr-048-datastore-service-runtime-inventory.md), [ADR-058](../../docs/decisions/adrs/adr-058-datastore-node-engine-provenance-and-endpoints.md) | +| `platform-applications` | `platform_applications` | `platform_application_id` | `organizations:organization_id, tenants:tenant_id, content_objects:content_object_id, markings:marking_id, upstream_bindings:binding_id, connectors:connector_id, settings:setting_id` | [ADR-049](../../docs/decisions/adrs/adr-049-platform-application-runtime-inventory.md) | +| `forwarding-agents` | `forwarding_agents` | `forwarding_agent_id` | `sources:source_id, transforms:transform_id, ship_targets:target_id, reload_channels:reload_channel_id, settings:setting_id` | [ADR-050](../../docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md) | +| `orchestration-authorities` | `orchestration_authorities` | `orchestration_authority_id` | `spawn_templates:template_id, realized_children:workload_id` | [ADR-051](../../docs/decisions/adrs/adr-051-orchestration-authority-runtime-inventory.md) | The node-scoped `forwarding_agents` family is distinct from the scenario-level `forwarding_agents` authoring section ([sections.md](sections.md)); they share @@ -82,8 +82,9 @@ not contradict these. 1. **Identity (`_id`).** Every family element and every addressable child element carries a stable `_id`. The id **MUST** be unique within its - collection and symbol-shaped (no whitespace or quoting that would make it - unaddressable in a qualified path; [document-model.md §6](document-model.md)). + collection and use the portable local-identifier grammar + ([document-model.md §6](document-model.md)). Runtime/native/provider ids that + are not ACES-local declaration identities retain their owning contracts. References address elements by these ids ([references.md](references.md)). 2. **Enum sentinels.** An open enum carries a closed core of well-defined values plus the sentinels `unknown` and `other`, so an authored value can record diff --git a/specs/sdl/scientific-scenario-completeness.md b/specs/sdl/scientific-scenario-completeness.md new file mode 100644 index 000000000..450da0891 --- /dev/null +++ b/specs/sdl/scientific-scenario-completeness.md @@ -0,0 +1,116 @@ +# Scientific-Scenario Completeness Profiles + +Status: **normative**. + +This specification defines the REV1 intended-use scope contract required by +GOV-942. It answers which semantic concerns are required, may remain +underspecified, or are excluded for a stated scenario-use claim, and joins that +stable taxonomy to a separately versioned assessment of what ACES delivers. + +It does not change ordinary SDL validity. A document containing only `name` may +be a valid SDL fragment while being unsuitable for deployment, participant +evaluation, controlled experimentation, or a reproducible benchmark. + +## Normative Artifacts + +The two machine-readable authorities are: + +- [REV1 taxonomy](../../contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json), + which owns atomic concern definitions, the five profile meanings, profile + dispositions, and explicit non-claims; and +- [2026-07-12 delivery assessment](../../contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json), + which owns current delivery status, evidence, limitations, external-contract + bindings, and issue references. + +The taxonomy revision and assessment revision are independent. Implementing a +missing concern updates or supersedes the assessment. Changing what a concern +or profile means requires a new taxonomy revision. The join identity is: + +```text +(profile_family, taxonomy_revision, assessment_revision, profile_id, concern_id) +``` + +## Disposition And Delivery + +Profile disposition and delivery status are different dimensions: + +| Dimension | Values | +| --- | --- | +| Profile disposition | `required`, `allowed-underspecified`, `excluded` | +| Delivery status | `implemented`, `partial`, `external-contract`, `deliberately-excluded`, `missing` | + +`external-contract` means that a named, versioned non-SDL contract supplies the +concern, has at least one conforming checked-in instance as a satisfiability +witness, and states a binding obligation to the same scenario/run lineage. The +witness establishes that the structural contract is non-empty. It does not +prove that a backend can realize the obligation or that a particular run +satisfied it. A formal design, accepted ADR, open issue, example mention, or +schema property by itself is not implementation evidence. + +For profile `p`, concern `c`, taxonomy `T`, and assessment `A`: + +```text +Complete(T, A, p) iff + A.taxonomy_revision = T.revision + and concerns(A) = concerns(T) + and for every c where T[p,c] = required, + A[c].status is implemented or external-contract. +``` + +An implemented status requires executable evidence. An external-contract +status requires evidence for every named contract, a conforming satisfiability +witness, and an explicit binding obligation. `partial`, `missing`, and +`deliberately-excluded` never satisfy a required row. Completeness is computed; +it is not stored as a mutable boolean. + +## REV1 Outcomes + +The following block is checked mechanically against the normative artifacts. + + +| Profile | Complete | Blocking required concerns | +| --- | --- | --- | +| `valid-sdl-fragment` | yes | none | +| `deployable-scenario-intent` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `credential-intent`, `scoped-specificity-open-world-intent` | +| `participant-evaluation-scenario` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `behavioral-relation-taxonomy`, `clock-declaration`, `credential-intent`, `deadlines-and-windows`, `flexible-step-tooling`, `pacing-and-synchronization`, `participant-action-observation`, `participant-budgets`, `participant-episode-reset`, `portable-behavior-contracts`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication` | +| `controlled-experiment-scenario` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `behavioral-relation-taxonomy`, `clock-declaration`, `credential-intent`, `deadlines-and-windows`, `host-architecture-constraints`, `pacing-and-synchronization`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication` | +| `reproducible-benchmark-study-input` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `behavioral-relation-taxonomy`, `clock-declaration`, `credential-intent`, `credential-materialization`, `deadlines-and-windows`, `flexible-step-tooling`, `hidden-benchmark-assets`, `host-architecture-constraints`, `pacing-and-synchronization`, `participant-action-observation`, `participant-budgets`, `participant-episode-reset`, `portable-behavior-contracts`, `reference-trajectories`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication`, `weakness-exploitability-semantics` | + + +Only a computed-complete profile may cite a minimal example as completeness +evidence. The current example for `valid-sdl-fragment` is admitted through the +production SDL parser. An incomplete stronger profile may have an illustration, +but that illustration MUST be labeled as a gap illustration and MUST NOT be +used to upgrade the profile. + +## Composition Boundary + +The stronger profiles compose SDL with existing experiment and evidence +contracts. Factors, allocation, stochastic controls, apparatus context, runs, +studies, captured evidence, and associated artifacts do not become new +`Scenario` fields. A binding obligation must connect those artifacts to the +same instantiated scenario and derivation lineage. + +Profile validity is also distinct from: + +- ASR-511/515 validation or admission strength; +- semantic and backend capability profiles; +- backend-relative realization support; +- successful deployment or finite conformance probes; +- statistical validity, replication, or generalizability; and +- trace equivalence, refinement, observational equivalence, Park-Milner + bisimulation, epistemic equivalence, or multi-agent strategic equivalence. + +Issue #747 owns the behavioral-relation taxonomy. Until that work is delivered, +REV1 records the concern as missing and stronger profiles that require it remain +incomplete. Passing the same bounded probes or producing equal result tuples is +not evidence of bisimulation. + +## Security And Evidence Boundary + +Profile artifacts contain only identifiers, classifications, repository paths, +contract ids, issue references, and bounded claim summaries. They MUST NOT +contain credentials, bearer tokens, private keys, hidden answer material, raw +evidence, backend objects, prompts, environment dumps, or executable commands. +Evidence paths are fixed repository-relative paths and are checked for +containment and existence. diff --git a/specs/sdl/sections.md b/specs/sdl/sections.md index fb1b65906..d33776ed9 100644 --- a/specs/sdl/sections.md +++ b/specs/sdl/sections.md @@ -22,53 +22,55 @@ resolution rules and full reference-edge catalog with failure semantics are in [`references.md`](references.md). A blank "References" cell means the section is referenced by others but does not itself reference another section. -## Metadata and composition fields - -These describe the document and its composition. They are **not** authoring -sections. - -| Field | Shape | Required | Notes | -|-------|-------|----------|-------| -| `name` | scalar | **REQUIRED** | The scenario identity. The only required top-level field. | -| `version` | scalar | optional (default `*`) | Scenario version; `*` means unpinned. | -| `description` | scalar | optional (default empty) | Free-text description. | -| `module` | mapping \| null | optional (default null) | Published module metadata when this document is a composable module: a canonical `publisher/name` id, a `version`, declared `parameters`, and `exports` ([ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md)). | -| `imports` | list | optional (default empty) | Module imports. Each import names a module by `source` (or the deprecated `path`) and binds it under a `namespace` with `parameters`. Imports are expanded before full semantic validation ([document-model.md §7](document-model.md)). | - -## Authoring sections — map-keyed - -Each is a map keyed by a user-defined identifier ([document-model.md §6](document-model.md)) -and defaults to an empty map when omitted. - -| Section | Required | Key shape | References | -|---------|----------|-----------|------------| -| `nodes` | optional | identifier ≤ 35 chars; may contain `.` | `features`, `conditions`, `injects`, `vulnerabilities`; hosts the runtime inventory ([runtime-inventory.md](runtime-inventory.md)) | -| `infrastructure` | optional | identifier matching a node | `nodes`; switch/network nodes; other `infrastructure` (dependencies) | -| `features` | optional | identifier | `vulnerabilities`; other `features` (dependencies, acyclic) | -| `conditions` | optional | identifier | — | -| `vulnerabilities` | optional | identifier | — | -| `entities` | optional | identifier | `vulnerabilities` | -| `injects` | optional | identifier | `entities` | -| `events` | optional | identifier | `conditions`, `injects` | -| `scripts` | optional | identifier | `events` | -| `stories` | optional | identifier | `scripts` | -| `content` | optional | identifier | `nodes` (VM target) | -| `accounts` | optional | identifier | `nodes` (VM) | -| `relationships` | optional | identifier | typed by subtype: `entities`/`accounts`/targetable elements; runtime families (`applications`, `database_services`, `mail_services`, `platform_applications`, `app_authorizations`); scenario `forwarding_agents` ([ADR-052](../../docs/decisions/adrs/adr-052-typed-runtime-relationship-subtypes.md)) | -| `agents` | optional | identifier | `entities`, `accounts`, `infrastructure`, `nodes`, `conditions`, `action_contracts`, `observation_boundaries`, targetable elements | -| `action_contracts` | optional | identifier | other `action_contracts` (interactions) | -| `observation_boundaries` | optional | identifier | own information refs (observable/hidden/evidence) | -| `outcome_interpretation_rules` | optional | identifier | `action_contracts`, `objectives`, `workflows` | -| `evidence_requirements` | optional | identifier | targetable elements for source, scope, channel, trigger, and boundary refs; distinct from `objectives` and scenario-native observability systems ([observability-and-evidence.md](observability-and-evidence.md)) | -| `objectives` | optional | identifier | `agents`/`entities` (actor), `action_contracts` (action), targetable elements (target), `conditions` (success — observable state only, [ADR-073](../../docs/decisions/adrs/adr-073-scoring-reward-language-scope.md)), `stories`/`scripts`/`events`/`workflows` (window), other `objectives` (depends_on, acyclic) | -| `workflows` | optional | identifier | own steps (`start`, successors), other `workflows` (compensation), `conditions` (predicates) | -| `variables` | optional | identifier matching `[A-Za-z_][A-Za-z0-9_-]*` | referenced by `${…}` placeholders ([variables-and-instantiation.md](variables-and-instantiation.md)) | - -## Authoring section — list-valued - -| Section | Shape | Required | Element identity | Referenced by | -|---------|-------|----------|------------------|---------------| -| `forwarding_agents` | list | optional (default empty) | `forwarding_agent_id` on each element | `relationships` (`forwarding_edge.forwarder_ref`) | +## Complete top-level field catalog + +This table is the complete, mechanically checked top-level language surface. +"Lifecycle" names the document forms in which the field is carried. A +composition field marked `expanded-empty` or `instantiated-empty` remains in the +model with its empty default after module expansion; its authored composition +instructions do not survive as executable scenario meaning. "References" is +`catalogued` when the field owns at least one row in the exact edge index in +[`references.md`](references.md). + +| Field | Kind | Shape | Lifecycle | Presence/default | Identity | References | Semantic owner | +| --- | --- | --- | --- | --- | --- | --- | --- | +| `name` | metadata | scalar | normalized, expanded, instantiated | required | `scenario_name` | none | [document model](document-model.md) | +| `version` | metadata | scalar | normalized, expanded, instantiated | optional; default `*` | none | none | [document model](document-model.md) | +| `description` | metadata | scalar | normalized, expanded, instantiated | optional; default empty string | none | none | [document model](document-model.md) | +| `module` | composition | mapping | normalized, expanded-empty, instantiated-empty | optional; default null | `module.id` | none | [ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md) | +| `imports` | composition | list | normalized, expanded-empty, instantiated-empty | optional; default empty list | `namespace` | none | [ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md) | +| `nodes` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [nodes and runtime inventory](runtime-inventory.md) | +| `infrastructure` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [document model](document-model.md) | +| `features` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [document model](document-model.md) | +| `conditions` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [proposition semantics](../../specs/formal/objectives/proposition-and-assertion-semantics.md) | +| `propositions` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [proposition semantics](../../specs/formal/objectives/proposition-and-assertion-semantics.md) | +| `assertions` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [proposition semantics](../../specs/formal/objectives/proposition-and-assertion-semantics.md) | +| `vulnerabilities` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | none | [document model](document-model.md) | +| `entities` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [document model](document-model.md) | +| `injects` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [reference catalog](references.md) | +| `events` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [reference catalog](references.md) | +| `scripts` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [reference catalog](references.md) | +| `stories` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [reference catalog](references.md) | +| `content` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [document model](document-model.md) | +| `accounts` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [document model](document-model.md) | +| `relationships` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [ADR-052](../../docs/decisions/adrs/adr-052-typed-runtime-relationship-subtypes.md) | +| `forwarding_agents` | section | list | normalized, expanded, instantiated | optional; default empty list | `forwarding_agent_id` | none | [ADR-050](../../docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md) | +| `agents` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [participant model](participant-model.md) | +| `action_contracts` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [participant model](participant-model.md) | +| `observation_boundaries` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [participant model](participant-model.md) | +| `outcome_interpretation_rules` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [participant model](participant-model.md) | +| `behavior_specifications` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [behavior specifications](behavior-specifications.md) | +| `evidence_requirements` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [observability and evidence](observability-and-evidence.md) | +| `objectives` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [objective semantics](objective-semantics.md) | +| `workflows` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [workflow semantics](workflow-semantics.md) | +| `variables` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | none | [variables and instantiation](variables-and-instantiation.md) | + + + +The section set therefore has two authoring shapes: maps keyed by stable +user-defined identifiers and the scenario-level `forwarding_agents` list, whose +elements carry their own stable identity. The checked summary above is derived +from the rows; changing a row without reconciling it fails the contract gate. `forwarding_agents` is the **scenario-level** forwarding-agent inventory. It is distinct from the node-scoped `forwarding_agents` runtime-family collection that @@ -83,11 +85,15 @@ ordering is normative (resolution and failure semantics in [`references.md`](references.md)): - **Narrative chain:** `injects` → `events` → `scripts` → `stories`, with - `injects` naming `entities` and `events` naming `conditions`. Objective + `injects` naming `entities` and `events` naming precondition assertions. Objective windows bind `stories`/`scripts`/`events`/`workflows`. -`conditions` are observable state: an objective's success is expressed against -`conditions`, and workflow predicates reference `conditions`. The SDL carries no +`propositions` state typed claims; `assertions` use them as preconditions, +invariants, or postconditions. Objective success composes invariant or +postcondition assertions, while events and workflow predicates reference +precondition assertions. `conditions` are executable probe declarations and +must explicitly identify the proposition they realize; they are not observable +facts. The SDL carries no graded scoring pipeline — the OCR-inherited `metrics`, `evaluations`, `tlos` (Training Learning Objectives), and `goals` sections were removed with [ADR-073](../../docs/decisions/adrs/adr-073-scoring-reward-language-scope.md). diff --git a/specs/sdl/variables-and-instantiation.md b/specs/sdl/variables-and-instantiation.md index 77a6537ad..a30bbabf5 100644 --- a/specs/sdl/variables-and-instantiation.md +++ b/specs/sdl/variables-and-instantiation.md @@ -29,7 +29,8 @@ Type conformance rules, enforced when the variable is defined: 3. If both `default` and `allowed_values` are set, `default` **MUST** be a member of `allowed_values`. -The variable **name** (the map key) **MUST** match `[A-Za-z_][A-Za-z0-9_-]*` +The variable **name** (the map key) **MUST** use the portable local-identifier +grammar `^[a-z0-9][a-z0-9_-]{0,63}$` ([document-model.md §6](document-model.md)). ## 2. Reference syntax @@ -48,6 +49,12 @@ A placeholder **MUST NOT** appear in an identifier-defining map key ([document-model.md §6](document-model.md)); variables parameterise values, not identities. +Module parameter names, import-parameter keys, and +`scenario-instantiation-request-v1.parameters` keys use the same grammar. +Parameter values retain their owning field types. For any declaration `d` and +two valid parameter environments `p1` and `p2`, canonical identity is invariant: +`address(d, p1) = address(d, p2)`. + Variables are **not** resolved at parse time. An authored document preserves `${…}` placeholders structurally; resolution happens only at instantiation. Authoring-time semantic validation checks every `${name}` token, whether it is a @@ -96,16 +103,56 @@ becomes dangling or ambiguous after substitution fails here. An instantiated document is concrete. It **MUST NOT** contain: 1. unresolved `${…}` placeholders (§3 step 8); and -2. variable **definitions** surviving as ordinary authoring variables. The - `variables` section parameterises the authored document; once a document is - instantiated, the bound parameter values — not the variable definitions — - are what the document means. (The resolved parameter values and any captured - pre-substitution references are retained as instantiation context for - downstream consumers, but not as live authoring variables.) +2. a `variables` member, even an empty one; +3. an `imports` member, even an empty one; or +4. a `module` member, even a null one. This is the authoring → instantiated distinction: a value that exists only to be substituted (a `${…}` reference) and the machinery that substitutes it (the -`variables` definitions) do not survive into the instantiated form. +`variables` definitions) do not survive into the instantiated form. `module` +is packaging metadata and `imports` are composition instructions; verified +resolution evidence survives under provenance instead. + +Every `instantiated-scenario-v1` payload **MUST** carry a closed +`instantiation_provenance` object. Its members are: + +| 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. | +| `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. | +| `capability_constraints` | Finite domains retained only for concrete `nodes..os` and `infrastructure..count` fields, addressed by RFC 6901 pointer and qualified parameter identity. | +| `explicitness` | Portable SEM-218 model-path classifications whose parameter identities remain resolvable after variable definitions are removed. | + +A qualified imported binding identity is the import's `namespace` tuple +concatenated with its one-segment local parameter identity. Root and qualified +identities **MUST** be globally unique. Every capability or explicitness +parameter identity **MUST** resolve to one binding. A capability binding's value +**MUST** belong to its retained domain and equal the value at its concrete JSON +Pointer. JSON equality distinguishes booleans from numbers; numerically equal +JSON numbers compare equal. Duplicate domain values are forbidden under that +equality. + +Import records preserve declared preorder and namespace segments, so nested +structure does not have to be recovered by splitting dotted display strings. +Requested/resolved source identities **MUST NOT** carry absolute host/cache +paths or registry credentials. Trust-policy contents, request headers, cache +locations, raw signatures, and source documents are excluded. A signer id and +digest are resolution evidence, not a replacement for a signature or an +independently chosen trust policy. + +The provenance supplies selected inputs and verification anchors for replay. It +does not make replay self-contained or prove that the described transformation +ran: repeated resolution still depends on source availability, source bytes, +and current trust policy. Tools **MUST NOT** duplicate binding values into +summaries, diagnostics, logs, or operation metadata merely because they are +present in the artifact. + +The published JSON Schema enforces the closed phase shape, required provenance, +scalar syntax, and absence of substitution tokens. Cross-field provenance +relations and SDL references/graphs require model and semantic admission. Thus +schema validity is necessary but not sufficient for compiler admission. ## 5. Authoring vs. instantiated reference checks diff --git a/specs/supply-chain/associated-artifact-manifests.md b/specs/supply-chain/associated-artifact-manifests.md new file mode 100644 index 000000000..0aa606a92 --- /dev/null +++ b/specs/supply-chain/associated-artifact-manifests.md @@ -0,0 +1,152 @@ +# Associated Artifact Manifests + +Status: normative + +Decision: [ADR-077](../../docs/decisions/adrs/adr-077-associated-artifact-manifest-boundary.md) + +This specification defines the portable ACES contract for non-semantic +artifacts associated with a scenario, sealed scenario snapshot, or experiment +artifact. The normative machine-readable surface is +`associated-artifact-manifest-v1` under +`contracts/schemas/associated-artifacts/`. + +## 1. Claims and identities + +A conforming implementation MUST keep these claims distinct: + +1. the SDL semantic digest identifies validated, expanded SDL meaning; +2. the associated-artifact set digest identifies one parent reference plus one + exact artifact-reference set; +3. each artifact checksum identifies concrete payload bytes; and +4. authenticity is a separately verified signature/trust-policy result. + +Associated artifacts do not change the semantic SDL digest or the identity of +an experiment task, authoring input, apparatus context, run, or study. + +## 2. Manifest contract + +The manifest is closed and contains: + +- `schema_version`: `associated-artifact-manifest/v1`; +- `manifest_id` and `manifest_version`: stable logical manifest identity; +- `canonicalization_profile`: `associated-artifact-set/v1`; +- `scope`: `scenario` or `experiment`; +- `parent_ref`: one constrained typed reference; +- `artifacts`: a non-empty object keyed by stable local artifact id; and +- `set_digest`: a derived lowercase `sha256:` digest. + +Scenario scope permits only `scenario` and `scenario-snapshot`. A generic +scenario reference is id-only. A snapshot may bind version and the incumbent +canonical SDL digest. Experiment scope permits only `task`, `authoring-input`, +`apparatus-context`, `run`, and `study`. Experiment parent references may bind +id and version but MUST NOT carry a digest or path until that parent family has +a normative canonical payload profile and concrete-payload validator. + +Every artifact entry reuses the `ExperimentArtifactRefModel` shape: stable +`artifact_id`, closed role, media type, absolute non-secret URI, checksum, byte +size, creation time, source assertion, applicable evidence/provenance links, +sensitivity, and optional description. URI is a locator, not integrity +evidence. It MUST NOT contain userinfo or secret-bearing query fields. + +Within one manifest: + +- each object key MUST equal its embedded `artifact_id`; +- artifact ids are opaque, case-sensitive, manifest-local identities, not paths + or content ids; +- duplicate JSON member names MUST be rejected before object construction; +- exact descriptors under different ids are invalid aliases; +- one URI MUST NOT carry conflicting checksum, size, or media-type claims; and +- the same checksum under distinct ids is permitted only when the remaining + descriptors deliberately express distinct roles or locators. + +No attachment is inherited. Scenario attachment does not imply task +attachment; task attachment does not imply authoring-input, apparatus, run, or +study attachment; and study membership does not import member attachments. +Another attachment requires another conforming manifest naming that parent. + +## 3. Set canonicalization + +`associated-artifact-set/v1` serializes this projection with RFC 8785: + +```text +{ + "profile": canonicalization_profile, + "scope": scope, + "parent_ref": exact non-null parent reference fields, + "artifacts": exact keyed non-null artifact descriptor fields +} +``` + +Checksum hex and prefixed parent digests are case-normalized before +canonicalization. Opaque ids, URI strings, descriptions, and other values are +not filesystem- or platform-normalized. SHA-256 of the canonical bytes, +rendered as lowercase `sha256:`, is the set digest. + +The projection excludes `set_digest`, logical manifest id/version, traversal +order, archive metadata, filesystem paths, permissions, symlink targets, +manifest filenames, export tiers, and packaging layout. A parent change or any +canonical artifact-entry change MUST change the set digest. + +## 4. Full conformance and byte binding + +Schema/model validation establishes structural validity only. Full conformance +requires `validate_associated_artifact_manifest()` with: + +- the validated manifest; +- the concrete parent artifact; and +- exactly one concrete byte reader for every artifact id. + +The validator MUST: + +1. match parent kind, id, version, and snapshot digest where applicable; +2. recompute and compare the set digest; +3. reject missing, extra, digest-only, path-only, URI-only, or boolean bindings; +4. stream each reader, recompute checksum and byte size, and compare both; and +5. enforce caller-supplied artifact-count, per-artifact-byte, and total-byte + limits, rejecting declared excess before reading and reading at most the + declared size plus one byte for mismatch detection. + +The validator MUST NOT acquire URIs, traverse directories, extract archives, +resolve credentials, invoke subprocesses, or infer payloads from filenames. +Callers own acquisition and immutable staging. + +Stable error diagnostics include: + +| Condition | Diagnostic code | +|---|---| +| Missing concrete bytes | `associated-artifact.payload-binding-missing` | +| Invalid or digest-only binding | `associated-artifact.payload-binding-invalid` | +| Undeclared binding | `associated-artifact.payload-binding-unexpected` | +| Checksum mismatch | `associated-artifact.payload-checksum-mismatch` | +| Size mismatch | `associated-artifact.payload-size-mismatch` | +| Set digest mismatch | `associated-artifact.set-digest-mismatch` | +| Parent mismatch | `associated-artifact.parent-mismatch` | +| Resource limit exceeded | `associated-artifact.resource-limit-exceeded` | + +Diagnostics MUST be bounded and MUST NOT include payload bytes, full rejected +objects, credentials, environment data, or credential-bearing locators. + +## 5. Trust policy + +The manifest/set is the `associated_artifact_set` reusable-asset family. The +derived set digest is its required `integrity_digest`; concrete payload +verification supplies required `artifact_checksum` evidence. A downstream +signature over the derived set digest may supply `authenticity_signature`, but +integrity alone never proves authenticity, authorization, sensitivity handling, +or entitlement. Associated bytes do not contribute to the parent asset's +integrity mechanism. + +## 6. Packaging and consumer boundary + +Scenario-pack tooling owns filesystem layout, manifest filename, archive/OCI +layout, traversal rules, release tiers, catalog metadata, and safe +materialization. It MUST select a stable byte set before producing this +manifest; a walk over a mutable live directory is not an atomic snapshot. + +Consumers own acquisition, immutable staging, storage, entitlement, atomic +promotion, retention, and use-time verification. They validate the parent and +every staged payload, derive rather than trust the set digest, retain the +manifest with verified bytes, and reverify before use when storage guarantees +do not make that redundant. A caller-supplied package digest may be retained as +untrusted metadata, but cannot become ACES conformance or trust evidence unless +it equals the validator-derived set digest. diff --git a/specs/supply-chain/reusable-asset-trust-integrity.md b/specs/supply-chain/reusable-asset-trust-integrity.md index e8ea52ab8..d6ff4239b 100644 --- a/specs/supply-chain/reusable-asset-trust-integrity.md +++ b/specs/supply-chain/reusable-asset-trust-integrity.md @@ -35,11 +35,11 @@ Trust rests on three orthogonal axes; a policy MUST keep them distinct: | Evidence class | Meaning | Existing ACES mechanism | |---|---|---| -| `integrity_digest` | Digest bound to canonical payload bytes | module `aces.lock.json` digest pins; scenario-snapshot binding; study-definition digest; controlled-vocabulary `source_digest`; manifest/config digests | +| `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 | | `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) | -| `artifact_checksum` | Hard checksum over content-artifact bytes | `ExperimentChecksumModel` (evidence records, task/study artifacts) | +| `artifact_checksum` | Hard checksum over content-artifact bytes | `ExperimentChecksumModel` (associated payloads, evidence records, task/study artifacts) | Each requirement declares an `enforcement` level: `required`, `recommended`, or `optional`. @@ -53,7 +53,8 @@ surface external consumers validate against), and a negative conformance fixture that pins the rejection: 1. **Complete family coverage.** The policy MUST declare exactly one entry for - every canonical reusable asset family: `reusable_scenario`, `sdl_module`, + every canonical reusable asset family: `reusable_scenario`, + `associated_artifact_set`, `sdl_module`, `experiment_task`, `experiment_study`, `behavior_vocabulary`, `participant_manifest`, `evidence_artifact`. 2. **Integrity baseline.** Every family MUST declare at least one integrity @@ -83,6 +84,11 @@ Its shape per family: provenance via composed-module lock records (required), authenticity via source-module signatures (recommended). Scenario *identity* stays distinct from scenario-snapshot *integrity*. +- **associated_artifact_set** — integrity via the derived + `associated-artifact-set/v1` parent-plus-reference-set digest (required) and + concrete payload checksums (required); an optional downstream signature over + the derived set digest is independent authenticity evidence. Associated + payloads do not change parent integrity. - **sdl_module** — integrity via lockfile digest pin (required), provenance via lock record with drift checks (required), authenticity via `RegistryTrustPolicy` signatures (required). diff --git a/tools/check_authority_boundary.py b/tools/check_authority_boundary.py index f05259a4a..c4d41a04c 100644 --- a/tools/check_authority_boundary.py +++ b/tools/check_authority_boundary.py @@ -35,7 +35,12 @@ import yaml -from tools.policy.common import PolicyFailure, apply_exceptions, failures_to_json, load_exceptions +from tools.policy.common import ( + PolicyFailure, + apply_exceptions, + failures_to_json, + load_exceptions, +) # --------------------------------------------------------------------------- # # Canonical paths and baseline invariants. Test code imports these directly # @@ -57,7 +62,7 @@ ADR_REFS: tuple[str, ...] = (ADR_SOURCE_REF, ADR_SEAM_REF) POLICY_VALUE = "normative-artifact-authority" -# The five canonical normative-artifact families ADR-009 names, pinned to +# The canonical normative-artifact families ADR-009 and ADR-019 name, pinned to # their expected root path AND family token. The YAML may not swap a # canonical id onto a different root or relabel its family without failing # the gate — otherwise `normative_schemas` and `normative_fixtures` could be @@ -67,7 +72,18 @@ "normative_schemas": ("contracts/schemas/", "schemas"), "normative_fixtures": ("contracts/fixtures/", "fixtures"), "normative_profiles": ("contracts/profiles/", "profiles"), - "normative_concept_authority": ("contracts/concept-authority/", "concept-authority"), + "normative_realization_envelopes": ( + "contracts/realization-envelopes/", + "realization-envelopes", + ), + "normative_concept_authority": ( + "contracts/concept-authority/", + "concept-authority", + ), + "normative_provenance": ( + "contracts/provenance/", + "provenance", + ), } CANONICAL_AUTHORITY_ROOT_IDS: tuple[str, ...] = tuple(CANONICAL_AUTHORITY_ROOT_BINDING) @@ -83,7 +99,6 @@ "research_notes": "research/", "process_notes": "notes/", "tooling": "tools/", - "changelog_fragments": "changelog.d/", } CANONICAL_NON_NORMATIVE_ROOT_IDS: tuple[str, ...] = tuple(CANONICAL_NON_NORMATIVE_ROOT_BINDING) @@ -104,6 +119,7 @@ # pins roots, so the classification cannot be silently dropped or relabelled. CANONICAL_ARTIFACT_FAMILY_BINDING: dict[str, str] = { "specs/agent-guidance/agent-guidance.yaml": "governance-guidance", + "specs/evolution/deprecation-records.yaml": "deprecation-records", } # Required top-level fields and per-entry fields. Used by both the YAML shape @@ -120,7 +136,12 @@ ) _REQUIRED_AUTHORITY_ROOT_FIELDS: tuple[str, ...] = ("id", "root", "authority", "family") _REQUIRED_NON_NORMATIVE_ROOT_FIELDS: tuple[str, ...] = ("id", "root", "note") -_REQUIRED_ARTIFACT_FAMILY_FIELDS: tuple[str, ...] = ("id", "artifact", "authority", "family") +_REQUIRED_ARTIFACT_FAMILY_FIELDS: tuple[str, ...] = ( + "id", + "artifact", + "authority", + "family", +) _REQUIRED_SCHEMA_AUTHORITY_FIELDS: tuple[str, ...] = ( "normative_root", "publication_manifest", @@ -375,10 +396,20 @@ def _check_authority_roots(raw: dict, source_path: str) -> tuple[list[dict], lis seen_ids.add(root_id) seen_roots.add(root_path) - validated.append({"id": root_id, "root": root_path, "authority": entry["authority"], "family": entry["family"]}) + validated.append( + { + "id": root_id, + "root": root_path, + "authority": entry["authority"], + "family": entry["family"], + } + ) validated_by_id = {entry["id"]: entry for entry in validated} - for canonical_id, (expected_root, expected_family) in CANONICAL_AUTHORITY_ROOT_BINDING.items(): + for canonical_id, ( + expected_root, + expected_family, + ) in CANONICAL_AUTHORITY_ROOT_BINDING.items(): if canonical_id not in seen_ids: failures.append( _fail( @@ -1398,7 +1429,11 @@ def evaluate_authority_boundary(repo_root: Path) -> list[PolicyFailure]: failures.extend(_check_schema_authority_block(raw, AUTHORITY_BOUNDARY_RELATIVE_PATH)) failures.extend( _check_normative_artifact_families( - repo_root, raw, authority_roots, non_normative_roots, AUTHORITY_BOUNDARY_RELATIVE_PATH + repo_root, + raw, + authority_roots, + non_normative_roots, + AUTHORITY_BOUNDARY_RELATIVE_PATH, ) ) diff --git a/tools/check_deprecation_lifecycle.py b/tools/check_deprecation_lifecycle.py new file mode 100644 index 000000000..5c241f485 --- /dev/null +++ b/tools/check_deprecation_lifecycle.py @@ -0,0 +1,586 @@ +#!/usr/bin/env python3 +# ruff: noqa: E402, I001 +"""Structural gate for the GOV-902 ecosystem deprecation and lifecycle rules. + +ADR-075 ("Ecosystem Versioning, Deprecation, and Migration Governance") and +its normative spec ``specs/evolution/versioning-deprecation-and-migration.md`` +decide that deprecations must be explicit, complete records so that +supersession and removal are predictable and reviewable. GOV-902 says the +ecosystem **shall** define those deprecation and lifecycle rules. + +The canonical, reviewable record surface lives in +``specs/evolution/deprecation-records.yaml`` (per ADR-009, normative prose and +its data live under ``specs/``). This checker pins the record contract: every +record must name the seven fields the spec requires -- exact surface and +identifier, first-notice version, replacement or explicit no-replacement +rationale, migration reference, notice window / removal-eligibility rule, and +verification evidence that the old surface stays supported (plus a reviewed +security exception if the ordinary window is shortened). Removal +(``status: removed``) is allowed only with a removal record, and for a +published JSON Schema surface the removal must reference an existing +``removed_schemas`` tombstone in the schema publication manifest (ADR-061), +not a duplicate mechanism. + +This is a CI-time governance gate over static data, not a runtime lifecycle +registry, migration service, persistence layer, or endpoint (ADR-075 forbids +those). Failures use ``tools.policy.common.PolicyFailure`` and the CLI honours +``--json`` and the shared ``tools/policy/exceptions.yaml`` waiver mechanism, +matching the other policy gates (``check_authority_boundary.py``, +``check_adr_immutability.py``, ``check_assurance_policy.py``). +""" + +from __future__ import annotations + +import argparse +import json +import sys +from collections.abc import Iterable +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)) + +import yaml + +from tools.policy.common import ( + PolicyFailure, + apply_exceptions, + failures_to_json, + load_exceptions, +) + +# --------------------------------------------------------------------------- # +# Canonical paths and pinned invariants. Test code imports these directly so # +# a rename here surfaces in the test suite rather than silently in production. # +# --------------------------------------------------------------------------- # + +DEPRECATION_RECORDS_RELATIVE_PATH = "specs/evolution/deprecation-records.yaml" +SPEC_RELATIVE_PATH = "specs/evolution/versioning-deprecation-and-migration.md" +SCHEMA_PUBLICATION_MANIFEST = "contracts/schema-publication-manifest.json" + +REQUIREMENT_REF = "GOV-902" +ADR_REF = "ADR-075" +POLICY_VALUE = "ecosystem-deprecation-records" + +# Surface-class keys, bound to the rows of the surface-class matrix in +# specs/evolution/versioning-deprecation-and-migration.md. A record may not +# name a surface the policy does not recognise; extending the matrix with a new +# family adds a key here (and a row + owner in the spec), which is the +# documented extension seam. +SURFACE_CLASSES: frozenset[str] = frozenset( + { + "python-distribution", + "published-json-schema", + "closed-contract-dto", + "processor-backend-support", + "apparatus-identity", + "sdl-scenario-module", + "experiment-artifact", + "adr", + "ground-control-requirement", + } +) + +STATUSES: frozenset[str] = frozenset({"deprecated", "removed"}) + +# The retention floor: record ids that MUST remain present in the ledger. A +# deprecation record is lifecycle history, so it may not silently disappear -- +# once a construct is deprecated, that fact is permanent (its `status` may +# later advance to `removed`, but its identity is retained). Pinning the known +# ids here means an empty or truncated ledger fails the gate, mirroring the +# canonical-floor pattern the authority-boundary gate uses for its roots and +# artifact families. Retiring a record id is a deliberate, reviewable edit to +# this floor -- exactly the predictability GOV-902 requires. +CANONICAL_DEPRECATION_RECORD_IDS: frozenset[str] = frozenset( + { + "aces-compat-namespace", + "sdl-import-path-field", + } +) + +# The published-schema surface is the only one whose removal is governed by a +# machine-readable tombstone (ADR-061). A `removed` record on this surface must +# point at an existing tombstone rather than asserting removal in prose. +_PUBLISHED_SCHEMA_SURFACE = "published-json-schema" + +_REQUIRED_TOP_LEVEL_FIELDS: tuple[str, ...] = ( + "policy", + "requirement_refs", + "adr_refs", + "spec", + "records", +) + +# Fields every record must carry regardless of status. `replacement` is handled +# separately (exactly one of replacement / no_replacement_rationale). +_REQUIRED_RECORD_FIELDS: tuple[str, ...] = ( + "id", + "surface_class", + "identifier", + "status", + "first_notice", + "migration_reference", + "notice_window", + "verification_evidence", +) + +_SECURITY_EXCEPTION_FIELDS: tuple[str, ...] = ( + "affected_versions", + "impact", + "mitigation", + "migration", + "review_authority", +) + + +# --------------------------------------------------------------------------- # +# Helpers. # +# --------------------------------------------------------------------------- # + + +def _fail(rule_id: str, message: str, path: str | None = None) -> PolicyFailure: + return PolicyFailure(rule_id, message, path) + + +def _is_str(value: object) -> bool: + return isinstance(value, str) + + +def _nonempty_str(value: object) -> bool: + return isinstance(value, str) and bool(value.strip()) + + +def _str_list(value: object) -> list[str] | None: + """Return ``value`` as a list of strings, or None if it is not one. + + A list with a non-string element is rejected so the gate does not silently + coerce ``[GOV-902, 17]`` into strings and keep passing. + """ + if not isinstance(value, list): + return None + if not all(isinstance(item, str) for item in value): + return None + return list(value) + + +# --------------------------------------------------------------------------- # +# Top-level shape and references. # +# --------------------------------------------------------------------------- # + + +def _check_top_level(raw: dict, source_path: str) -> list[PolicyFailure]: + failures: list[PolicyFailure] = [] + for field in _REQUIRED_TOP_LEVEL_FIELDS: + if field not in raw: + failures.append( + _fail( + "deprecation-records-field", + f"required top-level field is missing: {field}", + source_path, + ) + ) + + if "policy" in raw and not _is_str(raw["policy"]): + failures.append( + _fail( + "deprecation-records-field-type", + f"policy must be a string; got {type(raw['policy']).__name__}", + source_path, + ) + ) + elif "policy" in raw and raw["policy"] != POLICY_VALUE: + failures.append( + _fail( + "deprecation-records-policy-value", + f"policy must equal {POLICY_VALUE!r}; got {raw['policy']!r}", + source_path, + ) + ) + + requirement_refs = raw.get("requirement_refs") + if "requirement_refs" in raw and _str_list(requirement_refs) is None: + failures.append( + _fail( + "deprecation-records-field-type", + "requirement_refs must be a list of strings", + source_path, + ) + ) + elif _str_list(requirement_refs) is not None and REQUIREMENT_REF not in requirement_refs: + failures.append( + _fail( + "deprecation-records-requirement-ref", + f"requirement_refs must include {REQUIREMENT_REF}; got {requirement_refs!r}", + source_path, + ) + ) + + adr_refs = raw.get("adr_refs") + if "adr_refs" in raw and _str_list(adr_refs) is None: + failures.append( + _fail( + "deprecation-records-field-type", + "adr_refs must be a list of strings", + source_path, + ) + ) + elif _str_list(adr_refs) is not None and ADR_REF not in adr_refs: + failures.append( + _fail( + "deprecation-records-adr-ref", + f"adr_refs must include {ADR_REF}; got {adr_refs!r}", + source_path, + ) + ) + + if "spec" in raw and not _nonempty_str(raw["spec"]): + failures.append( + _fail( + "deprecation-records-field-type", + "spec must be a non-empty string path", + source_path, + ) + ) + + if "records" in raw and not isinstance(raw["records"], list): + failures.append( + _fail( + "deprecation-records-field-type", + f"records must be a list; got {type(raw['records']).__name__}", + source_path, + ) + ) + return failures + + +def _check_spec_present(repo_root: Path, raw: dict, source_path: str) -> list[PolicyFailure]: + spec = raw.get("spec") + if not _nonempty_str(spec): + return [] # type/shape failure already reported + if not (repo_root / spec).is_file(): + return [ + _fail( + "deprecation-records-spec-missing", + f"spec path {spec!r} does not exist on disk", + source_path, + ) + ] + return [] + + +# --------------------------------------------------------------------------- # +# Per-record checks. # +# --------------------------------------------------------------------------- # + + +def _load_removed_schema_paths(repo_root: Path) -> set[str]: + """Return the set of ``schema_path`` values recorded as removed_schemas + tombstones in the schema publication manifest (empty if absent).""" + manifest_path = repo_root / SCHEMA_PUBLICATION_MANIFEST + if not manifest_path.is_file(): + return set() + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return set() + removed = manifest.get("removed_schemas") + if not isinstance(removed, list): + return set() + paths: set[str] = set() + for entry in removed: + if isinstance(entry, dict) and _nonempty_str(entry.get("schema_path")): + paths.add(entry["schema_path"]) + return paths + + +def _check_security_exception(entry: dict, index: int, source_path: str) -> list[PolicyFailure]: + exception = entry.get("security_exception") + if exception is None: + return [] + if not isinstance(exception, dict): + return [ + _fail( + "deprecation-records-security-exception", + f"records[{index}].security_exception must be a mapping when present", + source_path, + ) + ] + failures: list[PolicyFailure] = [] + for field in _SECURITY_EXCEPTION_FIELDS: + if not _nonempty_str(exception.get(field)): + failures.append( + _fail( + "deprecation-records-security-exception", + ( + f"records[{index}].security_exception must name a non-empty {field!r} " + "(affected versions, impact, mitigation, migration, and review authority)" + ), + source_path, + ) + ) + return failures + + +def _check_removal( + entry: dict, + index: int, + removed_schema_paths: set[str], + source_path: str, +) -> list[PolicyFailure]: + """Validate the extra invariants for a ``status: removed`` record.""" + failures: list[PolicyFailure] = [] + if not _nonempty_str(entry.get("removal_record")): + failures.append( + _fail( + "deprecation-records-removal-record", + ( + f"records[{index}] has status 'removed' but no non-empty 'removal_record' " + "(removal is allowed only after a complete deprecation record reaches its " + "removal-eligibility rule)" + ), + source_path, + ) + ) + if entry.get("surface_class") == _PUBLISHED_SCHEMA_SURFACE: + tombstone = entry.get("removal_tombstone") + if not _nonempty_str(tombstone): + failures.append( + _fail( + "deprecation-records-removal-tombstone", + ( + f"records[{index}] removes a {_PUBLISHED_SCHEMA_SURFACE} surface and must name a " + "'removal_tombstone' schema_path recorded in the schema publication manifest" + ), + source_path, + ) + ) + elif tombstone not in removed_schema_paths: + failures.append( + _fail( + "deprecation-records-removal-tombstone", + ( + f"records[{index}].removal_tombstone {tombstone!r} is not a removed_schemas " + f"tombstone in {SCHEMA_PUBLICATION_MANIFEST} (ADR-061 governs published-schema removal)" + ), + source_path, + ) + ) + return failures + + +def _check_records(repo_root: Path, raw: dict, source_path: str) -> list[PolicyFailure]: + raw_records = raw.get("records") + if not isinstance(raw_records, list): + return [] # type-level failure already reported + + removed_schema_paths = _load_removed_schema_paths(repo_root) + failures: list[PolicyFailure] = [] + seen_ids: set[str] = set() + + for index, entry in enumerate(raw_records): + if not isinstance(entry, dict): + failures.append( + _fail( + "deprecation-records-entry-field", + f"records[{index}] must be a mapping; got {type(entry).__name__}", + source_path, + ) + ) + continue + + entry_ok = True + for field in _REQUIRED_RECORD_FIELDS: + if field not in entry: + failures.append( + _fail( + "deprecation-records-entry-field", + f"records[{index}] is missing required field: {field}", + source_path, + ) + ) + entry_ok = False + elif not _nonempty_str(entry[field]): + failures.append( + _fail( + "deprecation-records-entry-field", + f"records[{index}].{field} must be a non-empty string", + source_path, + ) + ) + entry_ok = False + + # Exactly one of replacement / no_replacement_rationale. The spec allows + # a no-replacement deprecation, but it must carry an explicit rationale. + has_replacement = _nonempty_str(entry.get("replacement")) + has_no_replacement = _nonempty_str(entry.get("no_replacement_rationale")) + if has_replacement == has_no_replacement: + failures.append( + _fail( + "deprecation-records-replacement", + (f"records[{index}] must name exactly one of 'replacement' or 'no_replacement_rationale'"), + source_path, + ) + ) + + # id uniqueness (only meaningful once the id itself is a valid string). + entry_id = entry.get("id") + if _nonempty_str(entry_id): + if entry_id in seen_ids: + failures.append( + _fail( + "deprecation-records-entry-duplicate", + f"records[{index}].id {entry_id!r} is duplicated", + source_path, + ) + ) + seen_ids.add(entry_id) + + surface = entry.get("surface_class") + if _nonempty_str(surface) and surface not in SURFACE_CLASSES: + failures.append( + _fail( + "deprecation-records-surface-class", + ( + f"records[{index}].surface_class {surface!r} is not a recognised surface " + f"class; expected one of {sorted(SURFACE_CLASSES)}" + ), + source_path, + ) + ) + + status = entry.get("status") + if _nonempty_str(status) and status not in STATUSES: + failures.append( + _fail( + "deprecation-records-status", + f"records[{index}].status {status!r} must be one of {sorted(STATUSES)}", + source_path, + ) + ) + + failures.extend(_check_security_exception(entry, index, source_path)) + + if status == "removed": + failures.extend(_check_removal(entry, index, removed_schema_paths, source_path)) + + _ = entry_ok # per-field failures already recorded; kept for readability + return failures + + +def _check_canonical_record_floor(raw: dict, source_path: str) -> list[PolicyFailure]: + """Enforce the retention floor: every canonical record id must be present. + + Deprecation records are lifecycle history. An empty or truncated ledger -- + ``records: []`` or a diff that drops a checked-in record -- would let that + history disappear while the gate stays green, because the per-record checks + pass vacuously over whatever entries remain. Pinning the known ids means the + authoritative record set cannot be silently emptied; a record's ``status`` + may advance (e.g. deprecated -> removed) but its identity is retained. + """ + raw_records = raw.get("records") + present_ids = set() + if isinstance(raw_records, list): + present_ids = { + entry["id"] for entry in raw_records if isinstance(entry, dict) and _nonempty_str(entry.get("id")) + } + failures: list[PolicyFailure] = [] + for canonical_id in sorted(CANONICAL_DEPRECATION_RECORD_IDS): + if canonical_id not in present_ids: + failures.append( + _fail( + "deprecation-records-canonical-record-missing", + ( + f"the canonical deprecation record {canonical_id!r} is missing; established " + "lifecycle records may not be removed from the ledger (retiring one is a " + "deliberate edit to CANONICAL_DEPRECATION_RECORD_IDS)" + ), + source_path, + ) + ) + return failures + + +# --------------------------------------------------------------------------- # +# Top-level entry point. # +# --------------------------------------------------------------------------- # + + +def evaluate_deprecation_records(repo_root: Path) -> list[PolicyFailure]: + """Return the list of structural failures for the GOV-902 deprecation-record + surface (empty list = OK).""" + records_path = repo_root / DEPRECATION_RECORDS_RELATIVE_PATH + if not records_path.is_file(): + return [ + _fail( + "deprecation-records-missing", + f"deprecation records surface not found: {DEPRECATION_RECORDS_RELATIVE_PATH}", + DEPRECATION_RECORDS_RELATIVE_PATH, + ) + ] + + try: + raw = yaml.safe_load(records_path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + return [ + _fail( + "deprecation-records-parse", + f"failed to parse {DEPRECATION_RECORDS_RELATIVE_PATH}: {exc}", + DEPRECATION_RECORDS_RELATIVE_PATH, + ) + ] + + if not isinstance(raw, dict): + return [ + _fail( + "deprecation-records-shape", + f"{DEPRECATION_RECORDS_RELATIVE_PATH} must be a YAML mapping at the top level", + DEPRECATION_RECORDS_RELATIVE_PATH, + ) + ] + + failures: list[PolicyFailure] = [] + failures.extend(_check_top_level(raw, DEPRECATION_RECORDS_RELATIVE_PATH)) + failures.extend(_check_spec_present(repo_root, raw, DEPRECATION_RECORDS_RELATIVE_PATH)) + failures.extend(_check_records(repo_root, raw, DEPRECATION_RECORDS_RELATIVE_PATH)) + failures.extend(_check_canonical_record_floor(raw, DEPRECATION_RECORDS_RELATIVE_PATH)) + return failures + + +# --------------------------------------------------------------------------- # +# CLI. # +# --------------------------------------------------------------------------- # + + +def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Validate the GOV-902 ecosystem deprecation and lifecycle records (ADR-075)." + ) + parser.add_argument( + "--repo-root", + type=Path, + default=REPO_ROOT, + help="Repository root (defaults to the repo containing this file).", + ) + parser.add_argument("--json", action="store_true", help="Emit JSON failures.") + return parser.parse_args(list(argv) if argv is not None else None) + + +def main(argv: Iterable[str] | None = None) -> int: + args = parse_args(argv) + failures = evaluate_deprecation_records(args.repo_root) + exceptions_file = args.repo_root / "tools" / "policy" / "exceptions.yaml" + if exceptions_file.is_file(): + failures = apply_exceptions(failures, load_exceptions(args.repo_root)) + if failures: + if args.json: + print(failures_to_json(failures)) + else: + for failure in failures: + print(failure.render(), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/check_scientific_scenario_completeness.py b/tools/check_scientific_scenario_completeness.py new file mode 100644 index 000000000..7028d034b --- /dev/null +++ b/tools/check_scientific_scenario_completeness.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +"""Validate the REV1 scientific-scenario completeness claim contract.""" + +from __future__ import annotations + +import json +import sys +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 aces_conformance.conformance import validate_contract_payload # noqa: E402 +from aces_contracts.scientific_completeness import ( # noqa: E402 + DeliveryStatus, + ProfileCompletenessResult, + ScientificCompletenessAssessmentModel, + ScientificCompletenessTaxonomyModel, + evaluate_profile_completeness, + load_scientific_completeness_assessment, + load_scientific_completeness_taxonomy, +) + +from tools.policy.common import PolicyFailure, safe_repo_path # noqa: E402 + +_EXPECTED_PROFILE_IDS = { + "valid-sdl-fragment", + "deployable-scenario-intent", + "participant-evaluation-scenario", + "controlled-experiment-scenario", + "reproducible-benchmark-study-input", +} +_REQUIRED_NON_CLAIM_TERMS = { + "backend conformance", + "behavioral equivalence", + "deployability", + "reproducibility", +} +_SUMMARY_START = "" +_SUMMARY_END = "" + + +def _render_summary(outcomes: tuple[ProfileCompletenessResult, ...]) -> str: + lines = [ + _SUMMARY_START, + "| Profile | Complete | Blocking required concerns |", + "| --- | --- | --- |", + ] + for outcome in outcomes: + blockers = ( + ", ".join(f"`{concern_id}`" for concern_id in outcome.blocking_concerns) + if outcome.blocking_concerns + else "none" + ) + lines.append(f"| `{outcome.profile_id}` | {'yes' if outcome.complete else 'no'} | {blockers} |") + lines.append(_SUMMARY_END) + return "\n".join(lines) + + +def _validate_profile_set( + taxonomy: ScientificCompletenessTaxonomyModel, +) -> list[PolicyFailure]: + profile_ids = {profile.profile_id for profile in taxonomy.profiles} + if profile_ids == _EXPECTED_PROFILE_IDS: + return [] + return [ + PolicyFailure( + "scientific-completeness-profile-set", + f"REV1 profiles must exactly match {sorted(_EXPECTED_PROFILE_IDS)}; got {sorted(profile_ids)}", + ) + ] + + +def _validate_evidence_paths( + repo_root: Path, + assessment: ScientificCompletenessAssessmentModel, +) -> list[PolicyFailure]: + failures: list[PolicyFailure] = [] + for item in assessment.concerns: + for evidence in item.evidence: + resolved = safe_repo_path(repo_root, evidence.path) + if resolved is None or not resolved.is_file(): + failures.append( + PolicyFailure( + "scientific-completeness-evidence-missing", + f"concern {item.concern_id!r} evidence path is missing or escapes the repository", + evidence.path, + ) + ) + for contract_id, witness_ref in item.satisfiability_witness_refs.items(): + resolved = safe_repo_path(repo_root, witness_ref) + if resolved is None or not resolved.is_file(): + failures.append( + PolicyFailure( + "scientific-completeness-witness-missing", + f"concern {item.concern_id!r} satisfiability witness is missing or escapes the repository", + witness_ref, + ) + ) + continue + try: + payload = json.loads(resolved.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + failures.append( + PolicyFailure( + "scientific-completeness-witness-invalid", + f"concern {item.concern_id!r} witness for {contract_id!r} is not valid JSON: {exc}", + witness_ref, + ) + ) + continue + diagnostics = validate_contract_payload(contract_id, payload) + if diagnostics: + failures.append( + PolicyFailure( + "scientific-completeness-witness-nonconforming", + f"concern {item.concern_id!r} witness does not conform to " + f"{contract_id!r}: {diagnostics[0].message}", + witness_ref, + ) + ) + return failures + + +def _published_schema_paths(repo_root: Path) -> dict[str, str]: + manifest_path = repo_root / "contracts/schema-publication-manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + return { + entry["contract_id"]: entry["schema_path"] + for entry in manifest.get("schemas", []) + if isinstance(entry, dict) and isinstance(entry.get("contract_id"), str) + } + + +def _validate_contract_evidence( + repo_root: Path, + assessment: ScientificCompletenessAssessmentModel, +) -> list[PolicyFailure]: + published_schemas = _published_schema_paths(repo_root) + failures: list[PolicyFailure] = [] + for item in assessment.concerns: + for evidence in item.evidence: + if evidence.contract_id is None: + continue + expected_path = published_schemas.get(evidence.contract_id) + if expected_path != evidence.path: + failures.append( + PolicyFailure( + "scientific-completeness-contract-evidence-mismatch", + f"concern {item.concern_id!r} contract " + f"{evidence.contract_id!r} must cite its published schema " + f"path {expected_path!r}", + evidence.path, + ) + ) + return failures + + +def _validate_profile_examples( + repo_root: Path, + taxonomy: ScientificCompletenessTaxonomyModel, + outcomes: tuple[ProfileCompletenessResult, ...], +) -> list[PolicyFailure]: + outcome_by_id = {outcome.profile_id: outcome for outcome in outcomes} + failures: list[PolicyFailure] = [] + for profile in taxonomy.profiles: + outcome = outcome_by_id[profile.profile_id] + if outcome.complete and not profile.example_refs: + failures.append( + PolicyFailure( + "scientific-completeness-example-required", + f"complete profile {profile.profile_id!r} requires a minimal example", + ) + ) + if not outcome.complete and profile.example_refs: + failures.append( + PolicyFailure( + "scientific-completeness-example-overclaim", + f"incomplete profile {profile.profile_id!r} must not present an example as completeness evidence", + ) + ) + for example_ref in profile.example_refs: + resolved = safe_repo_path(repo_root, example_ref) + if resolved is None or not resolved.is_file(): + failures.append( + PolicyFailure( + "scientific-completeness-example-missing", + f"profile {profile.profile_id!r} example is missing or escapes the repository", + example_ref, + ) + ) + return failures + + +def _validate_nonclaims( + taxonomy: ScientificCompletenessTaxonomyModel, +) -> list[PolicyFailure]: + non_claim_text = " ".join( + statement.lower() for profile in taxonomy.profiles for statement in profile.explicit_non_claims + ) + missing_terms = sorted(term for term in _REQUIRED_NON_CLAIM_TERMS if term not in non_claim_text) + if not missing_terms: + return [] + return [ + PolicyFailure( + "scientific-completeness-nonclaim-coverage", + f"REV1 explicit non-claims must cover: {missing_terms}", + ) + ] + + +def _validate_summary( + repo_root: Path, + outcomes: tuple[ProfileCompletenessResult, ...], +) -> list[PolicyFailure]: + spec_path = repo_root / "specs/sdl/scientific-scenario-completeness.md" + spec_text = spec_path.read_text(encoding="utf-8") if spec_path.is_file() else "" + if _render_summary(outcomes) in spec_text: + return [] + return [ + PolicyFailure( + "scientific-completeness-summary-drift", + "normative reader-facing outcome table does not match the computed profile outcomes", + "specs/sdl/scientific-scenario-completeness.md", + ) + ] + + +def _validate_required_exclusions( + taxonomy: ScientificCompletenessTaxonomyModel, + assessment: ScientificCompletenessAssessmentModel, +) -> list[PolicyFailure]: + assessment_by_id = {item.concern_id: item for item in assessment.concerns} + required_deliberate_exclusions = { + concern_id + for profile in taxonomy.profiles + for concern_id, disposition in profile.dispositions.items() + if disposition.value == "required" + and assessment_by_id[concern_id].status is DeliveryStatus.DELIBERATELY_EXCLUDED + } + if not required_deliberate_exclusions: + return [] + return [ + PolicyFailure( + "scientific-completeness-required-exclusion", + "deliberately excluded concerns cannot satisfy required profile rows: " + + ", ".join(sorted(required_deliberate_exclusions)), + ) + ] + + +def evaluate(repo_root: Path) -> list[PolicyFailure]: + try: + taxonomy = load_scientific_completeness_taxonomy() + assessment = load_scientific_completeness_assessment() + outcomes = evaluate_profile_completeness(taxonomy, assessment) + except (OSError, ValueError) as exc: + return [PolicyFailure("scientific-completeness-invalid", str(exc))] + + failures: list[PolicyFailure] = [] + failures.extend(_validate_profile_set(taxonomy)) + failures.extend(_validate_evidence_paths(repo_root, assessment)) + try: + failures.extend(_validate_contract_evidence(repo_root, assessment)) + except (OSError, ValueError, TypeError) as exc: + failures.append( + PolicyFailure( + "scientific-completeness-manifest-invalid", + f"cannot validate contract evidence against publication manifest: {exc}", + ) + ) + failures.extend(_validate_profile_examples(repo_root, taxonomy, outcomes)) + failures.extend(_validate_nonclaims(taxonomy)) + failures.extend(_validate_summary(repo_root, outcomes)) + failures.extend(_validate_required_exclusions(taxonomy, assessment)) + return failures + + +def main() -> int: + failures = evaluate(REPO_ROOT) + for failure in failures: + print(failure.render()) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/check_sdl_catalog_parity.py b/tools/check_sdl_catalog_parity.py new file mode 100644 index 000000000..decdf3176 --- /dev/null +++ b/tools/check_sdl_catalog_parity.py @@ -0,0 +1,861 @@ +#!/usr/bin/env python3 +# ruff: noqa: E402, I001 +"""Prove that the normative SDL catalogs cover the live language surface. + +The published schema and normative prose remain independently governed +authorities. This read-only check compares both with the reference +implementation registries so drift is reported instead of silently generated +away. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[1] +PYTHON_PACKAGES = REPO_ROOT / "implementations" / "python" / "packages" +for import_root in (REPO_ROOT, PYTHON_PACKAGES): + if str(import_root) not in sys.path: + sys.path.insert(0, str(import_root)) + +from aces_sdl._language_metadata import REFERENCE_COMPLETION_TARGETS +from aces_sdl._mapping_scopes import HASHMAP_SECTIONS +from aces_sdl._module_symbols import HASHMAP_SECTIONS as MODULE_HASHMAP_SECTIONS +from aces_sdl._runtime_service_families import ( + RUNTIME_SERVICE_FAMILIES, + RuntimeReferenceChild, +) +from aces_sdl.scenario import Scenario +from tools.policy.common import ( + PolicyFailure, + apply_exceptions, + failures_to_json, + load_exceptions, +) + +SECTIONS_PATH = "specs/sdl/sections.md" +REFERENCES_PATH = "specs/sdl/references.md" +RUNTIME_PATH = "specs/sdl/runtime-inventory.md" +SCHEMA_PATH = "contracts/schemas/sdl/sdl-authoring-input-v1.json" + +_TOP_LEVEL_HEADING = "## Complete top-level field catalog" +_REFERENCE_HEADING = "## 6. Machine-checkable reference-edge index" +_RUNTIME_HEADING = "## 2. Family index" +_SUMMARY_RE = re.compile( + r"" +) +_SEPARATOR_RE = re.compile(r"^:?-{2,}:?$") +_BACKTICK_RE = re.compile(r"`([^`]+)`") +_VALID_KINDS = frozenset({"metadata", "composition", "section"}) +_VALID_SHAPES = frozenset({"scalar", "mapping", "map", "list"}) +_VALID_LIFECYCLE = frozenset({"normalized", "expanded", "instantiated", "expanded-empty", "instantiated-empty"}) +_MAX_CATALOG_BYTES = 512 * 1024 +_MAX_CATALOG_ROWS = 512 +_METADATA_FIELDS = frozenset({"name", "version", "description"}) +_COMPOSITION_FIELDS = frozenset({"module", "imports"}) + +_NODE_VALIDATOR = "[node validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py)" +_INFRASTRUCTURE_VALIDATOR = ( + "[infrastructure validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py)" +) +_SECTION_VALIDATOR = "[section validator](../../implementations/python/packages/aces_sdl/validator/_sections.py)" +_CONTENT_VALIDATOR = ( + "[content validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py)" +) +_ACCOUNT_VALIDATOR = ( + "[account validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py)" +) +_RELATIONSHIP_VALIDATOR = ( + "[relationship validator](../../implementations/python/packages/aces_sdl/validator/_relationships.py)" +) +_PARTICIPANT_VALIDATOR = ( + "[participant validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py)" +) +_PARTICIPANT_SEMANTICS = ( + "[participant semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py)" +) +_OUTCOME_SEMANTICS = ( + "[outcome semantics](../../implementations/python/packages/aces_sdl/semantics/participant_outcome.py)" +) +_BEHAVIOR_SEMANTICS = ( + "[behavior semantics](../../implementations/python/packages/aces_sdl/semantics/participant_behavior.py)" +) +_BEHAVIOR_VALIDATOR = ( + "[behavior validator](../../implementations/python/packages/aces_sdl/validator/_content_objectives.py)" +) +_BEHAVIOR_MODEL = "[behavior model](behavior-specifications.md)" +_EVIDENCE_VALIDATOR = ( + "[evidence validator](../../implementations/python/packages/aces_sdl/validator/_evidence_requirements.py)" +) +_OBJECTIVE_SEMANTICS = "[objective semantics](objective-semantics.md)" +_WORKFLOW_SEMANTICS = "[workflow semantics](workflow-semantics.md)" +_PROPOSITION_VALIDATOR = ( + "[proposition validator](../../implementations/python/packages/aces_sdl/validator/_propositions.py)" +) +_SEMANTIC = "semantic validation" +_STRUCTURAL = "structural validation" +_DANGLING = "fatal dangling or ambiguous" + +# This independently owned expectation makes every normative reference row a +# checked contract. The catalog is not generated from this registry; changing +# either authority requires an explicit, reviewable reconciliation. +_REFERENCE_EDGE_EXPECTATIONS: dict[str, tuple[str, str, str, str]] = { + "nodes.*.features[]": ("features", _SEMANTIC, _DANGLING, _NODE_VALIDATOR), + "nodes.*.conditions[]": ("conditions", _SEMANTIC, _DANGLING, _NODE_VALIDATOR), + "conditions.*.proposition": ( + "propositions", + _SEMANTIC, + "fatal dangling or ambiguous when present", + _PROPOSITION_VALIDATOR, + ), + "propositions.*.subjects[]": ( + "targetable", + _SEMANTIC, + _DANGLING, + _PROPOSITION_VALIDATOR, + ), + "propositions.*.evidence_requirements[]": ( + "evidence_requirements", + _SEMANTIC, + _DANGLING, + _PROPOSITION_VALIDATOR, + ), + "assertions.*.proposition": ( + "propositions", + _SEMANTIC, + _DANGLING, + _PROPOSITION_VALIDATOR, + ), + "nodes.*.injects[]": ("injects", _SEMANTIC, _DANGLING, _NODE_VALIDATOR), + "nodes.*.vulnerabilities[]": ( + "vulnerabilities", + _SEMANTIC, + _DANGLING, + _NODE_VALIDATOR, + ), + "infrastructure.*.links[]": ( + "infrastructure", + _SEMANTIC, + _DANGLING, + _INFRASTRUCTURE_VALIDATOR, + ), + "infrastructure.*.dependencies[]": ( + "infrastructure", + _SEMANTIC, + _DANGLING, + _INFRASTRUCTURE_VALIDATOR, + ), + "features.*.dependencies[]": ( + "features", + _SEMANTIC, + "fatal dangling, ambiguous, or cyclic", + _SECTION_VALIDATOR, + ), + "entities.*.vulnerabilities[]": ( + "vulnerabilities", + _SEMANTIC, + _DANGLING, + _SECTION_VALIDATOR, + ), + "injects.*.from_entity": ("entities", _SEMANTIC, _DANGLING, _SECTION_VALIDATOR), + "injects.*.to_entities[]": ("entities", _SEMANTIC, _DANGLING, _SECTION_VALIDATOR), + "events.*.assertions[]": ( + "assertions", + _SEMANTIC, + "fatal dangling, ambiguous, or non-precondition role", + _PROPOSITION_VALIDATOR, + ), + "events.*.injects[]": ("injects", _SEMANTIC, _DANGLING, _SECTION_VALIDATOR), + "scripts.*.events[]": ("events", _SEMANTIC, _DANGLING, _SECTION_VALIDATOR), + "stories.*.scripts[]": ("scripts", _SEMANTIC, _DANGLING, _SECTION_VALIDATOR), + "content.*.target": ( + "nodes", + _SEMANTIC, + "fatal unless target is a vm node", + _CONTENT_VALIDATOR, + ), + "accounts.*.node": ( + "nodes", + _SEMANTIC, + "fatal unless target is a vm node", + _ACCOUNT_VALIDATOR, + ), + "relationships.*.source": ( + "targetable", + _SEMANTIC, + "fatal dangling or ambiguous; subtype may narrow domain", + _RELATIONSHIP_VALIDATOR, + ), + "relationships.*.target": ( + "targetable", + _SEMANTIC, + "fatal dangling or ambiguous; subtype may narrow domain", + _RELATIONSHIP_VALIDATOR, + ), + "agents.*.entity": ("entities", _SEMANTIC, _DANGLING, _PARTICIPANT_VALIDATOR), + "agents.*.starting_accounts[]": ( + "accounts", + _SEMANTIC, + _DANGLING, + _PARTICIPANT_VALIDATOR, + ), + "agents.*.starting_assertions[]": ( + "assertions", + _SEMANTIC, + "fatal dangling, ambiguous, or non-precondition role", + _PROPOSITION_VALIDATOR, + ), + "action_contracts.*.interactions.*.related_action_ref": ( + "action_contracts", + _SEMANTIC, + _DANGLING, + _PARTICIPANT_SEMANTICS, + ), + "observation_boundaries.*.view_rules.*.information_refs[]": ( + "derived:boundary_information", + _SEMANTIC, + "fatal outside declared boundary information", + _PARTICIPANT_SEMANTICS, + ), + "outcome_interpretation_rules.*.source_ref": ( + "action_contracts,objectives,workflows", + _SEMANTIC, + _DANGLING, + _OUTCOME_SEMANTICS, + ), + "behavior_specifications.*.participant_refs[]": ( + "agents", + _SEMANTIC, + _DANGLING, + _BEHAVIOR_SEMANTICS, + ), + "behavior_specifications.*.participant_role_refs[]": ( + "derived:agent_roles", + _SEMANTIC, + "fatal unless bound by a referenced participant", + _BEHAVIOR_SEMANTICS, + ), + "behavior_specifications.*.action_contract_refs[]": ( + "action_contracts", + _SEMANTIC, + _DANGLING, + _BEHAVIOR_SEMANTICS, + ), + "behavior_specifications.*.observation_boundary_refs[]": ( + "observation_boundaries", + _SEMANTIC, + _DANGLING, + _BEHAVIOR_SEMANTICS, + ), + "behavior_specifications.*.outcome_interpretation_rule_refs[]": ( + "outcome_interpretation_rules", + _SEMANTIC, + _DANGLING, + _BEHAVIOR_SEMANTICS, + ), + "behavior_specifications.*.authority_scope_refs[]": ( + "targetable", + _SEMANTIC, + _DANGLING, + _BEHAVIOR_VALIDATOR, + ), + "behavior_specifications.*.behavior_mode": ( + "vocabulary:behavior_mode", + _STRUCTURAL, + "fatal invalid vocabulary value", + _BEHAVIOR_MODEL, + ), + "behavior_specifications.*.ai_offensive_behavior_refs[]": ( + "vocabulary:ai_offensive_behavior", + _SEMANTIC, + "fatal unknown vocabulary identifier", + _BEHAVIOR_MODEL, + ), + "behavior_specifications.*.offensive_behavior_refs[]": ( + "vocabulary:offensive_behavior", + _SEMANTIC, + "fatal unknown vocabulary identifier", + _BEHAVIOR_MODEL, + ), + "behavior_specifications.*.realization_profile_ref": ( + "opaque:realization_profile", + _STRUCTURAL, + "fatal invalid reference shape; resolution belongs to realization", + _BEHAVIOR_MODEL, + ), + "behavior_specifications.*.backend_feature_support_refs[]": ( + "registry:behavior_features", + _SEMANTIC, + "fatal unsupported feature identifier", + _BEHAVIOR_SEMANTICS, + ), + "behavior_specifications.*.evidence_contract_refs[]": ( + "contract:participant_evidence", + _SEMANTIC, + "fatal unknown contract identifier", + _BEHAVIOR_SEMANTICS, + ), + "evidence_requirements.*.source_refs[]": ( + "targetable", + _SEMANTIC, + _DANGLING, + _EVIDENCE_VALIDATOR, + ), + "evidence_requirements.*.scope_refs[]": ( + "targetable", + _SEMANTIC, + _DANGLING, + _EVIDENCE_VALIDATOR, + ), + "evidence_requirements.*.channel_refs[]": ( + "targetable", + _SEMANTIC, + _DANGLING, + _EVIDENCE_VALIDATOR, + ), + "evidence_requirements.*.trigger_ref": ( + "targetable", + _SEMANTIC, + _DANGLING, + _EVIDENCE_VALIDATOR, + ), + "evidence_requirements.*.boundary_ref": ( + "targetable", + _SEMANTIC, + _DANGLING, + _EVIDENCE_VALIDATOR, + ), + "objectives.*.agent": ("agents", _SEMANTIC, _DANGLING, _OBJECTIVE_SEMANTICS), + "objectives.*.entity": ("entities", _SEMANTIC, _DANGLING, _OBJECTIVE_SEMANTICS), + "objectives.*.targets[]": ( + "targetable", + _SEMANTIC, + _DANGLING, + _OBJECTIVE_SEMANTICS, + ), + "objectives.*.success.assertions[]": ( + "assertions", + _SEMANTIC, + "fatal dangling, ambiguous, or precondition role", + _OBJECTIVE_SEMANTICS, + ), + "objectives.*.depends_on[]": ( + "objectives", + _SEMANTIC, + "fatal dangling, ambiguous, or cyclic", + _OBJECTIVE_SEMANTICS, + ), + "workflows.*.start": ( + "workflow_steps", + _SEMANTIC, + "fatal dangling step", + _WORKFLOW_SEMANTICS, + ), + "workflows.*.steps.*.when.assertions[]": ( + "assertions", + _SEMANTIC, + "fatal dangling, ambiguous, or non-precondition role", + _WORKFLOW_SEMANTICS, + ), +} + + +class CatalogParseError(ValueError): + """A normative catalog table is absent or malformed.""" + + +@dataclass(frozen=True) +class TopLevelRow: + field: str + kind: str + shape: str + lifecycle: tuple[str, ...] + presence: str + identity: str + references: str + owner: str + line_no: int + + +@dataclass(frozen=True) +class ReferenceRow: + source_path: str + domain: str + phase: str + failure: str + owner: str + line_no: int + + @property + def key(self) -> tuple[str, str]: + parts = self.source_path.replace("[]", "").split(".") + return parts[0], parts[-1] + + +@dataclass(frozen=True) +class RuntimeRow: + key: str + collection: str + primary_id: str + child_paths: tuple[str, ...] + owner: str + line_no: int + + +def _cells(line: str) -> list[str]: + parts = [part.strip() for part in line.strip().split("|")] + if parts and not parts[0]: + parts.pop(0) + if parts and not parts[-1]: + parts.pop() + return parts + + +def _unquote(cell: str) -> str: + match = _BACKTICK_RE.fullmatch(cell.strip()) + return match.group(1) if match else cell.strip() + + +def _table(text: str, heading: str, columns: int) -> list[tuple[int, list[str]]]: + size = len(text.encode("utf-8")) + if size > _MAX_CATALOG_BYTES: + raise CatalogParseError(f"catalog exceeds {_MAX_CATALOG_BYTES}-byte size limit") + lines = text.splitlines() + try: + start = next(index for index, line in enumerate(lines) if line.strip() == heading) + 1 + except StopIteration as exc: + raise CatalogParseError(f"missing catalog heading: {heading}") from exc + table: list[tuple[int, list[str]]] = [] + started = False + for index, line in enumerate(lines[start:], start=start): + if line.startswith("## "): + break + if line.lstrip().startswith("|"): + started = True + table.append((index + 1, _cells(line))) + if len(table) > _MAX_CATALOG_ROWS + 2: + raise CatalogParseError(f"catalog exceeds {_MAX_CATALOG_ROWS}-row limit") + elif started: + break + if len(table) < 3: + raise CatalogParseError(f"catalog under {heading!r} requires a header, separator, and data rows") + if len(table[0][1]) != columns: + raise CatalogParseError(f"catalog under {heading!r} has {len(table[0][1])} columns; expected {columns}") + separator = table[1][1] + if len(separator) != columns or not all(_SEPARATOR_RE.fullmatch(cell) for cell in separator): + raise CatalogParseError(f"catalog under {heading!r} has a malformed separator row") + for line_no, cells in table[2:]: + if len(cells) != columns: + raise CatalogParseError(f"catalog row at line {line_no} has {len(cells)} columns; expected {columns}") + return table[2:] + + +def _unique(rows: list[Any], key_name: str, label: str) -> None: + seen: dict[str, int] = {} + for row in rows: + key = getattr(row, key_name) + if key in seen: + raise CatalogParseError(f"duplicate {label} {key!r} at lines {seen[key]} and {row.line_no}") + seen[key] = row.line_no + + +def parse_top_level_catalog(text: str) -> list[TopLevelRow]: + rows = [ + TopLevelRow( + field=_unquote(cells[0]), + kind=cells[1].lower(), + shape=cells[2].lower(), + lifecycle=tuple(token.strip().lower() for token in cells[3].split(",") if token.strip()), + presence=cells[4].strip().lower(), + identity=_unquote(cells[5]), + references=cells[6].strip().lower(), + owner=cells[7].strip(), + line_no=line_no, + ) + for line_no, cells in _table(text, _TOP_LEVEL_HEADING, 8) + ] + _unique(rows, "field", "top-level field") + return rows + + +def parse_reference_catalog(text: str) -> list[ReferenceRow]: + rows = [ + ReferenceRow( + source_path=_unquote(cells[0]), + domain=_unquote(cells[1]), + phase=cells[2].strip().lower(), + failure=cells[3].strip().lower(), + owner=cells[4].strip(), + line_no=line_no, + ) + for line_no, cells in _table(text, _REFERENCE_HEADING, 5) + ] + seen: dict[tuple[str, str], int] = {} + for row in rows: + if row.key in seen: + raise CatalogParseError(f"duplicate reference edge {row.key!r} at lines {seen[row.key]} and {row.line_no}") + seen[row.key] = row.line_no + return rows + + +def parse_runtime_catalog(text: str) -> list[RuntimeRow]: + rows = [ + RuntimeRow( + key=_unquote(cells[0]), + collection=_unquote(cells[1]), + primary_id=_unquote(cells[2]), + child_paths=tuple(token.strip() for token in _unquote(cells[3]).split(",") if token.strip() != "none"), + owner=cells[4].strip(), + line_no=line_no, + ) + for line_no, cells in _table(text, _RUNTIME_HEADING, 5) + ] + _unique(rows, "key", "runtime family") + return rows + + +def _failure(rule_id: str, message: str, path: str) -> PolicyFailure: + return PolicyFailure(rule_id, message, path) + + +def _expected_kind(field: str) -> str: + if field in _METADATA_FIELDS: + return "metadata" + if field in _COMPOSITION_FIELDS: + return "composition" + return "section" + + +def _schema_shape(schema: dict[str, Any]) -> str: + schema_type = schema.get("type") + if schema_type == "string": + return "scalar" + if schema_type == "array": + return "list" + if schema_type == "object": + return "map" + if schema_type is None and schema.get("default") is None: + return "mapping" + return "unknown" + + +def _expected_presence(field: str) -> str: + model_field = Scenario.model_fields[field] + if model_field.is_required(): + return "required" + value = model_field.default_factory() if model_field.default_factory is not None else model_field.default + if value == "*": + return "optional; default `*`" + if value == "": + return "optional; default empty string" + if value is None: + return "optional; default null" + if value == []: + return "optional; default empty list" + if value == {}: + return "optional; default empty map" + return f"optional; default `{value}`" + + +def _expected_identity(field: str, shape: str) -> str: + if field == "name": + return "scenario_name" + if field == "module": + return "module.id" + if field == "imports": + return "namespace" + if field == "forwarding_agents": + return "forwarding_agent_id" + if shape == "map": + return "map_key" + return "none" + + +def _flatten_children(children: tuple[RuntimeReferenceChild, ...], prefix: str = "") -> tuple[str, ...]: + paths: list[str] = [] + for child in children: + path = ( + f"{prefix}/{child.collection_name}:{child.id_field}" + if prefix + else f"{child.collection_name}:{child.id_field}" + ) + paths.append(path) + paths.extend(_flatten_children(child.children, path)) + return tuple(paths) + + +def _check_top_level(text: str, schema: dict[str, Any]) -> tuple[list[PolicyFailure], list[TopLevelRow]]: + failures: list[PolicyFailure] = [] + try: + rows = parse_top_level_catalog(text) + except CatalogParseError as exc: + return [_failure("sdl-catalog-parse", str(exc), SECTIONS_PATH)], [] + by_field = {row.field: row for row in rows} + model_fields = set(Scenario.model_fields) + schema_fields = set(schema.get("properties", {})) + catalog_fields = set(by_field) + if model_fields != schema_fields or catalog_fields != model_fields: + failures.append( + _failure( + "sdl-catalog-field-set", + f"field sets differ: catalog-only={sorted(catalog_fields - model_fields)}, " + f"model-only={sorted(model_fields - catalog_fields)}, " + f"schema-only={sorted(schema_fields - model_fields)}, model-only-vs-schema={sorted(model_fields - schema_fields)}", + SECTIONS_PATH, + ) + ) + for field in sorted(catalog_fields & model_fields & schema_fields): + row = by_field[field] + expected_shape = _schema_shape(schema["properties"][field]) + if field in HASHMAP_SECTIONS: + expected_shape = "map" + if row.shape != expected_shape: + failures.append( + _failure( + "sdl-catalog-field-shape", + f"{field!r} is {expected_shape}, catalog says {row.shape}", + SECTIONS_PATH, + ) + ) + expected_presence = _expected_presence(field) + if row.presence != expected_presence: + failures.append( + _failure( + "sdl-catalog-field-default", + f"{field!r} is {expected_presence}, catalog says {row.presence}", + SECTIONS_PATH, + ) + ) + expected_identity = _expected_identity(field, expected_shape) + if row.identity != expected_identity: + failures.append( + _failure( + "sdl-catalog-field-identity", + f"{field!r} identity is {expected_identity!r}, catalog says {row.identity!r}", + SECTIONS_PATH, + ) + ) + if row.kind != _expected_kind(field) or row.kind not in _VALID_KINDS: + failures.append( + _failure( + "sdl-catalog-field-kind", + f"{field!r} has invalid kind {row.kind!r}", + SECTIONS_PATH, + ) + ) + if not row.lifecycle or not set(row.lifecycle) <= _VALID_LIFECYCLE: + failures.append( + _failure( + "sdl-catalog-lifecycle", + f"{field!r} has invalid lifecycle tokens {row.lifecycle!r}", + SECTIONS_PATH, + ) + ) + if row.shape not in _VALID_SHAPES or not row.identity or not row.owner: + failures.append( + _failure( + "sdl-catalog-row-incomplete", + f"{field!r} has an incomplete classification", + SECTIONS_PATH, + ) + ) + map_fields = {row.field for row in rows if row.shape == "map"} + if map_fields != set(HASHMAP_SECTIONS): + failures.append( + _failure( + "sdl-catalog-map-set", + f"map fields differ from mapping registry: {sorted(map_fields ^ set(HASHMAP_SECTIONS))}", + SECTIONS_PATH, + ) + ) + if not set(MODULE_HASHMAP_SECTIONS) <= map_fields: + failures.append( + _failure( + "sdl-catalog-module-map-set", + "module export maps are not a subset of catalogued maps", + SECTIONS_PATH, + ) + ) + summary = _SUMMARY_RE.search(text) + actual = { + "top": len(rows), + "meta": sum(row.kind != "section" for row in rows), + "sections": sum(row.kind == "section" for row in rows), + "maps": sum(row.shape == "map" for row in rows), + "lists": sum(row.shape == "list" and row.kind == "section" for row in rows), + } + if summary is None or any(int(summary.group(key)) != value for key, value in actual.items()): + failures.append( + _failure( + "sdl-catalog-summary", + f"checked summary is absent or stale; expected {actual}", + SECTIONS_PATH, + ) + ) + required = set(schema.get("required", [])) + model_required = {name for name, field in Scenario.model_fields.items() if field.is_required()} + if required != model_required: + failures.append( + _failure( + "sdl-catalog-schema-required", + f"published schema required set differs from model: {sorted(required ^ model_required)}", + SCHEMA_PATH, + ) + ) + return failures, rows + + +def _check_references(text: str, top_rows: list[TopLevelRow]) -> list[PolicyFailure]: + try: + rows = parse_reference_catalog(text) + except CatalogParseError as exc: + return [_failure("sdl-catalog-reference-parse", str(exc), REFERENCES_PATH)] + failures: list[PolicyFailure] = [] + by_source = {row.source_path: (row.domain, row.phase, row.failure, row.owner) for row in rows} + if by_source != _REFERENCE_EDGE_EXPECTATIONS: + differing = sorted( + source + for source in by_source.keys() | _REFERENCE_EDGE_EXPECTATIONS.keys() + if by_source.get(source) != _REFERENCE_EDGE_EXPECTATIONS.get(source) + ) + failures.append( + _failure( + "sdl-catalog-reference-row", + f"reference-edge contract differs for: {differing}", + REFERENCES_PATH, + ) + ) + by_key = {row.key: row for row in rows} + for key, domain in sorted(REFERENCE_COMPLETION_TARGETS.items()): + row = by_key.get(key) + if row is None or row.domain != domain: + actual = None if row is None else row.domain + failures.append( + _failure( + "sdl-catalog-reference-domain", + f"{key!r} expects domain {domain!r}, catalog says {actual!r}", + REFERENCES_PATH, + ) + ) + behavior_expectations = { + source: expected + for source, expected in _REFERENCE_EDGE_EXPECTATIONS.items() + if source.startswith("behavior_specifications.*.") + } + for source, expected in behavior_expectations.items(): + if by_source.get(source) != expected: + failures.append( + _failure( + "sdl-catalog-behavior-edge", + f"{source} must match its behavior reference contract", + REFERENCES_PATH, + ) + ) + source_sections = {row.key[0] for row in rows} + top_by_field = {row.field: row for row in top_rows} + for section in source_sections: + top = top_by_field.get(section) + if top is None or top.references != "catalogued": + failures.append( + _failure( + "sdl-catalog-reference-coverage", + f"reference source section {section!r} is not marked catalogued", + SECTIONS_PATH, + ) + ) + for row in top_rows: + if row.references == "catalogued" and row.field not in source_sections: + failures.append( + _failure( + "sdl-catalog-reference-coverage", + f"{row.field!r} is marked catalogued but has no edge row", + REFERENCES_PATH, + ) + ) + return failures + + +def _check_runtime(text: str) -> list[PolicyFailure]: + try: + rows = parse_runtime_catalog(text) + except CatalogParseError as exc: + return [_failure("sdl-catalog-runtime-parse", str(exc), RUNTIME_PATH)] + actual = {row.key: (row.collection, row.primary_id, row.child_paths) for row in rows} + expected = { + family.key: ( + family.collection_name, + family.id_field, + _flatten_children(family.child_refs), + ) + for family in RUNTIME_SERVICE_FAMILIES + } + if actual != expected: + differing = sorted(key for key in actual.keys() | expected.keys() if actual.get(key) != expected.get(key)) + return [ + _failure( + "sdl-catalog-runtime-family", + f"runtime-family catalog differs for: {differing}", + RUNTIME_PATH, + ) + ] + return [] + + +def evaluate_sdl_catalog_parity(repo_root: Path) -> list[PolicyFailure]: + """Return deterministic parity failures for the normative SDL catalogs.""" + required_paths = (SECTIONS_PATH, REFERENCES_PATH, RUNTIME_PATH, SCHEMA_PATH) + missing = [relative for relative in required_paths if not (repo_root / relative).is_file()] + if missing: + return [ + _failure( + "sdl-catalog-missing", + f"required catalog authority is missing: {relative}", + relative, + ) + for relative in missing + ] + try: + schema = json.loads((repo_root / SCHEMA_PATH).read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + return [_failure("sdl-catalog-schema-parse", str(exc), SCHEMA_PATH)] + sections_text = (repo_root / SECTIONS_PATH).read_text(encoding="utf-8") + top_failures, top_rows = _check_top_level(sections_text, schema) + failures = list(top_failures) + failures.extend(_check_references((repo_root / REFERENCES_PATH).read_text(encoding="utf-8"), top_rows)) + failures.extend(_check_runtime((repo_root / RUNTIME_PATH).read_text(encoding="utf-8"))) + return failures + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Validate normative SDL catalog parity.") + parser.add_argument("--repo-root", type=Path, default=REPO_ROOT) + parser.add_argument("--json", action="store_true", help="Emit JSON failures.") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + failures = evaluate_sdl_catalog_parity(args.repo_root) + exceptions_path = args.repo_root / "tools" / "policy" / "exceptions.yaml" + if exceptions_path.is_file(): + failures = apply_exceptions(failures, load_exceptions(args.repo_root)) + if failures: + if args.json: + print(failures_to_json(failures)) + else: + for failure in failures: + print(failure.render(), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/check_sdl_lineage.py b/tools/check_sdl_lineage.py new file mode 100644 index 000000000..6d6e8ba6a --- /dev/null +++ b/tools/check_sdl_lineage.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +# ruff: noqa: E402 +"""Offline integrity gate for the revision-pinned SDL lineage ledger.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +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 aces_contracts.provenance import LineageDisposition, SDLLineageLedgerModel +from aces_sdl._runtime_service_families import RUNTIME_SERVICE_FAMILIES +from pydantic import ValidationError + +from tools.policy.common import PolicyFailure, safe_repo_path + +LEDGER_PATH = "contracts/provenance/sdl-lineage-ledger-v1.json" +AUTHORING_SCHEMA_PATH = "contracts/schemas/sdl/sdl-authoring-input-v1.json" +CONCEPT_FAMILIES_PATH = "contracts/concept-authority/concept-families-v1.json" +REFERENCE_MODELS_PATH = "contracts/concept-authority/reference-models-v1.json" +SCHEMA_PUBLICATION_MANIFEST_PATH = "contracts/schema-publication-manifest.json" +CURRENT_PROSE_PATHS = ( + "docs/explain/sdl/precedents.md", + "docs/explain/sdl/lineage.md", + "docs/explain/sdl/related-work-comparison.md", + "docs/explain/sdl/validation.md", + "implementations/python/packages/aces_sdl/__init__.py", +) +DOI_LINK_RE = re.compile( + r"\[([^\]]+)\]\(https://doi\.org/(10\.\d{4,9}/[^)\s]+)\)", + re.IGNORECASE, +) +YEAR_RE = re.compile(r"\b(?:18|19|20|21)\d{2}\b") + + +def _failure(rule: str, message: str, path: str = LEDGER_PATH) -> PolicyFailure: + return PolicyFailure(rule, message, path) + + +def _load_json(repo_root: Path, rel_path: str) -> object: + path = safe_repo_path(repo_root, rel_path) + if path is None or not path.is_file(): + raise ValueError(f"missing or unsafe repository artifact {rel_path!r}") + return json.loads(path.read_text(encoding="utf-8")) + + +def _canonical_subjects(repo_root: Path) -> set[str]: + schema = _load_json(repo_root, AUTHORING_SCHEMA_PATH) + concepts = _load_json(repo_root, CONCEPT_FAMILIES_PATH) + references = _load_json(repo_root, REFERENCE_MODELS_PATH) + if not isinstance(schema, dict) or not isinstance(schema.get("properties"), dict): + raise ValueError("authoring schema has no properties catalog") + if not isinstance(concepts, dict) or not isinstance(concepts.get("families"), dict): + raise ValueError("concept-family catalog has no families") + if not isinstance(references, dict) or not isinstance(references.get("models"), dict): + raise ValueError("reference-model catalog has no models") + subjects = {f"sdl-field:{name}" for name in schema["properties"]} + subjects.update(f"runtime-family:{family.collection_name}" for family in RUNTIME_SERVICE_FAMILIES) + subjects.update(f"concept-family:{name}" for name in concepts["families"]) + subjects.update(f"reference-model:{name}" for name in references["models"]) + return subjects + + +def _resolve_json_pointer(document: object, pointer: str) -> bool: + if pointer == "#": + return True + if not pointer.startswith("#/"): + return False + current = document + for raw_part in pointer[2:].split("/"): + part = raw_part.replace("~1", "/").replace("~0", "~") + if isinstance(current, dict) and part in current: + current = current[part] + elif isinstance(current, list) and part.isdigit() and int(part) < len(current): + current = current[int(part)] + else: + return False + return True + + +def _validate_authorities(repo_root: Path, ledger: SDLLineageLedgerModel) -> list[PolicyFailure]: + failures: list[PolicyFailure] = [] + cache: dict[str, object] = {} + manifest = _load_json(repo_root, SCHEMA_PUBLICATION_MANIFEST_PATH) + if not isinstance(manifest, dict) or not isinstance(manifest.get("schemas"), list): + return [ + _failure( + "lineage-schema-publication-manifest-invalid", + "schema publication manifest has no schemas catalog", + SCHEMA_PUBLICATION_MANIFEST_PATH, + ) + ] + published_paths = { + entry["contract_id"]: entry["schema_path"] + for entry in manifest["schemas"] + if isinstance(entry, dict) + and isinstance(entry.get("contract_id"), str) + and isinstance(entry.get("schema_path"), str) + } + for subject in ledger.subjects: + artifact = subject.authority.artifact + contract_id = subject.authority.contract_id + if contract_id is not None and published_paths.get(contract_id) != artifact: + failures.append( + _failure( + "lineage-authority-contract-mismatch", + f"{subject.subject_id}: contract {contract_id!r} is not published at {artifact!r}", + ) + ) + path = safe_repo_path(repo_root, artifact) + if path is None or not path.is_file(): + failures.append( + _failure( + "lineage-authority-missing", + f"{subject.subject_id}: missing {artifact!r}", + ) + ) + continue + if subject.disposition is not LineageDisposition.CURRENT: + continue + if path.suffix != ".json": + failures.append( + _failure( + "lineage-current-authority-not-json", + f"{subject.subject_id}: current authority must be JSON", + ) + ) + continue + try: + document = cache.setdefault(artifact, json.loads(path.read_text(encoding="utf-8"))) + except (OSError, json.JSONDecodeError): + failures.append( + _failure( + "lineage-authority-invalid", + f"{subject.subject_id}: invalid JSON authority", + ) + ) + continue + if not _resolve_json_pointer(document, subject.authority.pointer): + failures.append( + _failure( + "lineage-authority-pointer-missing", + f"{subject.subject_id}: pointer {subject.authority.pointer!r} does not resolve in {artifact}", + ) + ) + return failures + + +def _validate_internal_paths(repo_root: Path, ledger: SDLLineageLedgerModel) -> list[PolicyFailure]: + failures: list[PolicyFailure] = [] + refs: set[str] = set() + for citation in ledger.citations: + refs.add(citation.verification_evidence.split("#", 1)[0]) + 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(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:")) + if disposition.notice_artifact: + refs.add(disposition.notice_artifact) + refs.update(boundary.artifact for boundary in disposition.derivation_scope) + for ref in sorted(refs): + path = safe_repo_path(repo_root, ref) + if path is None or not path.is_file(): + failures.append( + _failure( + "lineage-internal-artifact-missing", + f"internal artifact {ref!r} is missing or unsafe", + ) + ) + return failures + + +def _validate_bibliography(ledger: SDLLineageLedgerModel) -> list[PolicyFailure]: + failures: list[PolicyFailure] = [] + doi_identity: dict[str, tuple[str, int]] = {} + for citation in ledger.citations: + if citation.doi is None: + continue + doi = citation.doi.casefold() + identity = (citation.title.casefold(), citation.year) + previous = doi_identity.setdefault(doi, identity) + if previous != identity: + failures.append( + _failure( + "lineage-doi-identity-conflict", + f"DOI {doi!r} has conflicting title/year", + ) + ) + if citation.canonical_url.casefold() != f"https://doi.org/{doi}": + failures.append( + _failure( + "lineage-doi-url-mismatch", + f"{citation.citation_id}: DOI URL does not match DOI", + ) + ) + return failures + + +def _validate_current_prose( + repo_root: Path, + ledger: SDLLineageLedgerModel, + prose_paths: tuple[str, ...] = CURRENT_PROSE_PATHS, +) -> list[PolicyFailure]: + failures: list[PolicyFailure] = [] + doi_years = {citation.doi.casefold(): citation.year for citation in ledger.citations if citation.doi is not None} + for rel_path in prose_paths: + path = safe_repo_path(repo_root, rel_path) + if path is None or not path.is_file(): + failures.append( + _failure( + "lineage-prose-missing", + f"governed current prose {rel_path!r} is missing", + rel_path, + ) + ) + continue + prose = path.read_text(encoding="utf-8") + folded_prose = prose.casefold() + if "direct port" in folded_prose or "ported from" in folded_prose: + failures.append( + _failure( + "lineage-ambiguous-port-claim", + "replace ambiguous port wording with revision-, plane-, boundary-, and compatibility-qualified terms", + rel_path, + ) + ) + for label, doi in DOI_LINK_RE.findall(prose): + expected_year = doi_years.get(doi.casefold()) + label_years = {int(year) for year in YEAR_RE.findall(label)} + if expected_year is not None and label_years and expected_year not in label_years: + failures.append( + _failure( + "lineage-doi-label-year-mismatch", + f"DOI {doi!r} is verified as {expected_year}, but its link label says {sorted(label_years)}", + rel_path, + ) + ) + return failures + + +def evaluate(repo_root: Path = REPO_ROOT) -> list[PolicyFailure]: + try: + payload = _load_json(repo_root, LEDGER_PATH) + ledger = SDLLineageLedgerModel.model_validate(payload) + except (ValueError, json.JSONDecodeError, ValidationError) as exc: + return [_failure("lineage-ledger-invalid", f"ledger validation failed: {exc}")] + failures: list[PolicyFailure] = [] + try: + expected = _canonical_subjects(repo_root) + except (ValueError, json.JSONDecodeError) as exc: + return [_failure("lineage-canonical-catalog-invalid", str(exc))] + actual = {subject.subject_id for subject in ledger.subjects if subject.disposition is LineageDisposition.CURRENT} + missing = sorted(expected - actual) + unexpected = sorted(actual - expected) + if missing: + failures.append( + _failure( + "lineage-current-subjects-missing", + f"missing canonical subjects: {missing}", + ) + ) + if unexpected: + failures.append( + _failure( + "lineage-current-subjects-unexpected", + f"unexpected current subjects: {unexpected}", + ) + ) + failures.extend(_validate_authorities(repo_root, ledger)) + failures.extend(_validate_internal_paths(repo_root, ledger)) + failures.extend(_validate_bibliography(ledger)) + failures.extend(_validate_current_prose(repo_root, ledger)) + return failures + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + failures = evaluate() + if args.json: + print(json.dumps([failure.__dict__ for failure in failures], indent=2)) + else: + for failure in failures: + print(failure.render()) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index 54d6aeea9..b8747f7e3 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -14,11 +14,14 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: if name in { "sdl-authoring-input-v1", "instantiated-scenario-v1", + "instantiated-scenario-snapshot-v1", "scenario-instantiation-request-v1", }: return schemas_dir / "sdl" / f"{name}.json" if name.startswith("backend-manifest-v"): return schemas_dir / "backend-manifest" / f"{name}.json" + if name.startswith("realization-envelope-v"): + return schemas_dir / "realization-envelope" / f"{name}.json" if name.startswith("processor-manifest-v"): return schemas_dir / "processor-manifest" / f"{name}.json" if name.startswith("participant-implementation-manifest-v"): @@ -37,10 +40,16 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: return schemas_dir / "concept-authority" / f"{name}.json" if name == "reusable-asset-trust-policy-v1": return schemas_dir / "asset-trust" / f"{name}.json" + if name == "sdl-lineage-ledger-v1": + return schemas_dir / "provenance" / f"{name}.json" + if name == "associated-artifact-manifest-v1": + return schemas_dir / "associated-artifacts" / f"{name}.json" if name.startswith("semantic-profile-v"): return schemas_dir / "profiles" / f"{name}.json" if name.startswith("backend-profile-v"): return schemas_dir / "profiles" / f"{name}.json" + if name.startswith("scientific-completeness-"): + return schemas_dir / "profiles" / f"{name}.json" if name in { "participant-lifecycle-event-v1", "participant-observation-envelope-v1", diff --git a/tools/policy/adr_policy.yaml b/tools/policy/adr_policy.yaml index 7b351d1c9..a8a0719f2 100644 --- a/tools/policy/adr_policy.yaml +++ b/tools/policy/adr_policy.yaml @@ -168,6 +168,7 @@ module_boundaries: aces_backend_libvirt: - aces_backend_libvirt.target - aces_backend_libvirt.techvault_native + - aces_backend_libvirt.guest_certified_driver aces_backend_protocols: - aces_backend_protocols.manifest - aces_backend_protocols.capabilities @@ -219,6 +220,7 @@ module_boundaries: allowed_top_level_imports: - aces_backend_protocols - aces_backend_stubs + - aces_contracts - aces_processor - aces_sdl forbidden_import_prefixes: @@ -234,16 +236,17 @@ module_boundaries: allowed_top_level_imports: - aces_sdl -# ADR-015: non-test source files under packages/ cap at 600 lines. -# The allowlist (oversized_allowlist.yaml) lists the files that were over -# the cap when ADR-015 landed; each split PR (child of issue #3) removes -# its entry. The allowlist must stay a subset of the *code* constant +# ADR-015: non-test source files under packages/ cap at 500 lines (aligned +# with SonarCloud S104; lowered from 600 per the 2026-07-12 amendment / #561). +# The allowlist (oversized_allowlist.yaml) lists the files that were over the +# cap when the current cap was adopted; each split PR removes its entry. The +# allowlist must stay a subset of the *code* constant # _ADR015_INITIAL_OVERSIZED_FILES in tools/policy/repo_policy.py (the fixed # reference set) — entries can be drained but not added. The locked set is # intentionally NOT mirrored here: keeping it as config the same PR can edit # would make the "shrink only" rule self-satisfiable. oversized_source_files: - line_cap: 600 + line_cap: 500 allowlist_path: tools/policy/oversized_allowlist.yaml scope_roots: - implementations/python/packages diff --git a/tools/policy/oversized_allowlist.yaml b/tools/policy/oversized_allowlist.yaml index 723237d57..531abb5b7 100644 --- a/tools/policy/oversized_allowlist.yaml +++ b/tools/policy/oversized_allowlist.yaml @@ -1,15 +1,34 @@ -# Files over the 600-line cap (tools/policy/adr_policy.yaml :: oversized_source_files). +# Files over the 500-line cap (tools/policy/adr_policy.yaml :: oversized_source_files). # Each entry is a path relative to the repository root and must be one of the # files in _ADR015_INITIAL_OVERSIZED_FILES (tools/policy/repo_policy.py) — the -# fixed reference set. The allowlist may only shrink: each split PR (a child of -# issue #3) removes its file once it's been split below the cap. The modularity -# tracker (#3) closes when this list is empty. Do NOT add new entries — split -# the file instead. (See ADR-015.) +# fixed reference set. The allowlist may only shrink: each split PR removes its +# file once it's been split below the cap. The modularity tracker closes when +# this list is empty. Do NOT add new entries — split the file instead. This set +# was re-baselined to the 500-line cap on 2026-07-12 (#561); see ADR-015 §2 and +# its Amendments section. files: - - implementations/python/packages/aces_processor/models.py - - implementations/python/packages/aces_processor/compiler.py + - implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py + - implementations/python/packages/aces_backend_libvirt/realization.py + - implementations/python/packages/aces_backend_libvirt/techvault_native.py + - implementations/python/packages/aces_backend_stubs/stubs.py + - implementations/python/packages/aces_conformance/conformance.py - implementations/python/packages/aces_contracts/contracts.py + - implementations/python/packages/aces_contracts/workflow.py + - implementations/python/packages/aces_mcp/tools/authoring.py + - implementations/python/packages/aces_mcp/tools/inspection.py + - implementations/python/packages/aces_mcp/tools/reference.py + - implementations/python/packages/aces_operations/_evidence_run_artifact.py + - implementations/python/packages/aces_processor/compiler.py + - implementations/python/packages/aces_processor/models.py - implementations/python/packages/aces_processor/planner.py - - implementations/python/packages/aces_conformance/conformance.py + - implementations/python/packages/aces_runtime/control_plane_api.py + - implementations/python/packages/aces_runtime/control_plane.py + - implementations/python/packages/aces_runtime/workflow_result_contract_checks.py + - implementations/python/packages/aces_sdl/composition.py - implementations/python/packages/aces_sdl/module_registry.py - - implementations/python/packages/aces_mcp/tools/authoring.py + - implementations/python/packages/aces_sdl/orchestration.py + - implementations/python/packages/aces_sdl/participant_behavior.py + - implementations/python/packages/aces_sdl/runtime_mail_service.py + - implementations/python/packages/aces_sdl/runtime_security_monitoring.py + - implementations/python/packages/aces_sdl/semantics/objective_semantics.py + - implementations/python/packages/aces_sdl/semantics/participant_behavior.py diff --git a/tools/policy/repo_policy.py b/tools/policy/repo_policy.py index e143b7acf..0b8c75b8e 100644 --- a/tools/policy/repo_policy.py +++ b/tools/policy/repo_policy.py @@ -114,41 +114,55 @@ def structural_runner(input_document: dict) -> list[PolicyFailure]: # ────────────────────────────────────────────────────────────────────────── -# ADR-015: SDL→processor layering rule + 600-line source-file cap. +# ADR-015: SDL→processor layering rule + 500-line source-file cap. # # These two gates catch unintentional regressions in normal contributions: # a developer who accidentally writes `import aces_processor` in `aces_sdl/`, -# or one who pushes a >600-line file, or a split PR that forgets to drain +# or one who pushes a >500-line file, or a split PR that forgets to drain # its allowlist entry. The policy and its YAML config are PR-mutable; PR # review (not this code) defends against deliberate weakening. See ADR-015. # ────────────────────────────────────────────────────────────────────────── _POLICY_CONFIG_PATH = "tools/policy/adr_policy.yaml" -# The set of source files that were over the 600-line cap when ADR-015 -# landed. The size-cap allowlist (tools/policy/oversized_allowlist.yaml) may -# only ever be a SUBSET of this set: entries are removed as child PRs of #3 -# split their file, and no new entry may be added. This is a code constant — -# not config in adr_policy.yaml — so that "the allowlist only shrinks" is -# enforced against a fixed reference rather than against an input the same PR -# can edit. Adding a 15th oversized file therefore requires a diff to this -# module, which PR review scrutinises as policy, not as data noise. +# The set of source files that were over the source-file cap when the current +# cap was adopted. The cap was originally 600 lines (ADR-015 landing); it was +# lowered to 500 on 2026-07-12 (#561, ADR-015 Amendments), and this reference +# set was re-baselined to the files over 500 lines at that time. The size-cap +# allowlist (tools/policy/oversized_allowlist.yaml) may only ever be a SUBSET +# of this set: entries are removed as split PRs shrink their file, and no new +# entry may be added. This is a code constant — not config in adr_policy.yaml — +# so that "the allowlist only shrinks" is enforced against a fixed reference +# rather than against an input the same PR can edit. Adding an oversized file +# therefore requires a diff to this module, which PR review scrutinises as +# policy, not as data noise. _ADR015_INITIAL_OVERSIZED_FILES: frozenset[str] = frozenset( { - "implementations/python/packages/aces_processor/models.py", - "implementations/python/packages/aces_processor/manager.py", - "implementations/python/packages/aces_processor/compiler.py", - "implementations/python/packages/aces_sdl/validator.py", - "implementations/python/packages/aces_contracts/contracts.py", - "implementations/python/packages/aces_processor/planner.py", - "implementations/python/packages/aces_processor/control_plane.py", - "implementations/python/packages/aces_conformance/conformance.py", + "implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py", + "implementations/python/packages/aces_backend_libvirt/realization.py", + "implementations/python/packages/aces_backend_libvirt/techvault_native.py", "implementations/python/packages/aces_backend_stubs/stubs.py", - "implementations/python/packages/aces_sdl/module_registry.py", - "implementations/python/packages/aces_processor/control_plane_api.py", + "implementations/python/packages/aces_conformance/conformance.py", + "implementations/python/packages/aces_contracts/contracts.py", + "implementations/python/packages/aces_contracts/workflow.py", "implementations/python/packages/aces_mcp/tools/authoring.py", "implementations/python/packages/aces_mcp/tools/inspection.py", + "implementations/python/packages/aces_mcp/tools/reference.py", + "implementations/python/packages/aces_operations/_evidence_run_artifact.py", + "implementations/python/packages/aces_processor/compiler.py", + "implementations/python/packages/aces_processor/models.py", + "implementations/python/packages/aces_processor/planner.py", + "implementations/python/packages/aces_runtime/control_plane.py", + "implementations/python/packages/aces_runtime/control_plane_api.py", + "implementations/python/packages/aces_runtime/workflow_result_contract_checks.py", + "implementations/python/packages/aces_sdl/composition.py", + "implementations/python/packages/aces_sdl/module_registry.py", "implementations/python/packages/aces_sdl/orchestration.py", + "implementations/python/packages/aces_sdl/participant_behavior.py", + "implementations/python/packages/aces_sdl/runtime_mail_service.py", + "implementations/python/packages/aces_sdl/runtime_security_monitoring.py", + "implementations/python/packages/aces_sdl/semantics/objective_semantics.py", + "implementations/python/packages/aces_sdl/semantics/participant_behavior.py", } ) @@ -505,7 +519,7 @@ def _check_drain(allowlist: frozenset[str], allowlist_path: str) -> list[PolicyF "oversized-allowlist-locked", ( f"'{path}' is in {allowlist_path} but is not one of the files that were over the " - "600-line cap when ADR-015 landed (_ADR015_INITIAL_OVERSIZED_FILES in " + "source-file cap when it was adopted (_ADR015_INITIAL_OVERSIZED_FILES in " "tools/policy/repo_policy.py); the allowlist may only shrink — split the file instead of adding it" ), allowlist_path, diff --git a/tools/real-daemon/README.md b/tools/real-daemon/README.md index 42cc9f03d..61fae3458 100644 --- a/tools/real-daemon/README.md +++ b/tools/real-daemon/README.md @@ -56,3 +56,44 @@ python real_daemon_smoke.py # or: python libvirt_smoke.py For seeds/disks outside `/var/lib/libvirt/images`, the host needs `security_driver = "none"` and `user/group = "root"` in `/etc/libvirt/qemu.conf` (the AWS script sets these automatically). + +## Guest-certified realization proof (ASR-519, issue #715) + +`libvirt_smoke.py` proves substrate reconciliation/teardown at the *daemon* level. +The **guest-certified** proof goes one layer deeper: it boots a guest-observing +appliance through the production apply path and reads concern facts back **from +inside the guest** (resource allocation, network addressing, file content, and +service state), freshness-bound to a per-run challenge, then verifies teardown. +Domain existence alone never satisfies it. + +The reproducible operator/self-hosted command is: + +```sh +# Against a real libvirt/QEMU daemon (qemu:///system). Boots the appliance, +# certifies from inside the guest, writes a machine-readable evidence artifact, +# and returns non-zero on any failed stage. +aces libvirt techvault guest-certify \ + --scenario examples/scenarios/techvault-guest-certified.sdl.yaml \ + --project-dir . --run-id guest-proof-1 --yes +``` + +It emits the `aces.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 +guest report is bound to a redacted control-plane operation reference, the fresh +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 \ + uv run pytest -m integration \ + implementations/python/tests/test_libvirt_backend_guest_certified_real_libvirt.py +``` + +Both are skipped by the default hermetic `nox verify` graph, which never requires +libvirt, QEMU/KVM, privileges, a host image, network access, or credentials — the +guest-certified proof is an explicit separate gate. The same host requirements +apply (`security_driver = "none"` + `user/group = "root"` in +`/etc/libvirt/qemu.conf` when boot artifacts and the run-local guest fact channel +live outside `/var/lib/libvirt/images`; the AWS script sets these automatically). diff --git a/tools/real-daemon/evidence/guest-certified-asr519-20260712T031842Z.json b/tools/real-daemon/evidence/guest-certified-asr519-20260712T031842Z.json new file mode 100644 index 000000000..c99d82e2a --- /dev/null +++ b/tools/real-daemon/evidence/guest-certified-asr519-20260712T031842Z.json @@ -0,0 +1,491 @@ +{ + "backend": { + "capability_profile": { + "observation_contract_gaps": [], + "participant_runtime_contract_gaps": [] + }, + "manifest": { + "capabilities": { + "evaluator": null, + "observation": null, + "orchestrator": null, + "participant_runtime": { + "constraints": { + "simulation_disclosure": "deterministic-simulation: no live libvirt domain execution; see docs/decisions/issue-614-libvirt-participant-runtime.md" + }, + "feature_support": [ + { + "constraint_refs": [], + "disclosure_refs": [ + "docs/decisions/issue-614-libvirt-participant-runtime.md" + ], + "feature": "action_contracts", + "support_level": "disclosed_weak" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "docs/decisions/issue-614-libvirt-participant-runtime.md" + ], + "feature": "observation_boundaries", + "support_level": "disclosed_weak" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "docs/decisions/issue-614-libvirt-participant-runtime.md" + ], + "feature": "behavior_history", + "support_level": "disclosed_weak" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "docs/decisions/issue-614-libvirt-participant-runtime.md" + ], + "feature": "state_transitions", + "support_level": "disclosed_weak" + }, + { + "constraint_refs": [], + "disclosure_refs": [ + "docs/decisions/issue-614-libvirt-participant-runtime.md" + ], + "feature": "contention", + "support_level": "disclosed_weak" + } + ], + "name": "libvirt-deterministic-participant-runtime", + "supported_behavior_features": [ + "action_contracts", + "behavior_history", + "observation_boundaries", + "state_transitions" + ], + "supported_interaction_features": [ + "contention" + ], + "supported_participant_roles": [ + "red" + ] + }, + "provisioner": { + "constraints": {}, + "max_total_nodes": null, + "name": "libvirt-provisioner", + "supported_account_features": [ + "disabled", + "groups", + "home", + "shell" + ], + "supported_content_types": [ + "file" + ], + "supported_node_types": [ + "switch", + "vm" + ], + "supported_os_families": [ + "linux" + ], + "supports_accounts": true, + "supports_acls": false + } + }, + "compatibility": { + "processors": [ + "aces-reference-processor" + ] + }, + "concept_bindings": [ + { + "family": "assets", + "scope": "capabilities.provisioner.supported_node_types" + }, + { + "family": "assets", + "scope": "capabilities.provisioner.supported_os_families" + }, + { + "family": "tools-and-artifacts", + "scope": "capabilities.provisioner.supported_content_types" + }, + { + "family": "identities", + "scope": "capabilities.provisioner.supported_account_features" + } + ], + "constraints": {}, + "identity": { + "name": "libvirt-qemu", + "version": "0.19.1" + }, + "realization_envelope": { + "configuration_digest": "sha256:b33ad469eaf1a47963e49da8adc7376fc22880ac3e6126b427239daa00dc6d99", + "contract_id": "realization-envelope-v1", + "digest": "sha256:8416a600a6f1864e1b80e49fa60eef5f423f9249f2ea7dc56e14d8a669e33e2e", + "envelope_id": "libvirt-qemu.guest-certified-appliance.v1", + "schema_version": "realization-envelope/v1" + }, + "realization_support": [ + { + "constraints": {}, + "disclosure_kinds": [ + "backend-manifest-v2", + "operation-status-v1", + "runtime-snapshot-v1" + ], + "domain": "runtime-realization", + "support_mode": "constrained", + "supported_constraint_kinds": [ + "account-feature", + "content-type", + "node-type", + "os-family" + ], + "supported_exact_requirement_kinds": [ + "declared-capability-match" + ] + } + ], + "schema_version": "backend-manifest/v2", + "supported_contract_versions": [ + "backend-manifest-v2", + "realization-envelope-v1", + "provisioning-plan-v1", + "operation-receipt-v1", + "operation-status-v1", + "runtime-snapshot-v1", + "participant-episode-state-envelope-v1", + "participant-episode-history-event-stream-v1", + "participant-behavior-history-event-stream-v1", + "participant-shared-state-record-v1", + "participant-joint-action-record-v1", + "participant-time-management-context-v1" + ] + }, + "realization_provenance": { + "backend": "libvirt-qemu", + "basis": "daemon-observed-substrate", + "cleanup_verified": true, + "evidence_source_mode": "guest-certified", + "substrate_realized": true + } + }, + "compiled_artifact": { + "compiled_address_sets": { + "action_contracts": [], + "networks": [ + "provision.network.guest-net" + ], + "node_deployments": [ + "provision.node.guest-vm" + ], + "objectives": [], + "observation_boundaries": [], + "participant_behaviors": [] + }, + "compiled_model_fingerprint": "sha256:b0ce6d650a49f579592b7746e84ab43e73ca3d31a7b3e2dd2a6620eb2fcee654", + "processor": "aces-reference-processor" + }, + "defensive_evidence": { + "captured_at": "2026-07-12T03:20:52.516205+00:00", + "evaluator_evidence_channels": [], + "evidence_kind": "telemetry", + "evidence_source": "structural-evaluator-channel", + "loss_disclosure": "Deterministic mode: no live SOC substrate is booted. Wazuh/SOC defensive evidence is reported as the evaluator-only evidence channels declared by the scenario observation boundary, not upstream Wazuh detection output; no detection-quality claim is made. Daemon-observed libvirt substrate state is present, but it is not guest SOC observation.", + "payload_summary": "Evaluator-only Wazuh/SOC and policy-decision evidence channels are declared and kept off the participant view; neither evidence mode claims guest SOC readback.", + "redaction_state": "withheld", + "sensitivity": "restricted", + "visibility": "evaluator-only" + }, + "evaluator_outcome": { + "history": [ + { + "detail": "Scenario-evidence evaluator outcome derived from the structural participant proof.", + "details": {}, + "event_type": "evaluation_completed", + "evidence_refs": [ + "participant_action_proof" + ], + "max_score": null, + "passed": true, + "score": null, + "status": "ready", + "timestamp": "2026-07-12T03:20:52.516205+00:00" + } + ], + "limitations": [ + "Evaluator outcome reflects the structural participant-loop proof; the libvirt backend ships no generic evaluator component, so this is a evidence-run evaluator record, not a generic backend evaluator result." + ], + "result": { + "detail": "Structural participant-loop proof over the libvirt deterministic participant runtime.", + "evidence_refs": [ + "participant_action_proof", + "negative_boundary_checks" + ], + "max_score": null, + "observed_at": "2026-07-12T03:20:52.516205+00:00", + "passed": true, + "resource_type": "participant-loop-evaluation", + "run_id": "scenario-evidence", + "score": null, + "state_schema_version": "evaluation-result-state/v1", + "status": "ready", + "updated_at": "2026-07-12T03:20:52.516205+00:00" + } + }, + "evidence_source_mode": "guest-certified", + "invariant_ledger_refs": { + "action_contracts": [], + "evidence_refs": [ + "participant_action_proof", + "terminal_observation", + "defensive_evidence", + "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.", + "observation_boundaries": [], + "participant_behaviors": [], + "scenario_content_sha256": "sha256:54cfbf2301510771eff148748e60dc0917efbbf695270d9a5b837ee80bcbb454", + "scenario_name": "techvault-guest-certified" + }, + "limitations": [ + "The libvirt participant runtime uses the deterministic domain adapter; no live participant domain is executed (issue #614).", + "Wazuh/SOC evidence is evaluator-only structural evidence; daemon substrate state is not promoted to guest or application observation.", + "Deterministic mode does not realize a live libvirt substrate; topology and defensive evidence channels are compiled/structural, explicitly disclosed as not-live observations." + ], + "negative_boundary_checks": { + "all_internal_surfaces_withheld": true, + "checks": [], + "disclosure": "Negative boundary checks are evaluator-side derived analysis, not participant observations.", + "method": "Structural boundary analysis over the compiled observation boundary (hidden_refs) and the participant exposure policy (empty visible/disclosed refs). The participant action surface does not expose the internal DB, Wazuh, evaluator, or policy-gate surfaces.", + "value_status": "reported" + }, + "non_claims": [ + "No Wazuh detection-quality claim.", + "No model-defense robustness claim.", + "No byte-equivalence or application-internals equivalence claim between libvirt appliances and APTL containers.", + "No full semantic-equivalence claim beyond the invariant ledger in Brad-Edwards/aces#600." + ], + "participant_action_proof": { + "admitted_action_addresses": [], + "diagnostics": [], + "episode_states": {}, + "lifecycle_clean": true, + "participant_disclosed_refs": [], + "participant_visible_refs": [], + "runtime": "libvirt-deterministic-participant-runtime", + "structural_validation_note": "Deep behavior-history and episode-snapshot invariant validation is performed by the issue #614 participant-runtime test suite (processor-layer iterators); this artifact records the libvirt participant-runtime lifecycle outcome." + }, + "realization_facts": { + "authored": { + "scenario_name": "techvault-guest-certified", + "source": "authored" + }, + "binding": { + "boot_artifact_digests": { + "initramfs": "sha256:303be8a7ce5a5b9b32c68a9b6469e24034483ef6d978bbcdd2e29ca577a29efd", + "kernel": "sha256:bb4b6f0a792f26999ca7d881a183ec5ab94239073448ddd12cd28e579860face" + }, + "configuration_digest": "sha256:b33ad469eaf1a47963e49da8adc7376fc22880ac3e6126b427239daa00dc6d99", + "connection_uri_digest": "sha256:9bea68fb783f6b3b82c458cf8ce5e62508864d50076287b24ca436af2d110eeb", + "driver": "guest-certified-appliance", + "driver_configuration_digest": "sha256:0a314d970cd037fd46ebde0576ca601401e146181dd1549e525bab208a2c9e78", + "name_prefix_digest": "sha256:5ee63117aa996ced3460eb178f900164350fc8a3b17af78970decb6eed534b93", + "realization_envelope_digest": "sha256:8416a600a6f1864e1b80e49fa60eef5f423f9249f2ea7dc56e14d8a669e33e2e" + }, + "cleanup": { + "source": "driver-reported", + "status": "verified" + }, + "daemon_observed": { + "domains": [ + { + "address": "provision.node.guest-vm", + "architecture": "x86_64", + "image_policy": "generated-initramfs-appliance", + "memory_mib": 128, + "name": "aces-evidence-guest-vm", + "network_attachments": [ + "provision.network.guest-net" + ], + "observation_source": "daemon-observed", + "vcpus": 1 + } + ], + "networks": [ + { + "address": "provision.network.guest-net", + "cidr": "192.0.2.0/24", + "forward_mode": "none", + "gateway": "192.0.2.1", + "internal": true, + "name": "aces-evidence-guest-net", + "observation_source": "daemon-observed" + } + ], + "source": "daemon-observed" + }, + "driver_reported": { + "realized_addresses": [ + "provision.network.guest-net", + "provision.node.guest-vm" + ], + "source": "driver-reported" + }, + "guest_observed": { + "certifying": true, + "challenge": "fe994cd2e7866dd60b98e74e64a88cfb", + "domains": [ + { + "accounts": [], + "address": "provision.node.guest-vm", + "architecture": "x86_64", + "content": [ + "/etc/aces/marker|aabb38db6cd478c6dcbf41b2973d99a8e72cafe9a026df5808d5015452b38a55|644" + ], + "correlation": "sha256:e02c8435b23e310dbe541c17e94a776cf501b290b07e398dbb7a67c89c23d9bb", + "memory_mib": 78, + "network": [ + "52:54:00:c4:7e:78|192.0.2.10" + ], + "services": [ + "beacon|9000|1|1" + ], + "vcpus": 1 + } + ], + "observed_at": "2026-07-12T03:20:52.159020+00:00", + "operation_ref": "sha256:c18ba445c2cd1183774347891759b768d4863493c8e39626ed8885977031630c", + "probe_policy": "serial-fact-channel/v1", + "source": "guest-observed" + }, + "planned": { + "network_addresses": [ + "provision.network.guest-net" + ], + "node_addresses": [ + "provision.node.guest-vm" + ], + "source": "planned" + } + }, + "realized_form_disclosures": [ + { + "authored_ref": null, + "basis": "backend-realized", + "concern_id": "libvirt-backend-selection", + "concern_kind": "backend-selection", + "disclosure": "The libvirt-qemu backend supplied the run; live claims are limited to independently daemon-observed substrate fields.", + "evidence_refs": [], + "realized_by_ref": { + "ref_digest": null, + "ref_id": "libvirt-qemu", + "ref_kind": "backend", + "ref_path": null, + "ref_version": "0.19.1" + }, + "realized_ref": null, + "realized_value_summary": "libvirt-qemu backend (0.19.1); substrate daemon-observed at bounded fields." + }, + { + "authored_ref": null, + "basis": "backend-realized", + "concern_id": "libvirt-participant-implementation", + "concern_kind": "participant-implementation", + "disclosure": "The participant action proof uses the deterministic domain adapter; live domain execution is not performed.", + "evidence_refs": [], + "realized_by_ref": { + "ref_digest": null, + "ref_id": "libvirt-qemu", + "ref_kind": "backend", + "ref_path": null, + "ref_version": "0.19.1" + }, + "realized_ref": null, + "realized_value_summary": "Deterministic libvirt participant runtime (no live domain execution); see issue #614." + } + ], + "realized_topology": { + "basis": "mixed-source", + "disclosure": "Compiled topology remains planned; the native surface contains only independently daemon-observed substrate fields.", + "native_surface": { + "domains": [ + "aces-evidence-guest-vm" + ], + "networks": [ + "aces-evidence-guest-net" + ], + "source": "daemon-observed", + "substrate": "libvirt-qemu-initramfs" + }, + "network_attachment_matrix": { + "guest-vm": [ + "guest-net" + ] + }, + "networks": [ + { + "address": "provision.network.guest-net", + "cidr": "192.0.2.0/24", + "gateway": "192.0.2.1", + "internal": true, + "name": "guest-net", + "source": "planned" + } + ], + "nodes": [ + { + "address": "provision.node.guest-vm", + "name": "guest-vm", + "networks": [ + "guest-net" + ], + "node_type": "vm", + "os_family": "linux", + "services": [ + { + "name": "beacon", + "port": 9000, + "protocol": "tcp" + } + ], + "source": "planned" + } + ] + }, + "recorded_at": "2026-07-12T03:20:52.516205+00:00", + "redaction_provenance": { + "policy": "Only allowlisted, bounded fields are copied into the artifact. Raw libvirt XML, domain UUIDs, QEMU command lines, host paths, connection URIs, credentials, private keys, and backend-private inspect payloads are never written.", + "provenance_refs": [ + "docs/decisions/issue-615-libvirt-paper-evidence-preflight.md", + "docs/decisions/issue-614-libvirt-participant-runtime.md" + ], + "redacted_field_classes": [ + "raw-libvirt-xml", + "domain-uuid", + "qemu-command-line", + "host-path", + "connection-uri", + "credential", + "private-key", + "backend-private-inspect-payload" + ] + }, + "run_id": "asr519-20260712T031842Z", + "scenario": { + "content_sha256": "sha256:54cfbf2301510771eff148748e60dc0917efbbf695270d9a5b837ee80bcbb454", + "name": "techvault-guest-certified", + "relative_path": "examples/scenarios/techvault-guest-certified.sdl.yaml", + "version": "*" + }, + "schema": "aces.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.", + "form": "behavior-history-equivalent" + } +} diff --git a/tools/real-daemon/run_aws_guest_certify.sh b/tools/real-daemon/run_aws_guest_certify.sh new file mode 100755 index 000000000..b329eca34 --- /dev/null +++ b/tools/real-daemon/run_aws_guest_certify.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Provision an ephemeral AWS EC2 host with a real libvirt/QEMU daemon, run the +# guest-certified realization proof (ASR-519, issue #715) against it through the +# production apply path, pull back the redaction-safe machine-readable evidence +# artifact, then tear everything down. This is the guest-observed counterpart to +# run_aws_smoke.sh (which proves daemon-level reconciliation with a cirros disk). +# +# Usage: +# AWS_PROFILE=proof AWS_REGION=us-east-2 tools/real-daemon/run_aws_guest_certify.sh [--keep] +# +# --keep leave the instance running (skip teardown) for manual poking. +# +# On success the emitted evidence JSON is copied to +# tools/real-daemon/evidence/guest-certified-.json +# The instance uses TCG (software emulation); no bare-metal/nested-virt needed. +set -euo pipefail + +PROFILE="${AWS_PROFILE:-proof}" +REGION="${AWS_REGION:-us-east-2}" +INSTANCE_TYPE="${INSTANCE_TYPE:-c5.2xlarge}" +RUN_ID="${RUN_ID:-aws-guest-certified}" +KEEP=0 +[ "${1:-}" = "--keep" ] && KEEP=1 + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +WORK="$(mktemp -d)" +KEY="$WORK/aces-guest-test.pem" +NAME="aces-guest-certify-test" +AWS=(aws --profile "$PROFILE" --region "$REGION") + +cleanup_aws() { + [ "$KEEP" = "1" ] && { echo "--keep: leaving instance ${IID:-?} (${IP:-?}) up"; return; } + echo "=== teardown ===" + [ -n "${IID:-}" ] && "${AWS[@]}" ec2 terminate-instances --instance-ids "$IID" >/dev/null 2>&1 || true + [ -n "${IID:-}" ] && "${AWS[@]}" ec2 wait instance-terminated --instance-ids "$IID" 2>/dev/null || true + [ -n "${SG:-}" ] && "${AWS[@]}" ec2 delete-security-group --group-id "$SG" >/dev/null 2>&1 || true + "${AWS[@]}" ec2 delete-key-pair --key-name "$NAME" >/dev/null 2>&1 || true + echo "torn down." +} +trap cleanup_aws EXIT + +echo "=== identity ==="; "${AWS[@]}" sts get-caller-identity --query Account --output text + +AMI=$("${AWS[@]}" ssm get-parameter --name /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id --query Parameter.Value --output text) +MYIP=$(curl -s https://checkip.amazonaws.com) +VPC=$("${AWS[@]}" ec2 describe-vpcs --filters Name=isDefault,Values=true --query 'Vpcs[0].VpcId' --output text) +SUBNET=$("${AWS[@]}" ec2 describe-subnets --filters Name=default-for-az,Values=true --query 'Subnets[0].SubnetId' --output text) + +"${AWS[@]}" ec2 delete-key-pair --key-name "$NAME" >/dev/null 2>&1 || true +"${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 \ + || "${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 + +cat > "$WORK/userdata.sh" <<'UD' +#!/bin/bash +set -x +export DEBIAN_FRONTEND=noninteractive +apt-get update -y +apt-get install -y qemu-system-x86 qemu-utils libvirt-daemon-system libvirt-clients libvirt-dev genisoimage python3-dev pkg-config build-essential curl rsync busybox-static cpio +systemctl enable --now libvirtd +usermod -aG libvirt,kvm ubuntu +# The guest-observing appliance builder defaults to /usr/bin/busybox. +[ -x /usr/bin/busybox ] || ln -sf "$(command -v busybox)" /usr/bin/busybox +# test-host libvirt config so boot artifacts + the guest fact channel outside +# /var/lib/libvirt/images are readable/writable by the qemu process. +sed -i 's/^#*security_driver *=.*/security_driver = "none"/' /etc/libvirt/qemu.conf +grep -q '^security_driver' /etc/libvirt/qemu.conf || echo 'security_driver = "none"' >> /etc/libvirt/qemu.conf +sed -i 's/^#*user *=.*/user = "root"/; s/^#*group *=.*/group = "root"/' /etc/libvirt/qemu.conf +systemctl restart libvirtd +touch /var/lib/cloud/userdata-done +UD + +IID=$("${AWS[@]}" ec2 run-instances --image-id "$AMI" --instance-type "$INSTANCE_TYPE" \ + --key-name "$NAME" --security-group-ids "$SG" --subnet-id "$SUBNET" --associate-public-ip-address \ + --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=30,VolumeType=gp3}' \ + --user-data "file://$WORK/userdata.sh" \ + --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$NAME}]" \ + --query 'Instances[0].InstanceId' --output text) +echo "instance: $IID" +"${AWS[@]}" ec2 wait instance-running --instance-ids "$IID" +IP=$("${AWS[@]}" ec2 describe-instances --instance-ids "$IID" --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) +echo "public ip: $IP" + +SSHOPT=(-i "$KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=15) +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" +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 +# 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 + +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" + +echo "=== run guest-certified proof ===" +ssh "${SSHOPT[@]}" ubuntu@"$IP" "sudo bash -lc 'cd /home/ubuntu/aces && implementations/python/.venv/bin/python -c \" +from pathlib import Path +from aces_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\\\")) +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" +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 \ + "$REPO_ROOT/tools/real-daemon/evidence/guest-certified-$RUN_ID.json" +echo "pulled: tools/real-daemon/evidence/guest-certified-$RUN_ID.json"